Паттерны проектирования в Swift: когда и зачем
Вы когда-нибудь переписывали один и тот же код по десять раз? Или, открыв свой старый проект через месяц, с ужасом понимали, что ничего не можете в нём понять? Добро пожаловать в клуб. Обычно это происходит, когда архитектура приложения строится «на коленке», без какой-либо системы. И тут на помощь приходят паттерны проектирования в Swift — проверенные временем шаблоны, которые превращают хаос в стройную, понятную структуру.
В этой статье мы не будем сухо перечислять определения. Мы разберёмся, когда реально стоит применять каждый паттерн, а когда он лишь усложнит жизнь. Готовы навести порядок в своём коде? Поехали.
Что такое паттерны проектирования и почему они важны для Swift?
Паттерн проектирования — это не готовый кусок кода, а проверенная схема решения типовой задачи. Представьте, что вы строите дом. Вместо того чтобы каждый раз изобретать, как соединить балки, вы используете стандартные узлы — болты, гайки, уголки. Паттерны — те же «строительные узлы» для программиста.
В Swift, с его мощной системой протоколов и value types, многие паттерны приобретают новое звучание. Например, делегат здесь не просто интерфейс, а изящное решение на протоколах. А синглтон в Swift может быть реализован на let-константе без лишнего кода.
Если вы пишете на Swift и не используете паттерны, рано или поздно вы столкнётесь с проблемами:
- Жёсткая связанность классов — изменение в одном месте ломает всё.
- Дублирование кода — одни и те же алгоритмы размазаны по всему проекту.
- Трудность тестирования — логика не изолирована.
Хватит страдать. Давайте разберём пять самых полезных паттернов для iOS-разработчика.
1. Singleton (Одиночка) — когда объект должен быть в единственном экземпляре
Самый популярный и, пожалуй, самый опасный паттерн. Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему.
Когда использовать:
- Управление настройками приложения (UserDefaults).
- Работа с сетью (URLSession.shared).
- Логирование.
- Менеджер Core Data (persistent container).
Когда не стоит: Если вы делаете синглтон для каждого второго класса — это антипаттерн. Он усложняет тестирование и создаёт скрытые зависимости.
Пример на Swift:
final class NetworkManager {
static let shared = NetworkManager()
private init() {} // Приватный инициализатор — ключевой момент
func fetchData(from url: URL) async throws -> Data {
// Реальная логика запроса
return Data()
}
}
// Использование
let data = try await NetworkManager.shared.fetchData(from: url)
Обратите внимание на private init(). Без него любой сможет создать второй экземпляр, и паттерн потеряет смысл.
2. Factory Method (Фабричный метод) — когда создание объекта — это целая наука
Иногда логика создания объекта слишком сложна, чтобы помещать её в конструктор. Или же тип объекта должен определяться на лету. Factory Method выносит создание в отдельный метод, который может быть переопределён в подклассах.
Когда использовать:
- Создание разных типов view controller'ов в зависимости от состояния.
- Генерация моделей из JSON с разными полями.
- Создание UI-элементов с разными стилями.
Пример на Swift:
protocol Button {
var title: String { get }
func render()
}
struct PrimaryButton: Button {
var title: String
func render() { print("Синяя кнопка: \(title)") }
}
struct SecondaryButton: Button {
var title: String
func render() { print("Серая кнопка: \(title)") }
}
enum ButtonType {
case primary, secondary
}
// Фабричный метод
func createButton(type: ButtonType, title: String) -> Button {
switch type {
case .primary:
return PrimaryButton(title: title)
case .secondary:
return SecondaryButton(title: title)
}
}
// Использование
let button = createButton(type: .primary, title: "Отправить")
button.render() // "Синяя кнопка: Отправить"
Теперь, если вы захотите добавить новый тип кнопки, вам не придётся переписывать всю логику — просто добавляете новый case в фабрику.
3. Delegate (Делегат) — когда нужно, чтобы один объект работал за другой
Один из самых распространённых паттернов в Cocoa Touch. Делегат позволяет одному объекту делегировать (поручать) выполнение задач другому объекту. Это классическая реализация "один-к-одному".
Когда использовать:
- Обработка событий UI (например, нажатие на ячейку таблицы).
- Передача данных между контроллерами без создания жёсткой связи.
- Реализация кастомного поведения для стандартных компонентов.
Пример на Swift:
protocol ImageDownloadDelegate: AnyObject {
func downloadDidStart()
func downloadDidFinish(with image: UIImage?)
func downloadDidFail(error: Error)
}
class ImageDownloader {
weak var delegate: ImageDownloadDelegate?
func download(from url: URL) {
delegate?.downloadDidStart()
// Симуляция загрузки
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
let image = UIImage() // Представим, что это загруженное изображение
self.delegate?.downloadDidFinish(with: image)
}
}
}
class ViewController: UIViewController, ImageDownloadDelegate {
let downloader = ImageDownloader()
override func viewDidLoad() {
super.viewDidLoad()
downloader.delegate = self
downloader.download(from: URL(string: "https://example.com/image.jpg")!)
}
func downloadDidStart() {
print("Начинаем загрузку...")
}
func downloadDidFinish(with image: UIImage?) {
print("Загрузка завершена! Изображение получено.")
}
func downloadDidFail(error: Error) {
print("Ошибка загрузки: \(error.localizedDescription)")
}
}
Обратите внимание на weak var delegate. Это обязательное условие, чтобы избежать retain cycle (утечки памяти).
4. Observer (Наблюдатель) — когда о событии должны узнать все
В отличие от делегата, который работает один-к-одному, Observer позволяет одному объекту (субъекту) уведомлять множество подписчиков о своём состоянии. В Swift для этого часто используют Combine или NotificationCenter.
Когда использовать:
- Уведомления о смене темы или языка в приложении.
- Обновление UI при изменении данных в модели (MVVM).
- События, которые могут быть интересны многим модулям (например, авторизация пользователя).
Пример на Swift с NotificationCenter:
extension Notification.Name {