SOLID принципы в Swift: теория и практика от А до Я

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

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

Начать курс

Введение: Почему SOLID — это фундамент качественного Swift-кода

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

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

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

Теория

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

Нарушение SRP (плохой код)

class UserManager {
    func createUser(name: String, email: String) {
        // Создание пользователя
        saveToDatabase(user: User(name: name, email: email))
        sendWelcomeEmail(to: email)
        logEvent("User created: \(name)")
    }
    
    private func saveToDatabase(user: User) { /* ... */ }
    private func sendWelcomeEmail(to email: String) { /* ... */ }
    private func logEvent(_ message: String) { /* ... */ }
}

Здесь UserManager отвечает за создание, сохранение, отправку писем и логирование — четыре причины для изменения.

Исправление (хороший код)

class UserService {
    private let repository: UserRepository
    private let emailService: EmailService
    private let logger: Logger
    
    init(repository: UserRepository, emailService: EmailService, logger: Logger) {
        self.repository = repository
        self.emailService = emailService
        self.logger = logger
    }
    
    func createUser(name: String, email: String) {
        let user = User(name: name, email: email)
        repository.save(user)
        emailService.sendWelcome(to: email)
        logger.log("User created: \(name)")
    }
}

class UserRepository {
    func save(_ user: User) { /* только сохранение */ }
}
class EmailService {
    func sendWelcome(to email: String) { /* только отправка */ }
}
class Logger {
    func log(_ message: String) { /* только логирование */ }
}

Теперь каждый класс имеет одну ответственность, что облегчает тестирование и поддержку.

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

Теория

Классы должны быть открыты для расширения, но закрыты для модификации. Новую функциональность добавляем через наследование или протоколы, не трогая существующий код.

Нарушение OCP

class PaymentProcessor {
    func process(payment: Any, type: String) {
        if type == "card" {
            // Обработка карты
        } else if type == "paypal" {
            // Обработка PayPal
        } // При добавлении нового типа — меняем этот класс
    }
}

Исправление через протоколы

protocol PaymentMethod {
    func process(amount: Double)
}

class CardPayment: PaymentMethod {
    func process(amount: Double) { /* обработка карты */ }
}

class PayPalPayment: PaymentMethod {
    func process(amount: Double) { /* обработка PayPal */ }
}

class PaymentProcessor {
    func process(payment: PaymentMethod, amount: Double) {
        payment.process(amount: amount)
    }
}

// Добавляем новый тип без изменения существующего кода
class ApplePayPayment: PaymentMethod {
    func process(amount: Double) { /* обработка Apple Pay */ }
}

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

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

Теория

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

Нарушение LSP

class Rectangle {
    var width: Double
    var height: Double
    
    func setWidth(_ w: Double) { width = w }
    func setHeight(_ h: Double) { height = h }
    func area() -> Double { return width * height }
}

class Square: Rectangle {
    override func setWidth(_ w: Double) {
        super.setWidth(w)
        super.setHeight(w) // Нарушение: квадрат меняет высоту при изменении ширины
    }
    override func setHeight(_ h: Double) {
        super.setHeight(h)
        super.setWidth(h)
    }
}

Если клиент ожидает Rectangle, но получает Square, поведение ломается.

Исправление

protocol Shape {
    func area() -> Double
}

class Rectangle: Shape {
    var width: Double
    var height: Double
    
    init(width: Double, height: Double) {
        self.width = width
        self.height = height
    }
    
    func area() -> Double { return width * height }
}

class Square: Shape {
    var side: Double
    
    init(side: Double) {
        self.side = side
    }
    
    func area() -> Double { return side * side }
}

Вместо наследования используем общий протокол — подстановка работает корректно.

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

Теория

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

Нарушение 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() { /* не нужно */ }
}

Исправление

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() { /* работает */ }
}

Робот реализует только нужные ему протоколы — никаких пустых методов.

5. Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей

Теория

Модули верхнего уровня не должны зависеть от модулей нижнего

Блоги

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