Введение: Почему 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) — Принцип разделения интерфейсов
Суть: Клиенты не должны зависеть от методов, которые они не используют.