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

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

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

Начать курс

Введение в SOLID принципы в Swift

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

Независимо от того, пишете ли вы на UIKit, SwiftUI или AppKit, SOLID — это универсальный фундамент. Давайте погрузимся в детали.

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

Теория

Класс должен иметь только одну причину для изменения. Иными словами, каждый модуль отвечает ровно за одну функциональность. Если ваш ViewController управляет UI, обрабатывает сетевые запросы и парсит JSON — он нарушает SRP.

Практика на Swift

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

// ❌ Плохо: нарушение SRP
class UserViewController: UIViewController {
    func loadUser() {
        // Сетевой запрос
        let url = URL(string: "https://api.example.com/user")!
        URLSession.shared.dataTask(with: url) { data, _, _ in
            let user = try? JSONDecoder().decode(User.self, from: data!)
            DispatchQueue.main.async {
                self.updateUI(with: user)
            }
        }.resume()
    }
    
    func updateUI(with user: User?) {
        // Обновление UI
    }
}
// ✅ Хорошо: разделяем ответственность
class UserService {
    func fetchUser(completion: @escaping (User?) -> Void) {
        let url = URL(string: "https://api.example.com/user")!
        URLSession.shared.dataTask(with: url) { data, _, _ in
            let user = try? JSONDecoder().decode(User.self, from: data!)
            completion(user)
        }.resume()
    }
}

class UserViewController: UIViewController {
    let userService = UserService()
    
    func loadUser() {
        userService.fetchUser { [weak self] user in
            DispatchQueue.main.async {
                self?.updateUI(with: user)
            }
        }
    }
    
    private func updateUI(with user: User?) {
        // Обновление UI
    }
}

Вывод: Теперь UserService отвечает только за сеть, а UserViewController — за UI. Изменения в логике сети не затрагивают контроллер.

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

Теория

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

Практика на Swift

Представьте, что мы пишем модуль для расчёта скидок в интернет-магазине.

// ❌ Плохо: при добавлении новой скидки нужно менять класс
class DiscountCalculator {
    func calculate(price: Double, type: String) -> Double {
        if type == "seasonal" {
            return price * 0.9
        } else if type == "loyalty" {
            return price * 0.85
        }
        return price
    }
}
// ✅ Хорошо: используем протокол для расширения
protocol DiscountStrategy {
    func applyDiscount(to price: Double) -> Double
}

struct SeasonalDiscount: DiscountStrategy {
    func applyDiscount(to price: Double) -> Double {
        price * 0.9
    }
}

struct LoyaltyDiscount: DiscountStrategy {
    func applyDiscount(to price: Double) -> Double {
        price * 0.85
    }
}

class DiscountCalculator {
    func calculate(price: Double, strategy: DiscountStrategy) -> Double {
        strategy.applyDiscount(to: price)
    }
}

// Использование
let calculator = DiscountCalculator()
let price = calculator.calculate(price: 100, strategy: SeasonalDiscount())

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

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

Теория

Объекты базового класса должны быть заменяемы объектами его подклассов без нарушения корректности программы. В контексте Swift это означает, что наследник не должен сужать контракт родителя. Нарушение LSP часто приводит к неожиданным багам.

Практика на Swift

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

// ❌ Плохо: нарушение LSP
class Rectangle {
    var width: Double
    var height: Double
    
    func setWidth(_ w: Double) { width = w }
    func setHeight(_ h: Double) { height = h }
    func area() -> Double { width * height }
}

class Square: Rectangle {
    override func setWidth(_ w: Double) {
        super.setWidth(w)
        super.setHeight(w)
    }
    
    override func setHeight(_ h: Double) {
        super.setWidth(h)
        super.setHeight(h)
    }
}

// Клиентский код ожидает, что можно менять ширину и высоту независимо
func resize(rectangle: Rectangle) {
    rectangle.setWidth(5)
    rectangle.setHeight(10)
    // Для Square area() вернёт 100, а ожидается 50
}
// ✅ Хорошо: разделяем через протокол
protocol Shape {
    func area() -> Double
}

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

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

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

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

Теория

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

Практика на Swift

// ❌ Плохо: один протокол для всего
protocol Worker {
    func work()
    func eat()
    func sleep()
}

class Human: Worker {
    func work() { print("Работаю") }
    func eat() { print("Ем") }
    func sleep()                

Блоги

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