Введение: Почему SOLID — это фундамент качественного Swift-кода
Каждый iOS/macOS разработчик рано или поздно сталкивается с ситуацией, когда код становится "спагетти": изменения в одном месте ломают всё приложение, а тестирование превращается в кошмар. Именно здесь на помощь приходят SOLID принципы — пять фундаментальных правил объектно-ориентированного проектирования, сформулированных Робертом Мартином.
В этой статье мы разберём каждый принцип SOLID на примерах Swift, покажем, как их нарушение приводит к проблемам, и дадим готовые решения. Вы научитесь писать код, который легко поддерживать, расширять и тестировать.
1. Single Responsibility Principle (SRP) — Принцип единственной ответственности
Теория
SRP гласит: у класса должна быть только одна причина для изменения. Другими словами, каждый класс отвечает ровно за одну функциональность.
Нарушение SRP (плохой код)
class UserManager {
func createUser(name: String, email: String) {
// Создание пользователя
saveToDatabase(user: User(name: name, email: email))
sendWelcomeEmail(to: email)
logEvent("User created: \(name)")
}
private func saveToDatabase(user: User) { /* ... */ }
private func sendWelcomeEmail(to email: String) { /* ... */ }
private func logEvent(_ message: String) { /* ... */ }
}
Здесь UserManager отвечает за создание, сохранение, отправку писем и логирование — четыре причины для изменения.
Исправление (хороший код)
class UserService {
private let repository: UserRepository
private let emailService: EmailService
private let logger: Logger
init(repository: UserRepository, emailService: EmailService, logger: Logger) {
self.repository = repository
self.emailService = emailService
self.logger = logger
}
func createUser(name: String, email: String) {
let user = User(name: name, email: email)
repository.save(user)
emailService.sendWelcome(to: email)
logger.log("User created: \(name)")
}
}
class UserRepository {
func save(_ user: User) { /* только сохранение */ }
}
class EmailService {
func sendWelcome(to email: String) { /* только отправка */ }
}
class Logger {
func log(_ message: String) { /* только логирование */ }
}
Теперь каждый класс имеет одну ответственность, что облегчает тестирование и поддержку.
2. Open/Closed Principle (OCP) — Принцип открытости/закрытости
Теория
Классы должны быть открыты для расширения, но закрыты для модификации. Новую функциональность добавляем через наследование или протоколы, не трогая существующий код.
Нарушение OCP
class PaymentProcessor {
func process(payment: Any, type: String) {
if type == "card" {
// Обработка карты
} else if type == "paypal" {
// Обработка PayPal
} // При добавлении нового типа — меняем этот класс
}
}
Исправление через протоколы
protocol PaymentMethod {
func process(amount: Double)
}
class CardPayment: PaymentMethod {
func process(amount: Double) { /* обработка карты */ }
}
class PayPalPayment: PaymentMethod {
func process(amount: Double) { /* обработка PayPal */ }
}
class PaymentProcessor {
func process(payment: PaymentMethod, amount: Double) {
payment.process(amount: amount)
}
}
// Добавляем новый тип без изменения существующего кода
class ApplePayPayment: PaymentMethod {
func process(amount: Double) { /* обработка Apple Pay */ }
}
Теперь для добавления нового способа оплаты достаточно создать новый класс, реализующий протокол PaymentMethod.
3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков
Теория
Объекты базового класса должны заменяться объектами подклассов без нарушения работы программы. Подкласс не должен ослаблять предусловия или усиливать постусловия.
Нарушение LSP
class Rectangle {
var width: Double
var height: Double
func setWidth(_ w: Double) { width = w }
func setHeight(_ h: Double) { height = h }
func area() -> Double { return width * height }
}
class Square: Rectangle {
override func setWidth(_ w: Double) {
super.setWidth(w)
super.setHeight(w) // Нарушение: квадрат меняет высоту при изменении ширины
}
override func setHeight(_ h: Double) {
super.setHeight(h)
super.setWidth(h)
}
}
Если клиент ожидает Rectangle, но получает Square, поведение ломается.
Исправление
protocol Shape {
func area() -> Double
}
class Rectangle: Shape {
var width: Double
var height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
func area() -> Double { return width * height }
}
class Square: Shape {
var side: Double
init(side: Double) {
self.side = side
}
func area() -> Double { return side * side }
}
Вместо наследования используем общий протокол — подстановка работает корректно.
4. Interface Segregation Principle (ISP) — Принцип разделения интерфейсов
Теория
Клиенты не должны зависеть от методов, которые они не используют. Лучше много специализированных протоколов, чем один "толстый".
Нарушение 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() { /* не нужно */ }
}
Исправление
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() { /* работает */ }
}
Робот реализует только нужные ему протоколы — никаких пустых методов.
5. Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей
Теория
Модули верхнего уровня не должны зависеть от модулей нижнего