Введение: Почему 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