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

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

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

Начать курс

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

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

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

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

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

Проблема: "Божественный" ViewController

Типичная ошибка — помещать всю логику в UIViewController. Рассмотрим плохой пример:

class UserProfileViewController: UIViewController {
    @IBOutlet weak var nameLabel: UILabel!
    
    func loadUser() {
        // 1. Сетевая логика
        let url = URL(string: "https://api.example.com/user")!
        URLSession.shared.dataTask(with: url) { data, _, _ in
            // 2. Парсинг JSON
            let user = try! JSONDecoder().decode(User.self, from: data!)
            // 3. Сохранение в Core Data
            self.saveToCoreData(user)
            // 4. Обновление UI
            DispatchQueue.main.async {
                self.nameLabel.text = user.name
            }
        }.resume()
    }
    
    private func saveToCoreData(_ user: User) { /* ... */ }
}

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

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

// 1. Сервис для работы с сетью
class UserNetworkService {
    func fetchUser(completion: @escaping (Result<User, Error>) -> Void) {
        let url = URL(string: "https://api.example.com/user")!
        URLSession.shared.dataTask(with: url) { data, _, error in
            if let data = data {
                do {
                    let user = try JSONDecoder().decode(User.self, from: data)
                    completion(.success(user))
                } catch {
                    completion(.failure(error))
                }
            } else if let error = error {
                completion(.failure(error))
            }
        }.resume()
    }
}

// 2. Менеджер хранения данных
class UserStorageManager {
    func save(_ user: User) {
        // Логика сохранения в Core Data / Realm
    }
}

// 3. ViewModel (или Presenter)
class UserViewModel {
    private let networkService = UserNetworkService()
    private let storageManager = UserStorageManager()
    
    var onUserUpdated: ((User) -> Void)?
    
    func loadUser() {
        networkService.fetchUser { [weak self] result in
            switch result {
            case .success(let user):
                self?.storageManager.save(user)
                self?.onUserUpdated?(user)
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
}

// 4. ViewController теперь только управляет UI
class UserProfileViewController: UIViewController {
    @IBOutlet weak var nameLabel: UILabel!
    private let viewModel = UserViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        viewModel.onUserUpdated = { [weak self] user in
            DispatchQueue.main.async {
                self?.nameLabel.text = user.name
            }
        }
        viewModel.loadUser()
    }
}

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

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

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

Проблема: Жесткая привязка к типам уведомлений

class NotificationSender {
    func send(_ notification: Notification, type: String) {
        if type == "email" {
            // Отправка email
            print("Sending email: \(notification.message)")
        } else if type == "push" {
            // Отправка push
            print("Sending push: \(notification.message)")
        }
        // При добавлении SMS придется менять этот класс!
    }
}

Решение: Протокол и стратегия

// Открываем для расширения через протокол
protocol NotificationChannel {
    func send(message: String)
}

// Конкретные реализации
class EmailChannel: NotificationChannel {
    func send(message: String) {
        print("Sending email: \(message)")
    }
}

class PushChannel: NotificationChannel {
    func send(message: String) {
        print("Sending push: \(message)")
    }
}

// Для добавления SMS не нужно менять NotificationSender
class SMSCannel: NotificationChannel {
    func send(message: String) {
        print("Sending SMS: \(message)")
    }
}

// Класс закрыт для модификации
class NotificationSender {
    private let channel: NotificationChannel
    
    init(channel: NotificationChannel) {
        self.channel = channel
    }
    
    func send(_ notification: Notification) {
        channel.send(message: notification.message)
    }
}

// Использование
let emailSender = NotificationSender(channel: EmailChannel())
emailSender.send(notification)

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

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

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

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

// Квадрат нарушает LSP!
class Square: Rectangle {
    override var width: Double {
        didSet { height = width }
    }
    override var height: Double {
        didSet { width = height }
    }
}

// Функция, работающая с Rectangle
func printArea(shape: Rectangle) {
    shape.width = 5
    shape.height = 10
    print(shape.area()) // Для Square выведет 100, а должно быть 50!
}

Решение: Абстракция через протокол

protocol Shape {
    func area() -> Double
}

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

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

// Теперь функция работает корректно
func printArea(shape: Shape) {
    print(shape.area())
}

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

Суть: Клиенты не должны зависеть от методов, которые они не используют.

Проблема: "

Блоги

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