Приведи практические примеры применения принципов SOLID в Swift.

Ответ

SOLID — это набор из пяти принципов объектно-ориентированного дизайна, направленных на создание поддерживаемого и расширяемого кода.

1. S (Single Responsibility Principle — Принцип единственной ответственности)

Класс должен иметь одну и только одну причину для изменения.

// НАРУШЕНИЕ: Класс занимается и логикой, и сохранением, и валидацией.
class UserManager {
    func authenticate(user: String, pass: String) { /* ... */ }
    func saveToDatabase(user: User) { /* ... */ }
    func validateEmail(_ email: String) -> Bool { /* ... */ }
}

// СОБЛЮДЕНИЕ: Ответственности разделены.
class UserAuthenticator {
    func authenticate(credentials: Credentials) -> Bool { /* ... */ }
}

class UserRepository {
    func save(user: User) throws { /* ... */ }
    func fetch(userId: String) -> User? { /* ... */ }
}

class EmailValidator {
    func validate(_ email: String) -> Bool { /* ... */ }
}

2. O (Open/Closed Principle — Принцип открытости/закрытости)

Классы должны быть открыты для расширения, но закрыты для модификации.

protocol ReportGenerator {
    func generateReport() -> String
}

class PDFReportGenerator: ReportGenerator {
    func generateReport() -> String { return "PDF Report" }
}

class HTMLReportGenerator: ReportGenerator {
    func generateReport() -> String { return "HTML Report" }
}

class ReportProcessor {
    // Класс закрыт для модификации (не меняем его код),
    // но открыт для расширения (можно добавить новый тип генератора).
    func processReport(using generator: ReportGenerator) {
        let report = generator.generateReport()
        print("Processing: (report)")
    }
}

3. L (Liskov Substitution Principle — Принцип подстановки Барбары Лисков)

Объекты базового класса должны быть заменяемы объектами подклассов без изменения корректности программы.

// Базовый класс и подклассы, которые его корректно заменяют.
protocol Shape {
    var area: Double { get }
}

struct Rectangle: Shape {
    let width, height: Double
    var area: Double { width * height }
}

struct Circle: Shape {
    let radius: Double
    var area: Double { .pi * radius * radius }
}

// Функция работает с любым Shape, соблюдая LSP.
func printArea(of shape: Shape) {
    print("Area is: (shape.area)")
}

4. I (Interface Segregation Principle — Принцип разделения интерфейса)

Клиенты не должны зависеть от методов, которые они не используют.

// НАРУШЕНИЕ: Многофункциональный протокол заставляет SimplePrinter реализовывать ненужный метод.
protocol MultiFunctionDevice {
    func print(document: String)
    func scan() -> Data
    func fax(document: String)
}

// СОБЛЮДЕНИЕ: Протоколы разделены по функциям.
protocol Printable {
    func print(document: String)
}

protocol Scannable {
    func scan() -> Data
}

class SimplePrinter: Printable { // Реализует только нужное
    func print(document: String) { /* ... */ }
}

class Photocopier: Printable, Scannable { // Реализует несколько ролей
    func print(document: String) { /* ... */ }
    func scan() -> Data { /* ... */ }
}

5. D (Dependency Inversion Principle — Принцип инверсии зависимостей)

Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.

// Абстракция (протокол)
protocol DataStorage {
    func save(data: Data, forKey key: String)
    func loadData(forKey key: String) -> Data?
}

// Низкоуровневый модуль (деталь реализации)
class UserDefaultsStorage: DataStorage {
    func save(data: Data, forKey key: String) { UserDefaults.standard.set(data, forKey: key) }
    func loadData(forKey key: String) -> Data? { UserDefaults.standard.data(forKey: key) }
}

class KeychainStorage: DataStorage { /* ... */ }

// Высокоуровневый модуль зависит от абстракции, а не от конкретного хранилища.
class SettingsManager {
    private let storage: DataStorage // Зависимость от протокола

    // Внедрение зависимости через инициализатор (Dependency Injection)
    init(storage: DataStorage = UserDefaultsStorage()) {
        self.storage = storage
    }

    func saveSettings(_ settings: Settings) {
        let data = try! JSONEncoder().encode(settings)
        storage.save(data: data, forKey: "userSettings")
    }
}

Ответ 18+ 🔞

А, SOLID, ёпта! Ну, классика, блядь! Пять этих принципов, которые все вроде как знают, но соблюдают их так, будто это инструкция по сборке мебели из Икеи — вроде читаешь, а на выходе всё равно получается кривой табурет, на который сесть страшно. Давай разберём, что это за звери такие, но без заумного официоза, а по-человечьи.

S — Single Responsibility Principle (Принцип единственной ответственности)

Слушай, суть проще пареной репы: один класс — одна работа. Не надо делать из него швейцарский нож, который и бутерброд намажет, и гвоздь забьёт, и в жопу почешет. Вот смотри, как делать НЕ НАДО:

class UserManager {
    func authenticate(user: String, pass: String) { /* ... */ }
    func saveToDatabase(user: User) { /* ... */ }
    func validateEmail(_ email: String) -> Bool { /* ... */ }
}

Этот UserManager — просто маньяк-универсал, блядь! Он и логин проверяет, и в базу пихает, и почту валидирует. Представь, база данных поменялась — придётся лезть в этот класс и ковырять его. Почтовый стандарт обновился — опять за него. Полный пиздец, одним словом. А должно быть вот так:

class UserAuthenticator {
    func authenticate(credentials: Credentials) -> Bool { /* ... */ }
}

class UserRepository {
    func save(user: User) throws { /* ... */ }
    func fetch(userId: String) -> User? { /* ... */ }
}

class EmailValidator {
    func validate(_ email: String) -> Bool { /* ... */ }
}

Каждый занят своим делом. Один аутентифицирует, другой сохраняет, третий почту тыкает палкой. И если что-то сломается — сразу понятно, в каком именно чуваке искать проблему. Красота, ёпта!

O — Open/Closed Principle (Принцип открытости/закрытости)

Звучит как оксюморон, но смысл гениален: твой код должен быть как хороший конструктор — чтобы добавить новую детальку, не нужно ломать уже собранную башню. Расширяй, но не переделывай.

protocol ReportGenerator {
    func generateReport() -> String
}

class PDFReportGenerator: ReportGenerator {
    func generateReport() -> String { return "PDF Report" }
}

class HTMLReportGenerator: ReportGenerator {
    func generateReport() -> String { return "HTML Report" }
}

class ReportProcessor {
    func processReport(using generator: ReportGenerator) {
        let report = generator.generateReport()
        print("Processing: (report)")
    }
}

Видишь? ReportProcessor — он тупой как пробка, но в хорошем смысле. Ему похуй, какой генератор ты ему сунешь. PDF, HTML, хоть отчёт на бересте. Он просто говорит: «Дай сюда твой генератор, я с ним поработаю». И не надо его самого каждый раз переписывать. Захотел добавить вывод в Excel — просто делаешь новый класс ExcelReportGenerator, и всё. Сам процессор даже не чихнёт.

L — Liskov Substitution Principle (Принцип подстановки Барбары Лисков)

Это про то, что если у тебя есть, условно, «Птица», и от неё унаследованы «Воробей» и «Пингвин», то везде, где в коде ожидается «Птица», должен спокойно работать и воробей, и пингвин. А если пингвин не умеет летать и ломает всю логику — значит, наследование было ебаным идиотизмом.

protocol Shape {
    var area: Double { get }
}

struct Rectangle: Shape {
    let width, height: Double
    var area: Double { width * height }
}

struct Circle: Shape {
    let radius: Double
    var area: Double { .pi * radius * radius }
}

func printArea(of shape: Shape) {
    print("Area is: (shape.area)")
}

Вот тут всё чисто. Функция printArea принимает любую фигуру. Прямоугольник, круг, хоть треугольник — ей по барабану. Главное, чтобы площадь была. Если подсунешь что-то, что не умеет считать площадь — вот тогда и будет пиздец, нарушение принципа.

I — Interface Segregation Principle (Принцип разделения интерфейса)

Представь, ты покупаешь микроволновку, а в инструкции к ней на трёх страницах расписано, как ещё и стирать в ней бельё, и кофе молоть. Ты этим никогда пользоваться не будешь, но читать пришлось. Вот это и есть нарушение. Не заставляй класс реализовывать методы, которые ему нахуй не сдались.

// ТАК ДЕЛАТЬ — ПИЗДЕЦ КАК НЕ НАДО.
protocol MultiFunctionDevice {
    func print(document: String)
    func scan() -> Data
    func fax(document: String)
}

Зачем простому принтеру реализовывать scan и fax? Он же только печатать должен! Это как заставлять таксиста ещё и двигатель тебе починить по дороге. Бред.

// А вот так — красиво и правильно.
protocol Printable {
    func print(document: String)
}

protocol Scannable {
    func scan() -> Data
}

class SimplePrinter: Printable {
    func print(document: String) { /* ... */ } // И всё! Никакого лишнего геморроя.
}

class Photocopier: Printable, Scannable {
    func print(document: String) { /* ... */ }
    func scan() -> Data { /* ... */ } // А этот уже умеет и то, и другое. Молодец.
}

Каждый берёт только то, что ему нужно. Никакого насилия.

D — Dependency Inversion Principle (Принцип инверсии зависимостей)

Самый, пожалуй, важный для архитектуры. Высокоуровневые штуки (логика приложения) не должны зависеть от низкоуровневых (база данных, сеть, файловая система). Оба должны зависеть от какой-то абстракции (протокола, интерфейса).

protocol DataStorage {
    func save(data: Data, forKey key: String)
    func loadData(forKey key: String) -> Data?
}

class UserDefaultsStorage: DataStorage {
    func save(data: Data, forKey key: String) { UserDefaults.standard.set(data, forKey: key) }
    func loadData(forKey key: String) -> Data? { UserDefaults.standard.data(forKey: key) }
}

class KeychainStorage: DataStorage { /* ... */ }

class SettingsManager {
    private let storage: DataStorage // Смотри-ка! Зависит от протокола, а не от UserDefaults.

    init(storage: DataStorage = UserDefaultsStorage()) {
        self.storage = storage
    }

    func saveSettings(_ settings: Settings) {
        let data = try! JSONEncoder().encode(settings)
        storage.save(data: data, forKey: "userSettings") // Ему всё равно, что там под капотом.
    }
}

Вот в чём магия, блядь! SettingsManagerу похуй, куда сохраняются данные — в UserDefaults, в Keychain, в облако или на бумажку. Он просто тыкает в абстрактное DataStorage. Захотел поменять хранилище — подсунул другую реализацию в инициализатор. И ни одна строчка в самом менеджере не изменилась. Это и есть гибкость, ёпта!

Вот и весь SOLID, если без академического бреда. Соблюдаешь эти принципы — код живёт долго и счастливо, не соблюдаешь — превращается в монолитный говнокод, который все боятся трогать. Выбор за тобой.