Паттерны проектирования. Порождающие

Тема довольно "заезженная", поэтому я буду часто ссылаться на другие статьи (здесь, например, можно прочитать про историю появления понятия Design Pattern и прочитать о нескольких из них). Моя же цель другая - управлять поездом структурировать собранную информацию и написать примеры под углом разработки на 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
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.