🍎 Swift

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

Как паттерн устроен изнутри? У каждого паттерна есть четыре части:

  1. Название — чтобы мы могли ссылаться на него.
  2. Задача — какую проблему он решает.
  3. Решение — схема классов и объектов.
  4. Результат — что мы получаем (плюсы и минусы).

Например, паттерн 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 меньше кода, но нужно быть внимательнее с типами. Паттерны в обоих языках работают одинаково на уровне идей, но реализация отличается.

Типичные ошибки и подводные камни

  1. Злоупотребление Singleton. Многие новички делают синглтоном всё подряд: «А вдруг понадобится один экземпляр?». На самом деле Singleton нужен только когда:
    • Должен быть ровно один экземпляр (например, база данных).
    • Этот экземпляр нужен глобально.
    Если ты используешь Singleton для «удобства» — ты создаёшь скрытые зависимости, которые потом сложно тестировать.
  2. Забыл про приватный инициализатор в Singleton. Если не сделать private init(), кто угодно сможет создать второй экземпляр — и паттерн сломается.
  3. Builder без цепочки вызовов. Если твой Builder не возвращает self из каждого метода, ты теряешь главное удобство — возможность писать .setSize().addCheese().build().
  4. Сильная ссылка в Observer. В Swift, если наблюдатель (observer) хранится в массиве как сильная ссылка, а наблюдатель держит ссылку на издателя — получается цикл. Используй weak для ссылок на наблюдателей.
  5. Adapter делает слишком много. Адаптер должен только преобразовывать интерфейс, а не добавлять новую логику. Если ты в адаптере начал писать бизнес-логику — ты делаешь что-то не то.
  6. Strategy через if-else. Если ты внутри метода пишешь if strategy == .bubble { ... } else if ... — ты не используешь Strategy, ты имитируешь его. Настоящая стратегия — это полиморфизм: разные классы с одинаковым методом.
  7. Command без отмены. Одно из главных преимуществ Command — возможность откатить действие. Если ты не хранишь предыдущее состояние, ты теряешь половину пользы.

Лучшие практики и советы профессионалов

  1. Не учи паттерны наизусть. Понимай идею. Когда ты поймёшь, зачем нужен паттерн, ты сможешь реализовать его даже без шпаргалки.
  2. Начинай с простого. Singleton, Factory, Strategy — это база. Не лезь сразу в Visitor или Mediator, пока не научишься уверенно использовать простые паттерны.
  3. Используй протоколы. В Swift протоколы — это твой главный инструмент для паттернов. Они позволяют отделить интерфейс от реализации.
  4. Не бойся писать больше кода. Паттерны часто увеличивают количество файлов и строк. Это нормально. Лучше 5 маленьких классов, чем один огромный с кучей if-else.
  5. Тестируй изоляцию. Паттерны должны облегчать тестирование. Если после внедрения паттерна тесты стали сложнее — ты что-то делаешь не так.
  6. Смотри на стандартную библиотеку Swift. Многие паттерны уже встроены:
    • NotificationCenter — Observer.
    • Codable — Strategy (разные стратегии кодирования).
    • IteratorProtocol — Iterator.
    Изучай их — это примеры правильной реализации.
  7. Документируй. Если ты используешь паттерн, напиши комментарий: «Здесь используется паттерн Strategy для выбора алгоритма сортировки». Это поможет коллегам (и тебе через месяц).

Вопросы для самопроверки

  1. Что такое паттерн проектирования простыми словами?
    Ответ: Это готовое проверенное решение типовой задачи в программировании. Как чертёж для дома.
  2. На какие три группы делятся паттерны?
    Ответ: Порождающие (создание объектов), структурные (сборка объектов), поведенческие (взаимодействие объектов).
  3. Как реализовать Singleton в Swift?
    Ответ: Создать статическое свойство shared и сделать инициализатор приватным: private init() { }.
  4. В чём разница между Factory Method и Builder?
    Ответ: Factory Method создаёт объект целиком за один вызов. Builder создаёт объект пошагово, позволяя настраивать каждую часть.
  5. Для чего нужен паттерн Adapter?
    Ответ: Чтобы сделать несовместимые интерфейсы совместимыми. Как переходник для зарядки.
  6. Какая проблема может возникнуть в Observer, если не использовать weak?
    Ответ: Цикл сильных ссылок (retain cycle). Объекты будут держать друг друга, и память не освободится.
  7. Какой паттерн позволяет менять алгоритм во время выполнения программы?
    Ответ: Strategy. Мы просто подставляем другой объект с тем же интерфейсом.
  8. Что такое Command и зачем он нужен?
    Ответ: Command превращает действие в объект. Это позволяет откладывать выполнение, ставить в очередь и отменять действия.
  9. Почему не стоит делать всё Singleton?
    Ответ: Singleton создаёт глобальную зависимость. Код становится сложно тестировать и изменять. Используй его только когда действительно нужен один экземпляр.
  10. Какой паттерн используется в 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. Но когда задача становится сложной — паттерны спасут тебе дни работы и часы отладки.

Ты сделал первый шаг к тому, чтобы писать код, который понимают другие разработчики, который легко менять и который не разваливается при первом изменении требований. Поздравляю!