В чём суть принципа открытости/закрытости (Open-Closed Principle, OCP)?

Ответ

Принцип открытости/закрытости (OCP) — второй принцип SOLID. Он гласит: «Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации».

  • «Закрыты для модификации»: Исходный код стабильной, протестированной сущности не должен изменяться для добавления новой функциональности.
  • «Открыты для расширения»: Поведение сущности можно расширить, создавая новые классы, реализующие её интерфейс (абстракцию).

Проблема (нарушение OCP):

class Logger {
    func log(message: String, to type: String) {
        if type == "console" {
            print(message)
        } else if type == "file" {
            // Запись в файл
        }
        // Добавление нового типа лога (например, "network") требует изменения этого метода!
    }
}

Решение (соблюдение OCP через абстракцию):

// Абстракция, закрытая для модификации
protocol LogDestination {
    func write(_ message: String)
}

// Расширения, открытые для добавления
class ConsoleDestination: LogDestination {
    func write(_ message: String) { print(message) }
}

class FileDestination: LogDestination {
    func write(_ message: String) { /* Запись в файл */ }
}

class NetworkDestination: LogDestination {
    func write(_ message: String) { /* Отправка по сети */ }
}

// Класс Logger теперь стабилен
class Logger {
    private let destination: LogDestination
    init(destination: LogDestination) {
        self.destination = destination
    }
    func log(message: String) {
        destination.write(message)
    }
}

// Использование
let logger = Logger(destination: NetworkDestination())
logger.log("Test message")

Выгода: Чтобы добавить новый способ логирования, мы создаем новый класс, реализующий LogDestination, не трогая существующий код Logger, ConsoleDestination или FileDestination. Это снижает риск появления новых ошибок в уже работающем коде.

Ответ 18+ 🔞

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

Вот смотри, классический пример говнокода, который все пишут, пока не прилетит по шапке. Делают они класс Logger, а в нём метод, который решает, куда лог пихнуть.

class Logger {
    func log(message: String, to type: String) {
        if type == "console" {
            print(message)
        } else if type == "file" {
            // Запись в файл
        }
        // Добавление нового типа лога (например, "network") требует изменения этого метода!
    }
}

Ну и что тут такого, спросит какой-нибудь юный пирожок? А то, сука, что завтра приходит менеджер и орёт: «Хочу логи в телеграм-бот слать, ёпта!». И ты, блядь, лезешь в этот метод, который уже, может, сто раз протестирован и работает, и начинаешь там тыкать:

else if type == "telegram" { ... }

А потом ещё захотят в Slack, а потом в БД, а потом на печать, в рот меня чих-пых! И с каждым разом ты рискуешь сломать то, что уже работало. Это ж как Герасим, который всех подряд мочил, только ты сам себе Герасим, и мочишь свой же рабочий код, блядь!

А теперь, внимание, магия, ёпта! Делаем по-взрослому, с абстракцией.

Сначала объявляем протокол — это такая бумажка с печатью, которая говорит: «Всё, что умеет write(_ message: String), может быть местом для логов». Закрыли тему нахуй.

protocol LogDestination {
    func write(_ message: String)
}

А дальше — вжух, вжух, вжух! — делаем сколько угодно реализаций. Это и есть «открыто для расширения». Хочешь в консоль?

class ConsoleDestination: LogDestination {
    func write(_ message: String) { print(message) }
}

Хочешь в файл? Пожалуйста, на тебе файл!

class FileDestination: LogDestination {
    func write(_ message: String) { /* Запись в файл */ }
}

Пришла дикая идея слать логи на Марс через сеть? Да хуй с ним, делаем новый класс, а старый код даже не чихнёт!

class NetworkDestination: LogDestination {
    func write(_ message: String) { /* Отправка по сети */ }
}

И сам Logger превращается в элегантную, блядь, недотрогу. Он больше не знает НИХУЯ о том, куда пишутся логи. Он просто берёт то, что ему дали, и говорит: «На, запиши».

class Logger {
    private let destination: LogDestination
    init(destination: LogDestination) {
        self.destination = destination
    }
    func log(message: String) {
        destination.write(message)
    }
}

Использование? Элементарно, Ватсон!

let logger = Logger(destination: NetworkDestination())
logger.log("Test message")

Выгода-то какая, блядь? А такая, что если завтра понадобится логировать в облако амазона, ты не полезешь в Logger, ConsoleDestination или FileDestination с криками «мама, прости!». Ты просто создашь новый класс AmazonCloudDestination, реализуешь один ёбаный метод write, и всё. Существующий код останется нетронутым, как девственница в монастыре. Риск накосячить — ноль ебать. Вот что значит следовать принципу, а не писать код, как бомж в подворотне.