Введение: Почему SOLID важен для Swift-разработчика
Разработка под iOS и macOS требует не только знания фреймворков, но и умения создавать поддерживаемую, тестируемую и масштабируемую архитектуру. Именно здесь на помощь приходят SOLID принципы — пять фундаментальных правил объектно-ориентированного проектирования, сформулированных Робертом Мартином (дядюшкой Бобом).
В этой статье мы разберём каждый из пяти принципов SOLID на примерах кода на Swift, покажем типичные антипаттерны и предложим правильные решения. После прочтения вы сможете писать код, который легко поддерживать, расширять и тестировать.
1. Single Responsibility Principle (SRP) — Принцип единственной ответственности
Формулировка: Класс должен иметь только одну причину для изменения. Другими словами, каждый модуль отвечает ровно за одну функциональность.
Проблема: Класс-монстр
Рассмотрим типичный пример из iOS-разработки — класс UserManager, который делает слишком много:
class UserManager {
func fetchUser() -> User { /* сетевой запрос */ }
func saveUserToDatabase(_ user: User) { /* работа с CoreData */ }
func formatUserDisplayName(_ user: User) -> String { /* форматирование */ }
func sendWelcomeEmail(to user: User) { /* отправка email */ }
}
Здесь класс отвечает и за сетевые запросы, и за базу данных, и за UI-форматирование, и за отправку писем. Изменение любого из этих аспектов приведёт к изменению UserManager.
Решение: Разделение ответственности
class UserNetworkService {
func fetchUser() -> User { /* только сетевой запрос */ }
}
class UserDatabaseService {
func saveUser(_ user: User) { /* только работа с БД */ }
}
class UserFormatter {
func displayName(for user: User) -> String { /* только форматирование */ }
}
class EmailService {
func sendWelcome(to user: User) { /* только отправка email */ }
}
Теперь каждый класс имеет одну чёткую ответственность, что упрощает тестирование и модификацию.
2. Open/Closed Principle (OCP) — Принцип открытости/закрытости
Формулировка: Программные сущности должны быть открыты для расширения, но закрыты для модификации.
Проблема: Жёсткая логика
Допустим, у нас есть класс для расчёта скидок:
class DiscountCalculator {
func calculateDiscount(for order: Order, customerType: String) -> Double {
if customerType == "regular" {
return order.total * 0.05
} else if customerType == "premium" {
return order.total * 0.1
} else if customerType == "vip" {
return order.total * 0.2
}
return 0
}
}
При добавлении нового типа клиента (gold) придётся изменять существующий класс, что нарушает OCP.
Решение: Протоколы и полиморфизм
protocol DiscountStrategy {
func discount(for order: Order) -> Double
}
struct RegularDiscount: DiscountStrategy {
func discount(for order: Order) -> Double { order.total * 0.05 }
}
struct PremiumDiscount: DiscountStrategy {
func discount(for order: Order) -> Double { order.total * 0.1 }
}
struct VIPDiscount: DiscountStrategy {
func discount(for order: Order) -> Double { order.total * 0.2 }
}
class DiscountCalculator {
func calculateDiscount(for order: Order, using strategy: DiscountStrategy) -> Double {
return strategy.discount(for: order)
}
}
Теперь для добавления новой скидки достаточно создать новую структуру, реализующую протокол DiscountStrategy, без изменения DiscountCalculator.
3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков
Формулировка: Объекты в программе должны быть заменяемы экземплярами их подтипов без нарушения корректности работы программы.
Проблема: Нарушение контракта
Классический пример с геометрическими фигурами:
class Rectangle {
var width: Double
var height: Double
func area() -> Double { width * height }
}
class Square: Rectangle {
override var width: Double {
didSet { height = width }
}
override var height: Double {
didSet { width = height }
}
}
Если клиентский код ожидает Rectangle и устанавливает ширину и высоту независимо, то Square сломает логику — изменение ширины изменит высоту, что не ожидается для прямоугольника.
Решение: Избегайте наследования, где контракт нарушается
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 }
}
В Swift часто лучше использовать композицию и протоколы вместо глубокого наследования. Это гарантирует, что подтипы не нарушают поведение.
4. Interface Segregation Principle (ISP) — Принцип разделения интерфейса
Формулировка: Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше иметь несколько узкоспециализированных протоколов, чем один «толстый».
Проблема: Толстый протокол
protocol Worker {
func work()
func eat()
func sleep()
}
class Human: Worker {
func work() { /* работает */ }
func eat() { /* ест */ }
func sleep() { /* спит */ }
}
class Robot: Worker {
func work() { /* работает */ }
func eat() { /* робот не ест — пустая реализация */ }
func sleep() { /* робот не спит — пустая реализация */ }
}
Robot вынужден реализовывать методы eat() и sleep(), которые ему не нужны, что ведёт к пустым или выбрасывающим исключения методам.
Решение: Разделение на мелкие протоколы
protocol Workable {
func work()
}
protocol Eatable {
func eat()
}
protocol Sleepable {
func sleep()
}
class Human: Workable, Eatable, Sleepable {
func work() { /* работает */ }
func eat() { /* ест */ }
func sleep() { /* спит */ }
}
class Robot: Workable {
func work() { /* работает */ }
}