SOLID принципы в Swift: теория и практика для продвинутых iOS разработчиков

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

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

Начать курс

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

В мире iOS-разработки создание поддерживаемого и масштабируемого кода — ключевая задача. SOLID принципы, сформулированные Робертом Мартином, остаются золотым стандартом архитектуры ПО. На продвинутом уровне владение этими принципами в Swift позволяет не только писать чистый код, но и строить гибкие системы, устойчивые к изменениям требований. В этой статье мы разберем каждый принцип с примерами на Swift, ориентируясь на реальные задачи iOS/macOS-разработчика.

1. Single Responsibility Principle (SRP) в Swift

Теория: Каждый класс или структура должна иметь только одну причину для изменения. На практике это означает, что модуль отвечает за одну четко определенную задачу.

Проблема: перегруженный ViewController

В UIKit часто встречаются контроллеры, которые одновременно управляют UI, обрабатывают сетевые запросы и сохраняют данные. Это нарушает SRP.

// ❌ Нарушение SRP
class UserViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    private var users: [User] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        fetchUsers()
    }

    private func fetchUsers() {
        // Сетевой запрос
        URLSession.shared.dataTask(with: URL(string: "https://api.example.com/users")!) { data, _, _ in
            // Парсинг JSON
            // Сохранение в Core Data
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        }.resume()
    }
}

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

Выносим работу с данными и сетью в отдельные сервисы.

// ✅ SRP: отдельный сервис для работы с пользователями
protocol UserServiceProtocol {
    func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void)
}

class UserService: UserServiceProtocol {
    func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) {
        // Логика сетевого запроса
    }
}

// ✅ SRP: отдельный менеджер для Core Data
class CoreDataManager {
    func saveUsers(_ users: [User]) {
        // Сохранение в Core Data
    }
}

// ✅ SRP: ViewController только управляет UI
class UserViewController: UIViewController {
    private let userService: UserServiceProtocol
    private let coreDataManager: CoreDataManager

    init(userService: UserServiceProtocol, coreDataManager: CoreDataManager) {
        self.userService = userService
        self.coreDataManager = coreDataManager
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        loadUsers()
    }

    private func loadUsers() {
        userService.fetchUsers { [weak self] result in
            switch result {
            case .success(let users):
                self?.coreDataManager.saveUsers(users)
                self?.updateUI(with: users)
            case .failure(let error):
                self?.showError(error)
            }
        }
    }
}

2. Open/Closed Principle (OCP) и протоколы Swift

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

Пример: система уведомлений

// ❌ Нарушение OCP: при добавлении нового типа уведомления нужно менять switch
class NotificationSender {
    func send(_ notification: Notification, type: String) {
        switch type {
        case "email":
            // Отправка email
        case "push":
            // Отправка push
        default:
            break
        }
    }
}

// ✅ OCP: используем протокол
protocol NotificationChannel {
    func send(_ notification: Notification)
}

class EmailChannel: NotificationChannel {
    func send(_ notification: Notification) {
        // Логика отправки email
    }
}

class PushChannel: NotificationChannel {
    func send(_ notification: Notification) {
        // Логика отправки push
    }
}

class NotificationSender {
    private let channels: [NotificationChannel]

    init(channels: [NotificationChannel]) {
        self.channels = channels
    }

    func send(_ notification: Notification) {
        channels.forEach { $0.send(notification) }
    }
}

// Теперь можно добавить SMS, не меняя существующий код
class SMSChannel: NotificationChannel {
    func send(_ notification: Notification) {
        // Логика отправки SMS
    }
}

3. Liskov Substitution Principle (LSP) в иерархиях Swift

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

Типичное нарушение: прямоугольник и квадрат

// ❌ Нарушение LSP
class Rectangle {
    var width: Double
    var height: Double
    
    func area() -> Double {
        return width * height
    }
}

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

// Клиентский код
func printArea(of rectangle: Rectangle) {
    rectangle.width = 4
    rectangle.height = 5
    print("Area: \(rectangle.area())") // Для Square будет 25, а не 20!
}

// ✅ Решение: используем протокол или отдельную иерархию
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 }
}

4. Interface Segregation Principle (ISP) и Dependency Inversion (DIP)

ISP: Разделение "толстых" протоколов

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

// ❌ Толстый протокол
protocol Worker {
    func work()
    func eat()
    func sleep()
}

// ✅ ISP: разделяем на мелкие протоколы
protocol Workable {
    func work()
}

protocol Eatable {
    func eat()
}

protocol Sleepable {
    func sleep()
}

// Теперь робот может реализовать только Workable
struct Robot: Workable {
    func work() { /* ... */ }
}

DIP: Инверсия зависимостей

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

// ❌ Нарушение DIP: жесткая зависимость от конкретной реализации
class UserViewModel {
    private let apiClient = APIClient() // Прямая зависимость

    func loadUser(id: Int) {
        apiClient.fetchUser(id: id)
    }
}

// ✅ DIP: зависимость от абстракции
protocol UserRepository {
    func fetchUser(id: Int) async throws -> User
}

class UserViewModel {
    private let repository: UserRepository

    init(repository: UserRepository) {
        self.repository = repository
    }

    func loadUser(id: Int) async throws -> User {
        return try await                

Блоги

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