Padrões de projeto em Swift: um guia completo
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты строишь дом. У тебя есть два пути:
- Путь хаоса: ты начинаешь класть кирпичи как попало, придумываешь окна на ходу, а крышу делаешь из того, что осталось. В итоге дом получается кривым, в нём дует из всех щелей, а когда ты захочешь добавить второй этаж — всё рухнет.
- Путь архитектора: ты берёшь готовый чертёж. В нём уже продумано, где будут несущие стены, как провести электрику и куда поставить двери. Ты просто следуешь проверенной схеме — и получаешь надёжный дом, который легко достраивать и чинить.
Паттерны проектирования — это и есть такие готовые «чертежи» для кода. Это не готовые куски программы (их нельзя просто скопировать), а проверенные способы решать типовые задачи. Например:
- Как сделать так, чтобы у объекта был только один экземпляр? (Singleton)
- Как создать сложный объект пошагово, не путаясь в конструкторе? (Builder)
- Как уведомить много объектов о том, что что-то случилось? (Observer)
Важно: Паттерны — это не магия и не панацея. Их не нужно использовать везде подряд. Но если ты знаешь паттерны, ты перестаёшь изобретать велосипед и начинаешь говорить с другими программистами на одном языке. Когда ты говоришь «я использовал здесь Декоратор», коллега сразу понимает, как устроен твой код, не читая его целиком.
В Swift паттерны особенно важны, потому что язык сочетает объектно-ориентированное и протокол-ориентированное программирование. Это даёт гибкость, но и требует аккуратности.
Теория: как это работает изнутри
Все паттерны делятся на три большие группы. Запомни эту классификацию — она поможет не путаться:
| Группа | Что делают | Примеры |
|---|---|---|
| Порождающие | Отвечают за создание объектов. Помогают сделать этот процесс гибким и скрыть детали. | Singleton, Factory, Builder, Prototype |
| Структурные | Определяют, как классы и объекты собираются в большие структуры. | Adapter, Decorator, Facade, Proxy |
| Поведенческие | Управляют взаимодействием между объектами и распределением обязанностей. | Observer, Strategy, Command, Iterator |
Как паттерн устроен изнутри? У каждого паттерна есть четыре части:
- Название — чтобы мы могли ссылаться на него.
- Задача — какую проблему он решает.
- Решение — схема классов и объектов.
- Результат — что мы получаем (плюсы и минусы).
Например, паттерн Singleton (Одиночка):
- Задача: гарантировать, что у класса есть только один экземпляр, и предоставить к нему глобальную точку доступа.
- Решение: приватный инициализатор + статическое свойство, которое хранит единственный экземпляр.
- Результат: экономия памяти, но сложность в тестировании (все части программы зависят от одного объекта).
Важно: В Swift многие паттерны реализуются проще, чем в других языках, благодаря протоколам, расширениям (extensions) и замыканиям (closures). Но базовая логика остаётся той же.
Синтаксис в Swift: все формы и варианты
В Swift нет специальных ключевых слов «паттерн» или «singleton». Паттерны реализуются с помощью обычных конструкций языка. Вот основные инструменты, которые тебе понадобятся:
| Инструмент Swift | Для каких паттернов используется | Пример синтаксиса |
|---|---|---|
private init() |
Singleton (запрет создания извне) | private init() { } |
static let shared = ... |
Singleton (глобальная точка доступа) | static let shared = MyClass() |
protocol |
Strategy, Adapter (определение общего поведения) | protocol Flyable { func fly() } |
extension |
Decorator (добавление функциональности) | extension String { func reversed() -> String } |
typealias |
Command (тип для замыкания) | typealias Action = () -> Void |
associatedtype |
Factory (создание объектов с общим протоколом) | associatedtype Product |
Особенности Swift:
- В Swift нет абстрактных классов (как в Java). Вместо них используются протоколы.
- Структуры (struct) не поддерживают наследование, поэтому для паттернов, где нужно наследование (например, Decorator), чаще используют классы.
- Замыкания (closures) позволяют реализовать паттерны Strategy и Command без создания отдельных классов — просто передаёшь функцию.
Примеры кода — от простого к сложному
Все примеры рабочие. Скопируй их в Xcode Playground и запускай.
Пример 1: Singleton (Одиночка) — простейший
// Паттерн Singleton: гарантирует, что у класса только один экземпляр
class AppSettings {
// Статическое свойство shared — единственный экземпляр класса
static let shared = AppSettings()
// Приватный инициализатор — никто снаружи не сможет создать новый объект
private init() {
// Здесь можно загрузить настройки из файла
print("Настройки загружены")
}
var theme: String = "светлая"
var fontSize: Int = 14
}
// Использование:
let settings = AppSettings.shared // Получаем единственный объект
settings.theme = "тёмная"
print(settings.theme) // Выведет: тёмная
// Попробуй создать ещё один экземпляр:
// let another = AppSettings() // Ошибка! Инициализатор приватный
Пример 2: Factory Method (Фабричный метод) — создание объектов
// Протокол, который определяет, что умеет делать транспорт
protocol Transport {
func deliver()
}
// Конкретные виды транспорта
class Truck: Transport {
func deliver() {
print("Доставка грузовиком по дороге")
}
}
class Ship: Transport {
func deliver() {
print("Доставка кораблём по морю")
}
}
// Фабрика, которая создаёт нужный транспорт
class TransportFactory {
// Фабричный метод — решает, какой объект создать
static func createTransport(for cargo: String) -> Transport {
if cargo == "наземный" {
return Truck()
} else {
return Ship()
}
}
}
// Использование:
let transport1 = TransportFactory.createTransport(for: "наземный")
transport1.deliver() // Выведет: Доставка грузовиком по дороге
let transport2 = TransportFactory.createTransport(for: "морской")
transport2.deliver() // Выведет: Доставка кораблём по морю
Пример 3: Builder (Строитель) — пошаговое создание сложного объекта
// Класс, который мы будем собирать по частям
class Pizza {
var size: String = ""
var cheese: Bool = false
var pepperoni: Bool = false
var mushrooms: Bool = false
func describe() {
print("Пицца размером (size), сыр: (cheese), пепперони: (pepperoni), грибы: (mushrooms)")
}
}
// Строитель — содержит методы для настройки каждой части
class PizzaBuilder {
private var pizza = Pizza()
func setSize(_ size: String) -> PizzaBuilder {
pizza.size = size
return self // Возвращаем self для цепочки вызовов
}
func addCheese() -> PizzaBuilder {
pizza.cheese = true
return self
}
func addPepperoni() -> PizzaBuilder {
pizza.pepperoni = true
return self
}
func addMushrooms() -> PizzaBuilder {
pizza.mushrooms = true
return self
}
func build() -> Pizza {
return pizza // Возвращаем готовый объект
}
}
// Использование:
let myPizza = PizzaBuilder()
.setSize("большая")
.addCheese()
.addPepperoni()
.build()
myPizza.describe() // Выведет: Пицца размером большая, сыр: true, пепперони: true, грибы: false
Пример 4: Adapter (Адаптер) — соединяем несовместимое
// Старый класс с неудобным интерфейсом
class OldPrinter {
func printOld(text: String) {
print("Старый принтер печатает: (text)")
}
}
// Новый протокол, который мы хотим использовать
protocol NewPrinter {
func printNew(text: String)
}
// Адаптер — делает из старого принтера новый
class PrinterAdapter: NewPrinter {
private let oldPrinter: OldPrinter
init(oldPrinter: OldPrinter) {
self.oldPrinter = oldPrinter
}
func printNew(text: String) {
// Адаптируем вызов: преобразуем данные, если нужно
let adaptedText = text.uppercased()
oldPrinter.printOld(text: adaptedText)
}
}
// Использование:
let oldPrinter = OldPrinter()
let adapter = PrinterAdapter(oldPrinter: oldPrinter)
adapter.printNew(text: "Привет, мир!") // Выведет: Старый принтер печатает: ПРИВЕТ, МИР!
Пример 5: Observer (Наблюдатель) — уведомление подписчиков
// Протокол для наблюдателя
protocol Observer: AnyObject {
func update(event: String)
}
// Класс, за которым наблюдают
class NewsPublisher {
// Список подписчиков
private var observers = [Observer]()
func addObserver(_ observer: Observer) {
observers.append(observer)
}
func removeObserver(_ observer: Observer) {
// Удаляем конкретного наблюдателя
observers.removeAll { $0 === observer }
}
func publishNews(_ news: String) {
print("Опубликована новость: (news)")
// Уведомляем всех подписчиков
for observer in observers {
observer.update(event: news)
}
}
}
// Конкретный наблюдатель
class Subscriber: Observer {
let name: String
init(name: String) {
self.name = name
}
func update(event: String) {
print("(name) получил уведомление: (event)")
}
}
// Использование:
let publisher = NewsPublisher()
let subscriber1 = Subscriber(name: "Анна")
let subscriber2 = Subscriber(name: "Иван")
publisher.addObserver(subscriber1)
publisher.addObserver(subscriber2)
publisher.publishNews("Вышла новая версия Swift!")
// Выведет:
// Опубликована новость: Вышла новая версия Swift!
// Анна получил уведомление: Вышла новая версия Swift!
// Иван получил уведомление: Вышла новая версия Swift!
Пример 6: Strategy (Стратегия) — выбираем алгоритм на ходу
// Протокол для стратегии сортировки
protocol SortStrategy {
func sort(_ array: [Int]) -> [Int]
}
// Конкретные стратегии
class BubbleSort: SortStrategy {
func sort(_ array: [Int]) -> [Int] {
print("Сортировка пузырьком")
var arr = array
for i in 0.. arr[j + 1] {
arr.swapAt(j, j + 1)
}
}
}
return arr
}
}
class QuickSort: SortStrategy {
func sort(_ array: [Int]) -> [Int] {
print("Быстрая сортировка")
guard array.count > 1 else { return array }
let pivot = array[array.count / 2]
let less = array.filter { $0 < pivot }
let equal = array.filter { $0 == pivot }
let greater = array.filter { $0 > pivot }
return sort(less) + equal + sort(greater)
}
}
// Контекст, который использует стратегию
class Sorter {
private var strategy: SortStrategy
init(strategy: SortStrategy) {
self.strategy = strategy
}
func setStrategy(_ strategy: SortStrategy) {
self.strategy = strategy
}
func sortArray(_ array: [Int]) -> [Int] {
return strategy.sort(array)
}
}
// Использование:
let data = [3, 1, 4, 1, 5, 9, 2, 6]
let sorter = Sorter(strategy: BubbleSort())
print(sorter.sortArray(data)) // Использует пузырёк
sorter.setStrategy(QuickSort())
print(sorter.sortArray(data)) // Теперь быстрая сортировка
Пример 7: Command (Команда) — превращаем действие в объект
// Протокол команды
protocol Command {
func execute()
}
// Конкретные команды
class LightOnCommand: Command {
func execute() {
print("Свет включён")
}
}
class LightOffCommand: Command {
func execute() {
print("Свет выключен")
}
}
// Пульт — хранит команды и выполняет их
class RemoteControl {
private var command: Command?
func setCommand(_ command: Command) {
self.command = command
}
func pressButton() {
command?.execute()
}
}
// Использование:
let remote = RemoteControl()
let lightOn = LightOnCommand()
let lightOff = LightOffCommand()
remote.setCommand(lightOn)
remote.pressButton() // Выведет: Свет включён
remote.setCommand(lightOff)
remote.pressButton() // Выведет: Свет выключен
Пример 8: Facade (Фасад) — упрощаем сложную систему
// Сложные подсистемы
class AudioSystem {
func playSound() { print("Звук воспроизводится") }
func stopSound() { print("Звук остановлен") }
}
class VideoSystem {
func playVideo() { print("Видео воспроизводится") }
func stopVideo() { print("Видео остановлено") }
}
class Projector {
func turnOn() { print("Проектор включён") }
func turnOff() { print("Проектор выключен") }
}
// Фасад — простой интерфейс для сложной системы
class HomeTheaterFacade {
private let audio = AudioSystem()
private let video = VideoSystem()
private let projector = Projector()
func watchMovie() {
print("Начинаем просмотр фильма...")
projector.turnOn()
audio.playSound()
video.playVideo()
}
func endMovie() {
print("Завершаем просмотр...")
video.stopVideo()
audio.stopSound()
projector.turnOff()
}
}
// Использование:
let homeTheater = HomeTheaterFacade()
homeTheater.watchMovie()
// Выведет:
// Начинаем просмотр фильма...
// Проектор включён
// Звук воспроизводится
// Видео воспроизводится
homeTheater.endMovie()
// Выведет:
// Завершаем просмотр...
// Видео остановлено
// Звук остановлен
// Проектор выключен
Сравнение с Python: чем похоже и чем отличается
| Аспект | Swift | Python |
|---|---|---|
| Типизация | Строгая статическая. Типы проверяются на этапе компиляции. Это помогает ловить ошибки раньше, но требует больше кода. | Динамическая. Типы проверяются во время выполнения. Кода меньше, но ошибки могут всплыть в самый неподходящий момент. |
| Singleton | Через private init и static let shared. Надёжно, но многословно. |
Через __new__ или модуль (модули в Python — синглтоны по умолчанию). Проще, но можно случайно создать копию. |
| Протоколы/Интерфейсы | Есть протоколы (protocol). Можно добавлять реализации по умолчанию через extension. |
Есть абстрактные классы (ABC) и протоколы через Protocol (в typing). Менее распространены. |
| Замыкания | Замыкания — это функции без имени. Используются для Strategy, Command. Синтаксис: { (x: Int) -> Int in return x * 2 } |
Лямбды (lambda x: x * 2). Короче, но менее читаемы для сложных случаев. |
| Наследование | Только классы могут наследовать. Структуры — нет. Это ограничивает некоторые паттерны. | Всё наследуется от object. Множественное наследование разрешено (но это может запутать). |
| Паттерн Observer | Реализуется вручную или через NotificationCenter. |
Есть встроенный модуль observable (в новых версиях) или сторонние библиотеки. |
Совет: Если ты знаешь Python, не пытайся переносить его стиль в Swift. В Swift больше кода, но он безопаснее. В Python меньше кода, но нужно быть внимательнее с типами. Паттерны в обоих языках работают одинаково на уровне идей, но реализация отличается.
Типичные ошибки и подводные камни
- Злоупотребление Singleton. Многие новички делают синглтоном всё подряд: «А вдруг понадобится один экземпляр?». На самом деле Singleton нужен только когда:
- Должен быть ровно один экземпляр (например, база данных).
- Этот экземпляр нужен глобально.
- Забыл про приватный инициализатор в Singleton. Если не сделать
private init(), кто угодно сможет создать второй экземпляр — и паттерн сломается. - Builder без цепочки вызовов. Если твой Builder не возвращает
selfиз каждого метода, ты теряешь главное удобство — возможность писать.setSize().addCheese().build(). - Сильная ссылка в Observer. В Swift, если наблюдатель (observer) хранится в массиве как сильная ссылка, а наблюдатель держит ссылку на издателя — получается цикл. Используй
weakдля ссылок на наблюдателей. - Adapter делает слишком много. Адаптер должен только преобразовывать интерфейс, а не добавлять новую логику. Если ты в адаптере начал писать бизнес-логику — ты делаешь что-то не то.
- Strategy через if-else. Если ты внутри метода пишешь
if strategy == .bubble { ... } else if ...— ты не используешь Strategy, ты имитируешь его. Настоящая стратегия — это полиморфизм: разные классы с одинаковым методом. - Command без отмены. Одно из главных преимуществ Command — возможность откатить действие. Если ты не хранишь предыдущее состояние, ты теряешь половину пользы.
Лучшие практики и советы профессионалов
- Не учи паттерны наизусть. Понимай идею. Когда ты поймёшь, зачем нужен паттерн, ты сможешь реализовать его даже без шпаргалки.
- Начинай с простого. Singleton, Factory, Strategy — это база. Не лезь сразу в Visitor или Mediator, пока не научишься уверенно использовать простые паттерны.
- Используй протоколы. В Swift протоколы — это твой главный инструмент для паттернов. Они позволяют отделить интерфейс от реализации.
- Не бойся писать больше кода. Паттерны часто увеличивают количество файлов и строк. Это нормально. Лучше 5 маленьких классов, чем один огромный с кучей if-else.
- Тестируй изоляцию. Паттерны должны облегчать тестирование. Если после внедрения паттерна тесты стали сложнее — ты что-то делаешь не так.
- Смотри на стандартную библиотеку Swift. Многие паттерны уже встроены:
NotificationCenter— Observer.Codable— Strategy (разные стратегии кодирования).IteratorProtocol— Iterator.
- Документируй. Если ты используешь паттерн, напиши комментарий: «Здесь используется паттерн Strategy для выбора алгоритма сортировки». Это поможет коллегам (и тебе через месяц).
Вопросы для самопроверки
- Что такое паттерн проектирования простыми словами?
Ответ: Это готовое проверенное решение типовой задачи в программировании. Как чертёж для дома. - На какие три группы делятся паттерны?
Ответ: Порождающие (создание объектов), структурные (сборка объектов), поведенческие (взаимодействие объектов). - Как реализовать Singleton в Swift?
Ответ: Создать статическое свойствоsharedи сделать инициализатор приватным:private init() { }. - В чём разница между Factory Method и Builder?
Ответ: Factory Method создаёт объект целиком за один вызов. Builder создаёт объект пошагово, позволяя настраивать каждую часть. - Для чего нужен паттерн Adapter?
Ответ: Чтобы сделать несовместимые интерфейсы совместимыми. Как переходник для зарядки. - Какая проблема может возникнуть в Observer, если не использовать weak?
Ответ: Цикл сильных ссылок (retain cycle). Объекты будут держать друг друга, и память не освободится. - Какой паттерн позволяет менять алгоритм во время выполнения программы?
Ответ: Strategy. Мы просто подставляем другой объект с тем же интерфейсом. - Что такое Command и зачем он нужен?
Ответ: Command превращает действие в объект. Это позволяет откладывать выполнение, ставить в очередь и отменять действия. - Почему не стоит делать всё Singleton?
Ответ: Singleton создаёт глобальную зависимость. Код становится сложно тестировать и изменять. Используй его только когда действительно нужен один экземпляр. - Какой паттерн используется в Swift для Codable?
Ответ: Strategy. Разные стратегии кодирования (JSON, Property List) реализуются через протоколы.
Итог и что изучить дальше
Ты только что познакомился с восемью ключевыми паттернами проектирования на Swift. Это не просто теория — это инструменты, которые ты будешь использовать каждый день, когда код станет сложнее одного файла.
Что дальше?
- Попрактикуйся. Возьми любой свой проект (или придумай новый) и попробуй внедрить в него хотя бы 2-3 паттерна. Например: Singleton для настроек, Factory для создания объектов, Observer для уведомлений.
- Изучи продвинутые паттерны: Visitor, Mediator, State, Memento. Они решают более специфические задачи.
- Почитай книгу «Паттерны проектирования» (GoF). Это классика. Но учти: примеры там на C++, тебе придётся адаптировать их под Swift.
- Изучи архитектурные паттерны: MVC, MVP, MVVM, VIPER. Они описывают структуру всего приложения, а не отдельного куска кода.
- Посмотри, как паттерны используются в реальных проектах. Открой любой open-source проект на Swift (например, Alamofire, Kingfisher) и поищи там знакомые паттерны.
Запомни: Паттерны — это не цель, а средство. Не пытайся впихнуть паттерн туда, где достаточно простого if-else. Но когда задача становится сложной — паттерны спасут тебе дни работы и часы отладки.
Ты сделал первый шаг к тому, чтобы писать код, который понимают другие разработчики, который легко менять и который не разваливается при первом изменении требований. Поздравляю!