Swift Design Patterns: Strategy

Зачем нужен?

Паттерн Strategy применяется для уменьшения связанности. В двух словах: программируйте на уровне интерфейсов, а не реализаций.

Соответствует open/closed принципу SOLID: изменение конкретных классов не влияет на клиента, т.к он зависит только от интерфейса.

Определение

  • определяет семейство алгоритмов(поведений), инкапсулирует и обеспечивает их взаимозаменяемость. Позволяет модифицировать алгоритмы независимо от их использования на стороне клиента.
  • инкапсулируем абстракцию в интерфейсе, скрываем реализацию в конкретных классах

Реализация

  1. Найдите алгоритм(поведение)
  2. Определите сигнатуру в супертипе(интерфейсе/абстрактном классе/протоколе)
  3. Скройте реализацию(или реализации) в конкретном классе, который соответствует этому интерфейсу
  4. Свяжите клиента алгоритма с интерфейсом

Пример

Допустим у нас есть утки. Утки могут крякать по разному: кряканье, писк, а могут и вовсе не крякать. Алгоритмом в данном случае будет способ кряканья.

Теперь определим сигнатуру:

// в качестве супертипа - протокол(интерфейс)
protocol QuackBehavior {
    func quack()
}

Скроем реализацию в конкретных классах:

// конкретные классы поведения, описываемые супертипом
class Quack: QuackBehavior {
    func quack() {
        print("Я крякаю: quack-quack")
    }
}

class QuackMute: QuackBehavior {
    func quack() {
        print("Я не крякаю :(")
    }
}

class Squeak: QuackBehavior {
    func quack() {
        print("Я пищу: squeak-squeak")
    }
}

Свяжем клиента(в нашем случае - утку) с интерфейсом:

class Duck {
    // Клиент Duck связан с абстракцией, а не с конкретными реализациями
    private var quackBehavior: QuackBehavior
    
    required convenience init() {
        self.init(quackBehavior: QuackMute())
    }
    
    private init(quackBehavior: QuackBehavior) {
        self.quackBehavior = quackBehavior
    }
        
    func performQuack() {
        // делегируем поведение
        quackBehavior.quack()
    }
        
    func setQuackBehavior(quackBehavior: QuackBehavior) {
        self.quackBehavior = quackBehavior
    }
}

В данной реализации метод setQuackBehavior позволяет динамически изменять поведение.

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

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

Подробнее про протоколо-ориентированный подход:

Реализация паттерна Strategy: