Паттерн стратегии: первый шаблон проектирования, который нужно понять

Начнем с проблемы!

Вы должны разработать симулятор уток, в котором вы должны показать плавание уток, кряканье или любую другую функцию, которую вы можете придумать. Итак, как объектно-ориентированный проектировщик, вы могли бы подумать о создании суперкласса Duck, от которого наследуются все подклассы Duck.

public class Duck {
swim()
quack()
display() // abstract method
//other duck based methods
}

На данный момент двумя типами уток могут быть MallardDuck и RedHeadDuck, представленные подклассами, наследующими класс Duck.

public class MallardDuck extends Duck {
display() // Each duck implements its own display behaviour
// looks like a mallard
}
public class RedHeadDuck extends Duck {
display() // Each duck implements its own display behaviour
//looks like a redhead
}

Это выглядит хорошо до сих пор, не так. Теперь предположим, что я хочу, чтобы утки могли летать, т. е. у уток должно быть поведение полета, которое можно представить с помощью fly(). Вы сразу же воспользуетесь своими навыками объектно-ориентированного программирования и добавите fly() в суперкласс Duck, и все утки унаследуют его.

public class Duck {
swim()
quack()
display()      // abstract method
fly()          //flying behaviour
//other duck based methods
}

Но затем возникает серьезная проблема в вашем дизайне, поскольку я никогда не говорил вам, что все типы уток должны летать. Допустим, существует около 48 различных типов уток, т. е. 48 подклассов, наследующих класс Duck. Но эти подклассы включают RubberDuck (особый вид утки из резины!), который тоже не должен летать! (Как резиновая уточка может летать?.. как все просто). Таким образом, два основных момента касаются вашего дизайна:

  • Не все подклассы должны иметь fly() , т. е. некоторые подклассы должны переопределять fly(), чтобы ничего не делать.
  • Каждый подкласс, имеющий поведение fly, может по-разному реализовать свою функцию fly().

Возможно, вы подумали о переопределении fly() в каждом подклассе, о котором я расскажу позже. Но таким образом вам придется просматривать и, возможно, переопределять fly() для каждого нового подкласса Duck, который когда-либо добавлялся в систему. Теперь вы, должно быть, поняли, что наследование, вероятно, вам здесь не поможет.

Интерфейсы могут помочь…?

Вот вам и новый подход к дизайну с помощью интерфейсов. Вы создадите интерфейс для поведения в полете, а также для поведения кряканья (Теперь вы стали умнее, как вы знаете, точно так же, как некоторые утки не могут летать, некоторые также не могут крякать или могут пищать - другое шарлатанский стиль!).

interface Flyable {
fly()
}
interface Quackable {
quack()
}

И подклассы утки будут реализовывать интерфейсы соответственно.

public class MallardDuck extends Duck implements Flyable, Quackable {
display()
quack()
fly()
}
public class RubberDuck extends Duck implements Quackable {
display()
quack()
//doesn't implement flyable interface
}
public class DecoyDuck extends Duck{
display()
// DecoyDuck can't quack and fly, hence implemented no interfaces
}

Проблема в этом дизайне

Теперь мы знаем, что не все подклассы должны иметь поведение летать или крякать, поэтому наследование не является правильным ответом. Но в то время как подклассы, реализующие поведение Flyable и/или Quackable, решают часть проблемы (нет неуместно летающих резиновых уточек), это полностью уничтожает повторное использование кода для этих поведений, так что это станет совершенно другой проблемой обслуживания. Потому что, скажем, если я попрошу вас немного изменить поведение мухи, вам придется изменить его во всех 48 подклассах!! (больше изменений, больше вероятность ошибок..)

Теперь ваш разум будет в замешательстве, так как ни наследование, ни интерфейсы не помогут вам в этой ситуации.

К счастью, для этой ситуации существует принцип проектирования.

Определите аспекты вашего приложения, которые различаются, и отделите их от того, что остается неизменным.

Вот еще один способ понять этот принцип: возьмите изменяющиеся части и инкапсулируйте их, чтобы позже вы могли расширять или изменять изменяющиеся части, не затрагивая неизменяемые части.

Применение этого принципа в вашем дизайне

Теперь, чтобы отделить «части, которые меняются, от тех, которые остаются неизменными», мы создадим два набора классов (полностью отдельно от Duck), один для летать и один для кряканья. . Каждый набор классов будет содержать реализацию своего соответствующего поведения. Например, у нас может быть один класс, реализующий кряканье, другой — писк (еще один тип кряканья) и третий — реализующий тишину!

Отныне поведения Duck будут жить в отдельных классах — классах, которые реализуют определенный интерфейс поведения. Таким образом, нашим подклассам Duck не нужно знать никаких подробностей реализации своего поведения. Это приводит к другому принципу проектирования, который заключается в следующем:

Программируйте интерфейс, а не реализацию.

Итак, ранее мы полагались на реализацию поведения в подклассах. Мы были привязаны к использованию этой конкретной реализации, и не было места для изменения поведения (кроме написания дополнительного кода).

В нашем новом дизайне подклассы Duck будут использовать поведение, представленное интерфейсом(FlyBehaviourr и QuackBehaviour), так что фактическая реализация поведения (другими словами, конкретное конкретное поведение, закодированное в классе, реализующем FlyBehaviour и QuackBehaviour) не будет заблокировано в подклассе Duck.

Реализация утиного поведения

interface FlyBehaviour {
fly()
}
interface QuackBehaviour {
quack()
}
//Different types of Flying behaviours
public class FlyingWithWings implements FlyBehaviour{
fly() {
// implements the duck flying 
}
}
public class FlyNoWay implements FlyBehaviour {
fly() {
// do nothing... 
}
}
//Different types of Quacking behaviours
public class Quack implements QuackBehaviour {
quack() {
//implements the duck quacking
}
}
public class Squeak implements QuackBehaviour {
quack() {
//implements the duck squeaking
}
}
public class MuteQuack implements QuackBehaviour {
quack() {
//implements the mute quacking
}
}

Интеграция поведения утки

  • Сначала мы добавим две переменные экземпляра в суперкласс Duck с именами flybehaviour и quackbehaviour, которые объявлены как интерфейсные типы. (не конкретный тип реализации класса). Каждый объект-утка будет устанавливать эти переменные экземпляра полиморфно для ссылки на конкретный класс поведения, который он хотел бы использовать во время выполнения (FlyWithWings, Squeak и т. д.).
public class Duck {
FlyBehaviour flybehaviour;
QuackBehaviour quackbehaviour;
performQuack()
swim()
display()
performFly()
//other duck like methods
}

Мы заменим fly() и quack() в классе Duck аналогичными методами, называемыми performQuack()и performFly(). ; мы увидим, как они работают в следующем пункте.

  • Реализация performQuack():
public class Duck {
QuackBehaviour quackbehaviour;
//more
  public void performQuack() {
     quackbehaviour.quack();
}

Вместо того, чтобы обрабатывать само поведение шарлатана, объект Duck делегирует поведение объекту, на который ссылается quackbehaviour.

  • Теперь, как реализовать подкласс MallardDuck и установить переменные экземпляра flybehaviour и quackbehaviour:
public class MallardDuck extends Duck {
    public MallardDuck() {
        quackbehaviour = new Quack();
        flybehaviour = new FlyWithWings();
}
public void display() {
    System.out.println("I am a real mallard duck!!");
}

Подкласс MallardDuck наследует переменные экземпляра quackbehaviour и flybehaviour от суперкласса Duck.

Теперь мы видим, что у каждой утки есть объекты FlyBehaviour и QuackBehaviour, которым она делегирует полет и кряканье соответственно. Когда вы объединяете классы таким образом, вы используете композицию. Это приводит к третьему принципу дизайна этой статьи.

Предпочтите композицию наследованию.

Итак, вот как вы решили проблему и успешно разработали симулятор на основе Duck, который может справиться с изменениями или расширениями в будущем. Конструкция гибкая, многоразовая и простая в обслуживании. И я должен вам сказать, используя все три принципа проектирования, вы также применили свой первый шаблон проектирования и самый простой из них — Шаблон стратегии. Формально его можно определить как:

Шаблон стратегии определяет набор алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Стратегия позволяет алгоритмам изменяться независимо от клиентов, которые ее используют.

Моя статья полностью основана на первой главе книги — Head First Design Patterns, которая, как мне кажется, является одной из лучших книг для понимания шаблонов проектирования. Я надеюсь, что у вас есть идея (или вы полностью поняли) простейший шаблон проектирования, который я описал, и вы должны использовать его в своих проектах.

Удачного обучения!