SOLID принципы в Swift: теория и практика для iOS/macOS

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему SOLID важен для Swift-разработчика

Разработка под iOS и macOS требует не только знания фреймворков, но и умения создавать поддерживаемую, тестируемую и масштабируемую архитектуру. Именно здесь на помощь приходят SOLID принципы — пять фундаментальных правил объектно-ориентированного проектирования, сформулированных Робертом Мартином (дядюшкой Бобом).

В этой статье мы разберём каждый из пяти принципов SOLID на примерах кода на Swift, покажем типичные антипаттерны и предложим правильные решения. После прочтения вы сможете писать код, который легко поддерживать, расширять и тестировать.

1. Single Responsibility Principle (SRP) — Принцип единственной ответственности

Формулировка: Класс должен иметь только одну причину для изменения. Другими словами, каждый модуль отвечает ровно за одну функциональность.

Проблема: Класс-монстр

Рассмотрим типичный пример из iOS-разработки — класс UserManager, который делает слишком много:

class UserManager {
    func fetchUser() -> User { /* сетевой запрос */ }
    func saveUserToDatabase(_ user: User) { /* работа с CoreData */ }
    func formatUserDisplayName(_ user: User) -> String { /* форматирование */ }
    func sendWelcomeEmail(to user: User) { /* отправка email */ }
}

Здесь класс отвечает и за сетевые запросы, и за базу данных, и за UI-форматирование, и за отправку писем. Изменение любого из этих аспектов приведёт к изменению UserManager.

Решение: Разделение ответственности

class UserNetworkService {
    func fetchUser() -> User { /* только сетевой запрос */ }
}

class UserDatabaseService {
    func saveUser(_ user: User) { /* только работа с БД */ }
}

class UserFormatter {
    func displayName(for user: User) -> String { /* только форматирование */ }
}

class EmailService {
    func sendWelcome(to user: User) { /* только отправка email */ }
}

Теперь каждый класс имеет одну чёткую ответственность, что упрощает тестирование и модификацию.

2. Open/Closed Principle (OCP) — Принцип открытости/закрытости

Формулировка: Программные сущности должны быть открыты для расширения, но закрыты для модификации.

Проблема: Жёсткая логика

Допустим, у нас есть класс для расчёта скидок:

class DiscountCalculator {
    func calculateDiscount(for order: Order, customerType: String) -> Double {
        if customerType == "regular" {
            return order.total * 0.05
        } else if customerType == "premium" {
            return order.total * 0.1
        } else if customerType == "vip" {
            return order.total * 0.2
        }
        return 0
    }
}

При добавлении нового типа клиента (gold) придётся изменять существующий класс, что нарушает OCP.

Решение: Протоколы и полиморфизм

protocol DiscountStrategy {
    func discount(for order: Order) -> Double
}

struct RegularDiscount: DiscountStrategy {
    func discount(for order: Order) -> Double { order.total * 0.05 }
}

struct PremiumDiscount: DiscountStrategy {
    func discount(for order: Order) -> Double { order.total * 0.1 }
}

struct VIPDiscount: DiscountStrategy {
    func discount(for order: Order) -> Double { order.total * 0.2 }
}

class DiscountCalculator {
    func calculateDiscount(for order: Order, using strategy: DiscountStrategy) -> Double {
        return strategy.discount(for: order)
    }
}

Теперь для добавления новой скидки достаточно создать новую структуру, реализующую протокол DiscountStrategy, без изменения DiscountCalculator.

3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков

Формулировка: Объекты в программе должны быть заменяемы экземплярами их подтипов без нарушения корректности работы программы.

Проблема: Нарушение контракта

Классический пример с геометрическими фигурами:

class Rectangle {
    var width: Double
    var height: Double
    
    func area() -> Double { width * height }
}

class Square: Rectangle {
    override var width: Double {
        didSet { height = width }
    }
    override var height: Double {
        didSet { width = height }
    }
}

Если клиентский код ожидает Rectangle и устанавливает ширину и высоту независимо, то Square сломает логику — изменение ширины изменит высоту, что не ожидается для прямоугольника.

Решение: Избегайте наследования, где контракт нарушается

protocol Shape {
    func area() -> Double
}

struct Rectangle: Shape {
    let width: Double
    let height: Double
    func area() -> Double { width * height }
}

struct Square: Shape {
    let side: Double
    func area() -> Double { side * side }
}

В Swift часто лучше использовать композицию и протоколы вместо глубокого наследования. Это гарантирует, что подтипы не нарушают поведение.

4. Interface Segregation Principle (ISP) — Принцип разделения интерфейса

Формулировка: Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше иметь несколько узкоспециализированных протоколов, чем один «толстый».

Проблема: Толстый протокол

protocol Worker {
    func work()
    func eat()
    func sleep()
}

class Human: Worker {
    func work() { /* работает */ }
    func eat() { /* ест */ }
    func sleep() { /* спит */ }
}

class Robot: Worker {
    func work() { /* работает */ }
    func eat() { /* робот не ест — пустая реализация */ }
    func sleep() { /* робот не спит — пустая реализация */ }
}

Robot вынужден реализовывать методы eat() и sleep(), которые ему не нужны, что ведёт к пустым или выбрасывающим исключения методам.

Решение: Разделение на мелкие протоколы

protocol Workable {
    func work()
}

protocol Eatable {
    func eat()
}

protocol Sleepable {
    func sleep()
}

class Human: Workable, Eatable, Sleepable {
    func work() { /* работает */ }
    func eat() { /* ест */ }
    func sleep() { /* спит */ }
}

class Robot: Workable {
    func work() { /* работает */ }
}

                
                

Блоги

Рекомендаций по книгам