Представьте, что вы проектируете дом с модульной архитектурой. Вы хотите создать дизайн, который позволит владельцам добавлять в дом новые комнаты или секции, не снося при этом какие-либо существующие стены или постройки.
Для этого вам нужно будет спроектировать дом таким образом, чтобы он мог легко вместить новые дополнения. Вам нужно будет спланировать и убедиться, что существующая структура достаточно гибкая, чтобы приспосабливаться к изменениям, не требуя каких-либо серьезных модификаций.
Звучит слишком хорошо, чтобы быть правдой? Ну, это правда. Добро пожаловать в принцип открытого-закрытого.
Принцип
Принцип открытого-закрытого (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, которая помогает нам писать более надежный и удобный для сопровождения код. Понимая и применяя этот принцип, мы можем создавать программное обеспечение, более устойчивое к изменениям и более простое в управлении.
Удачного кодирования :)