Введение в 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()