
Стратегия на Swift
Стратегия — это поведенческий паттерн, выносит набор алгоритмов в собственные классы и делает их взаимозаменимыми.
Другие объекты содержат ссылку на объект-стратегию и делегируют ей работу. Программа может подменить этот объект другим, если требуется иной способ решения задачи.
Сложность:
Популярность:
Применимость: Стратегия часто используется в Swift-коде, особенно там, где нужно подменять алгоритм во время выполнения программы. Многие примеры стратегии можно заменить простыми lambda-выражениями.
Признаки применения паттерна: Класс делегирует выполнение вложенному объекту абстрактного типа или интерфейса.
Концептуальный пример
Этот пример показывает структуру паттерна Стратегия, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире Swift.
Example.swift: Пример структуры паттерна
import XCTest /// Контекст определяет интерфейс, представляющий интерес для клиентов. class Context { /// Контекст хранит ссылку на один из объектов Стратегии. Контекст не знает /// конкретного класса стратегии. Он должен работать со всеми стратегиями /// через интерфейс Стратегии. private var strategy: Strategy /// Обычно Контекст принимает стратегию через конструктор, а также /// предоставляет сеттер для её изменения во время выполнения. init(strategy: Strategy) { self.strategy = strategy } /// Обычно Контекст позволяет заменить объект Стратегии во время выполнения. func update(strategy: Strategy) { self.strategy = strategy } /// Вместо того, чтобы самостоятельно реализовывать множественные версии /// алгоритма, Контекст делегирует некоторую работу объекту Стратегии. func doSomeBusinessLogic() { print("Context: Sorting data using the strategy (not sure how it'll do it)\n") let result = strategy.doAlgorithm(["a", "b", "c", "d", "e"]) print(result.joined(separator: ",")) } } /// Интерфейс Стратегии объявляет операции, общие для всех поддерживаемых версий /// некоторого алгоритма. /// /// Контекст использует этот интерфейс для вызова алгоритма, определённого /// Конкретными Стратегиями. protocol Strategy { func doAlgorithm<T: Comparable>(_ data: [T]) -> [T] } /// Конкретные Стратегии реализуют алгоритм, следуя базовому интерфейсу /// Стратегии. Этот интерфейс делает их взаимозаменяемыми в Контексте. class ConcreteStrategyA: Strategy { func doAlgorithm<T: Comparable>(_ data: [T]) -> [T] { return data.sorted() } } class ConcreteStrategyB: Strategy { func doAlgorithm<T: Comparable>(_ data: [T]) -> [T] { return data.sorted(by: >) } } /// Давайте посмотрим как всё это будет работать. class StrategyConceptual: XCTestCase { func test() { /// Клиентский код выбирает конкретную стратегию и передаёт её в /// контекст. Клиент должен знать о различиях между стратегиями, чтобы /// сделать правильный выбор. let context = Context(strategy: ConcreteStrategyA()) print("Client: Strategy is set to normal sorting.\n") context.doSomeBusinessLogic() print("\nClient: Strategy is set to reverse sorting.\n") context.update(strategy: ConcreteStrategyB()) context.doSomeBusinessLogic() } }
Output.txt: Результат выполнения
Client: Strategy is set to normal sorting. Context: Sorting data using the strategy (not sure how it'll do it) a,b,c,d,e Client: Strategy is set to reverse sorting. Context: Sorting data using the strategy (not sure how it'll do it) e,d,c,b,a
Пример из реальной жизни
Example.swift: Пример из реальной жизни
import XCTest class StrategyRealWorld: XCTestCase { /// This example shows a simple implementation of a list controller that is /// able to display models from different data sources: /// /// (MemoryStorage, CoreDataStorage, RealmStorage) func test() { let controller = ListController() let memoryStorage = MemoryStorage<User>() memoryStorage.add(usersFromNetwork()) clientCode(use: controller, with: memoryStorage) clientCode(use: controller, with: CoreDataStorage()) clientCode(use: controller, with: RealmStorage()) } func clientCode(use controller: ListController, with dataSource: DataSource) { controller.update(dataSource: dataSource) controller.displayModels() } private func usersFromNetwork() -> [User] { let firstUser = User(id: 1, username: "username1") let secondUser = User(id: 2, username: "username2") return [firstUser, secondUser] } } class ListController { private var dataSource: DataSource? func update(dataSource: DataSource) { /// ... resest current states ... self.dataSource = dataSource } func displayModels() { guard let dataSource = dataSource else { return } let models = dataSource.loadModels() as [User] /// Bind models to cells of a list view... print("\nListController: Displaying models...") models.forEach({ print($0) }) } } protocol DataSource { func loadModels<T: DomainModel>() -> [T] } class MemoryStorage<Model>: DataSource { private lazy var items = [Model]() func add(_ items: [Model]) { self.items.append(contentsOf: items) } func loadModels<T: DomainModel>() -> [T] { guard T.self == User.self else { return [] } return items as! [T] } } class CoreDataStorage: DataSource { func loadModels<T: DomainModel>() -> [T] { guard T.self == User.self else { return [] } let firstUser = User(id: 3, username: "username3") let secondUser = User(id: 4, username: "username4") return [firstUser, secondUser] as! [T] } } class RealmStorage: DataSource { func loadModels<T: DomainModel>() -> [T] { guard T.self == User.self else { return [] } let firstUser = User(id: 5, username: "username5") let secondUser = User(id: 6, username: "username6") return [firstUser, secondUser] as! [T] } } protocol DomainModel { var id: Int { get } } struct User: DomainModel { var id: Int var username: String }
Output.txt: Результат выполнения
ListController: Displaying models... User(id: 1, username: "username1") User(id: 2, username: "username2") ListController: Displaying models... User(id: 3, username: "username3") User(id: 4, username: "username4") ListController: Displaying models... User(id: 5, username: "username5") User(id: 6, username: "username6")