Тема довольно «заезженная», поэтому я буду часто ссылаться на другие статьи (здесь, например, можно прочитать про историю появления понятия Design Pattern и прочитать о нескольких из них). Моя же цель другая — undefinedуправлять поездомundefined структурировать собранную информацию и написать примеры под углом разработки на Swift.
Паттернами проектирования принято называть типовые решения архитектурных проблем, которые встречаются в процессе разработки. Их разделяют на 3 категории:
Порождающие (Creational)
Структурные (Structural)
Поведенческие (Behavioral)
В рамках небольшой серии статей я хочу поговорить о каждой категории отдельно. Начнем же с порождающих паттернов. Они отвечают за создание объектов и управление этим процессом.
Мне, как мобильному разработчику, на ум сразу приходит сборка экрана вместе с его зависимостями, но на этом примеры не заканчиваются. Попробуем вспомнить основные порождающие шаблоны на примерах кода.
1. Паттерн Фабрика
Основная идея паттерна — вынести логику создания конкретного объекта в отдельный компонент вместо вычислений на месте. Представим приложение‑навигатор, которое поддерживает расчет времени от пункта А в пункт В в зависимости от выбранного транспорта.
protocol TransportService { func calculateTripDuration(_ way: Int) -> Int } class VehicleService: TransportService { func calculateTripDuration(_ way: Int) -> Int { // Логика расчета времени } } class BicycleService: TransportService { func calculateTripDuration(_ way: Int) -> Int { // Логика расчета времени } } class MapViewModel { var transportType: String = "Bycicle" var tripLength: Int ... func provideTripDuration() -> Int { if transportType == "Vehicle" { return VehicleService().calculateTripDuration(tripLength) } else { return BicycleService().calculateTripDuration(tripLength) } } ... }
Фабрика берет логику выбора сервиса на себя, и при необходимости она также может конфигурировать создаваемый объект и передавать ему зависимости.
enum TransportType { case vehicle case bycicle } class TransportServiceFactory { func makeService(_ type: TransportType) -> TransportService { switch type { case .vehicle: return VehicleService() case .bycicle: return BicycleService() } } }
Тогда в клиентском коде останется передать фабрике только тип транспорта.
class MapViewModel { var transportType: TransportType = .bycicle ... func provideTripDuration(type: TransportType) -> Int { let service = TransportServiceFactory().makeService(type) return service.calculateTripDuration(tripLength) } }
Такой подход позволяет добавлять новые сервисы, не изменяя ViewModel. Для этого достаточно расширить enum и фабрику.
2. Абстрактная фабрика
Абстрактная фабрика полезна в ситуациях, когда необходимо скрыть внутренние детали создания объектов за публичным API. Этот паттерн также подходит для случаев, когда создание объекта представляет собой сложный процесс, требующий многократного повторения в разных частях приложения.
В C++, C# и Java существуют абстрактные классы, экземпляры которых нельзя создавать напрямую. Они могут определять общий интерфейс и содержать частичную реализацию, а также использоваться как основа для других классов. В Swift похожую задачу описания требований к типам решают протоколы. Именно их мы и будем использовать в дальнейшем.
В одной из статей (ссылка на нее ниже) автор назвал паттерн Абстрактная фабрика не иначе, как «Фабрика фабрик». Фактически, так и есть, мы создаем вложенные интерфейсы. В другой же статье мне понравилась структура, которая хорошо объясняет — что для реализации такого паттерна нужно. Ей и будем следовать.
Абстрактный продукт
Конкретный продукт
Абстрактная фабрика
Конкретная фабрика
Клиент
Допустим, мы хотим описать отпуск в разных странах.
1. Абстрактные продукты — определим три протокола
protocol VacationActivityProtocol { // описывает вид отдыха func enjoy() } protocol TravelDocumentProtocol { // описывает необходимые документы func prepare() } protocol CommunicationLanguageProtocol { // описывает язык для общения func speak() }
2. Конкретные продукты — создадим реализации для каждого направления
// MARK: - Thailand struct BeachRelaxation: VacationActivityProtocol { func enjoy() { print("Sunbathing and swimming") } } struct InternationalPassport: TravelDocumentProtocol { func prepare() { print("Preparing an international passport") } } struct ThaiLanguage: CommunicationLanguageProtocol { func speak() { print("Speaking Thai") } } // MARK: - Armenia struct MountainHiking: VacationActivityProtocol { func enjoy() { print("Hiking in the mountains") } } struct DomesticPassport: TravelDocumentProtocol { func prepare() { print("Preparing a domestic passport") } } struct ArmenianAndRussian: CommunicationLanguageProtocol { func speak() { print("Speaking Armenian or Russian") } }
3. Абстрактная фабрика — определим протокол фабрики, которая создаёт все три продукта
protocol VacationFactoryProtocol { func makeActivity() -> VacationActivityProtocol func makeDocument() -> TravelDocumentProtocol func makeLanguage() -> CommunicationLanguageProtocol }
4. Конкретная фабрика — реализуем фабрики для двух направлений
struct ThailandVacationFactory: VacationFactoryProtocol { func makeActivity() -> VacationActivityProtocol { BeachRelaxation() } func makeDocument() -> TravelDocumentProtocol { InternationalPassport() } func makeLanguage() -> CommunicationLanguageProtocol { ThaiLanguage() } } struct ArmeniaVacationFactory: VacationFactoryProtocol { func makeActivity() -> VacationActivityProtocol { MountainHiking() } func makeDocument() -> TravelDocumentProtocol { DomesticPassport() } func makeLanguage() -> CommunicationLanguageProtocol { ArmenianAndRussian() } }
Каждая фабрика создаёт семейство продуктов, соответствующее выбранному направлению отпуска.
5. Клиент — создадим класс, который планирует отпуск, используя только абстрактную фабрику
final class VacationPlanner { private let factory: VacationFactoryProtocol init(factory: VacationFactoryProtocol) { self.factory = factory } func planVacation() { let activity = factory.makeActivity() let document = factory.makeDocument() let language = factory.makeLanguage() activity.enjoy() document.prepare() language.speak() } }
Теперь наш класс по организации отпуска готов.
let thailandVacation = VacationPlanner( factory: ThailandVacationFactory() ) thailandVacation.planVacation() // Sunbathing and swimming // Preparing an international passport // Speaking Thai
Абстрактная фабрика позволяет создавать семейства связанных объектов, не привязывая клиентский код к конкретным реализациям. В нашем случае VacationPlanner не нужно изменять, чтобы переключиться с Таиланда на Армению. Достаточно передать другую фабрику.
3. Паттерн Строитель (Builder)
Основная идея шаблона Строитель в возможности пошагово настраивать параметры создаваемого объекта. Одним из популярных примеров из SDK Foundation можно назвать сборку параметров URLRequest'a перед тем, как положить его в URLSession.
Для наглядности возьмем более общий пример — настройка анимационного аватара в приложении. Сначала перечислим некоторые параметры и создадим саму модель:
// Типы параметры enum HairColor { case blond case brunette case ginger } enum EyesColor { case green case blue case hazel } // Модель struct Avatar { let hair: HairColor let eyes: EyesColor let hasGlasses: Bool }
А теперь создадим объект, создающий нашу модель согласно паттерну:
final class AvatarBuilder { private var hair: HairColor = .blond private var eyes: EyesColor = .green private var hasGlasses: Bool = true func with(hair: HairColor) -> AvatarBuilder { self.hair = hair return self } func with(eyes: EyesColor) -> AvatarBuilder { self.eyes = eyes return self } func with(glasses: Bool) -> AvatarBuilder { self.hasGlasses = glasses return self } func build() -> Avatar { Avatar( hair: hair, eyes: eyes, hasGlasses: hasGlasses ) } }
И сам способ сборки в коде:
let avatar = AvatarBuilder() .with(hair: .ginger) .with(eyes: .hazel) .with(glasses: true) .build()
Обычно шаблон встречается там, где есть большое количество параметров, зачастую опциональных. Если речь о модели с небольшым количеством свойств, то использование паттерна будет избыточным.
Этот подход напоминает работу с модификаторами в SwiftUI, с помощью которых настраиваются UI‑элементы. В статье ниже можно найти пример применения паттерна Builder для создания и настройки UI‑компонента.
4. Паттерн Прототип
Не уходя далеко от нашего примера с аватаром, я предлагаю задуматься над таким вопросом: «А могу ли я не создавать объект с нуля, а копировать его с небольшими изменениями?». Как раз эту задачу и описывает паттерн Прототип — он как бы берет за основу оригинальный объект (прототип) и создает его копию с необходимыми изменениями.
Надо сказать, что для работы со структурами и классами (основные объекты в Swift) этот шаблон будет иметь свои нюансы. Начнем со структуры.
struct Avatar { let hair: HairColor let eyes: EyesColor let hasGlasses: Bool func copy( hair: HairColor? = nil, eyes: EyesColor? = nil, hasGlasses: Bool? = nil ) -> Avatar { Avatar( hair = hair ?? self.hair, eyes = eyes ?? self.eyes, hasGlasses = hasGlasses ?? self.hasGlasses ) } }
И использование такого подхода на месте будет выглядеть следующим образом:
let original = Avatar(hair: .blond, eyes: .blue, hasGlasses: true) let copy = original.copy(hasGlasses: false)
Можно сделать свойства модели переменными, а не константами, и тогда достаточно на месте поменять нужное свойство в копии модели без лишнего кода.
let original = Avatar(hair: .blond, eyes: .blue, hasGlasses: true) var copy = original copy.hasGlasse = false
Да, но все же первый вариант кажется «чище», учитывая рекомендации по использованию констант в моделях.
Теперь сравним такие варианты решения проблемы при работе с классом.
final class Avatar { let hair: HairColor let eyes: EyesColor let hasGlasses: Bool init(...) { ... } func copy( hair: HairColor? = nil, eyes: EyesColor? = nil, hasGlasses: Bool? = nil ) -> Self { Avatar( hair = hair ?? self.hair, eyes = eyes ?? self.eyes, hasGlasses = hasGlasses ?? self.hasGlasses ) } } let original = Avatar(hair: .blond, eyes: .blue, hasGlasses: true) let copy = original.copy(hasGlasses: false) ______________________________________________________________________ final class Avatar { var hair: HairColor var eyes: EyesColor var hasGlasses: Bool init(...) { ... } let original = Avatar(hair: .blond, eyes: .blue, hasGlasses: true) var copy = original copy.hasGlasse = false
В случае с классами ценность паттерна Prototype становится особенно очевидной. Поскольку классы в Swift обладают ссылочной семантикой, присваивание одного экземпляра другому не создаёт копию объекта: обе переменные будут ссылаться на один и тот же экземпляр. Поэтому изменение его свойств через одну переменную отразится и при обращении через другую.
В первом примере мы реализуем метод copy(), который создаёт новый экземпляр на основе существующего, позволяя при необходимости изменить отдельные свойства. В результате оригинал и копия становятся независимыми объектами.
Отмечу, что в Foundation существует протокол
NSCopying, который позволяет определить механизм копирования экземпляров классов. При этом сам протокол не реализует копирование автоматически — разработчик должен описать его логику.
5. Синглтон
Позволю себе предположить, что этот шаблон — один из самых обсуждаемых в разработке. Более того, нередко можно встретить мнение, что его следует считать антипаттерном. В моральную составляющую использования этого архитектурного приёма углубляться не будем, а лишь вспомним, что он собой представляет.
Синглтон — это порождающий паттерн проектирования, который гарантирует наличие единственного экземпляра класса и предоставляет глобальную точку доступа к нему.
final class Logger { static shared: Logger = Logger() private init() {} func log(_ message: String) { print("[LOG]: \(message)") } }
Чтобы воспользоваться функциональностью Logger, достаточно обратиться к его единственному экземпляру через свойство shared.
func setAvatar { Logger.shared.log("User avatar is set.") }
Заключение
Мы вспомнили основные паттерны, которые относятся к категории Порождающих. Уверен, что вы часто использовали их на практике, не задумываясь об их теоретической состовляющей.
В следующей статье поговорим о Структурных паттернах.
Материалы
Паттерн Фабрика
Абстрактная Фабрика
Abstract Factory
Builder
Одиночкa
LeshaRB
А можно узнать смысл данной статьи
Есть старая добрая книга банды четырех
Ну или русского автора
https://refactoring.guru/ru/design-patterns
vagamagajaga Автор
Попробуйте прочитать предисловие к статье ещё раз, там будет ответ на ваш вопрос
P.S. Почти любая информация уже есть в сети, если для вас это статья тривиальна или избыточна - пройдите мимо. За ссылку спасибо, не знаком с этим ресурсом