Представьте, что вы проектируете дом с модульной архитектурой. Вы хотите создать дизайн, который позволит владельцам добавлять в дом новые комнаты или секции, не снося при этом какие-либо существующие стены или постройки.

Для этого вам нужно будет спроектировать дом таким образом, чтобы он мог легко вместить новые дополнения. Вам нужно будет спланировать и убедиться, что существующая структура достаточно гибкая, чтобы приспосабливаться к изменениям, не требуя каких-либо серьезных модификаций.

Звучит слишком хорошо, чтобы быть правдой? Ну, это правда. Добро пожаловать в принцип открытого-закрытого.

Принцип

Принцип открытого-закрытого (OCP) объектно-ориентированного программирования гласит, что программные объекты (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации.

Проще говоря, это означает, что вы должны иметь возможность добавлять новые функции в свое программное обеспечение без необходимости изменять существующий код, который уже работает. Этот код останется нетронутым.

Хороший вопрос, который следует задать себе при реализации этого принципа, будет следующим: нужно ли мне будет модифицировать существующий код, когда я добавлю новую функциональность?

Но почему?

Почему важно, чтобы мы строили наш проект по этому принципу? Ну, есть несколько мотивов для использования этого принципа:

  • Удобство сопровождения. По мере развития проекта кодовая база может становиться все более сложной, что затрудняет поддержку и добавление новых функций. Если вы когда-либо пытались добавить новые функции в проект, сложность которого выросла, вы не понаслышке знаете, как это может быть неприятно. Принцип открытия-закрытия может обеспечить стабильность и удобство обслуживания вашей кодовой базы даже по мере развития вашего проекта.
  • Повторное использование. Разрабатывая программные компоненты, которые являются модульными и легко расширяемыми, мы можем повторно использовать существующий код для реализации новых функций или возможностей. Это может сэкономить нам время и усилия, поскольку нам не нужно начинать с нуля, когда мы хотим добавить что-то новое в наше программное обеспечение.

Давай попробуем

Хорошо, давайте рассмотрим пример, нарушающий принцип открытого-закрытого, и проанализируем его:

class PaymentService {
    constructor(amount, paymentProvider) {
        this.amount = amount
        this.paymentProvider = paymentProvider
    }

    makePayment() {
        switch (this.paymentProvider) {
            case 'credit':
                this.creditCardPayment()
                break
            case 'paypal':
                this.paypalPayment()
                break
        }
    }

    creditCardPayment() {
        console.log(`Performing credit payment of ${this.amount} dollars`)
    }

    paypalPayment() {
        console.log(`Performing paypal payment of ${this.amount} dollars`)
    }

}

Проблема с этим кодом заключается в том, что нам нужно внести изменения в оператор switch, если мы хотим использовать другого платежного провайдера, например Stripe. Давайте посмотрим:

class PaymentService {
    constructor(amount, paymentProvider) {
        this.amount = amount
        this.paymentProvider = paymentProvider
    }

    makePayment() {
        switch (this.paymentProvider) {
            case 'credit':
                this.creditCardPayment()
                break
            case 'paypal':
                this.paypalPayment()
                break
            // THIS PART IS NEW IN THE SWITCH STATEMENT
            case 'stripe':
                this.stripePayment()
                break
        }
    }

    creditCardPayment() {
        console.log(`Performing credit payment of ${this.amount} dollars`)
    }

    paypalPayment() {
        console.log(`Performing paypal payment of ${this.amount} dollars`)
    }

    stripePayment(){
        console.log(`Performing stripe payment of ${this.amount} dollars`)
    }

}

Таким образом, каждый раз, когда мы добавляем нового платежного провайдера, оператор switch должен будет меняться, и мы нарушим принцип открытия-закрытия.

Как мы можем решить эту проблему?

Хорошим подходом к реализации принципа открытости-закрытости является использование шаблона проектирования «Стратегия». Шаблон стратегии — это шаблон проектирования, который позволяет определить семейство алгоритмов, инкапсулировать каждый из них и сделать их взаимозаменяемыми. Таким образом, вы можете переключать используемый алгоритм во время выполнения без необходимости изменения контекста, который его использует. Это приводит к более гибкому и поддерживаемому коду.

Давайте посмотрим на это в действии:

Сначала мы создадим интерфейс или базовый класс с именем PaymentProvider, который объявляет метод для осуществления платежей. Это будет служить интерфейсом стратегии:

class PaymentProvider {
    makePayment(amount) {
        throw new Error('makePayment method must be implemented') // We throw an error so that we know we are not supposed to use this function, but override it instead
    }
}

Далее мы разделим классы для каждого платежного провайдера, реализующего интерфейс PaymentProvider, и предоставим их реализацию метода makePayment:

class CreditCardPaymentProvider extends PaymentProvider {
    makePayment(amount) {
        console.log(`Performing credit payment of ${amount} dollars`)
    }
}

class PaypalPaymentProvider extends PaymentProvider {
    makePayment(amount) {
        console.log(`Performing PayPal payment of ${amount} dollars`)
    }
}

class StripePaymentProvider extends PaymentProvider {
    makePayment(amount) {
        console.log(`Performing Stripe payment of ${amount} dollars`)
    }
}

Теперь мы изменим класс PaymentService, чтобы во время построения он принимал экземпляр PaymentProvider вместо строки поставщика платежа:

class PaymentService {
    constructor(amount, paymentProvider) {
        this.amount = amount
        this.paymentProvider = paymentProvider
    }

    makePayment() {
        this.paymentProvider.makePayment(this.amount)
    }
}

Теперь мы можем добавить столько провайдеров платежей, сколько захотим, и нам не нужно будет менять существующий код.

Заключение

Принцип открытого-закрытого — это фундаментальная концепция программирования SOLID, которая помогает нам писать более надежный и удобный для сопровождения код. Понимая и применяя этот принцип, мы можем создавать программное обеспечение, более устойчивое к изменениям и более простое в управлении.

Удачного кодирования :)