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

Введение

Принципы SOLID — это набор принципов, установленных Робертом С.Мартином (дядей Бобом). Основная цель этих принципов — разработать программное обеспечение, которое легко поддерживать, тестировать, понимать и расширять.

Эти принципы таковы:

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

Давайте договоримся о чем-то

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

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

На самом деле слово SOFT_WARE обещает, что любое программное обеспечение должно быть гибким и изменяемым. Таким образом, программное обеспечение должно следовать некоторым проверенным принципам проектирования, чтобы выполнить это обещание, одним из которых является принцип открытого-закрытого (OCP).

Теория

Принцип Open-Closed отвечает за букву О в принципах SOLID. Роберт К. Мартин считал этот принцип наиболее важным принципом объектно-ориентированного проектирования. Однако он не был первым, кто это определил. Первоначально об этом писал Бертран Мейер в 1988 году в своей книге Объектно-ориентированное проектирование программного обеспечения. Он заявил это как:

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

Но что это значит? Проще говоря, это означает, что если ваши бизнес-требования изменятся, вам не следует изменять существующий код (закрыто для модификаций). Вместо этого вы должны добавить новый код, который расширяет существующий код, не затрагивая его (Открыть для расширения).

В результате конечная сущность, соответствующая этому принципу, будет иметь две особенности:

  • Стабильный: поскольку этот объект закрыт для модификаций, его поведение не меняется и оно четко определено. Чтобы не было побочных эффектов на код, потребляющий эту сущность.
  • Гибкость: поскольку этот объект открыт для расширений, вы можете добавлять новые функции, не затрагивая существующий код.

Если вы читаете об этом принципе впервые, вы можете подумать, что здесь есть противоречие. Как программное обеспечение должно быть одновременно закрытым для модификаций и открытым для расширений?

Что ж, чтобы прояснить этот момент, давайте представим архитектуру плагинов.

Архитектура плагинов как практический пример для OCP

В своей статье дядя Боб сказал:

«Системы плагинов — это окончательное завершение, апофеоз принципа открытости-закрытости»

Но что делает архитектуру плагинов такой особенной? Подумайте об этом, если ваша система состоит из нескольких плагинов, которые можно легко подключать и отключать, не влияя друг на друга. Каждый плагин выполняет одну-единственную задачу и делает это очень хорошо. Система ничего не знает ни о каком новом плагине, который должен его расширить. Вместо этого этот новый плагин просто должен выполнять системный контракт. Это означает, что добавление или удаление плагина вообще не влияет на существующий код.

Да, как вы могли подумать, OCP и SRP каким-то образом связаны друг с другом.

Позвольте мне процитировать слова дяди Боба, чтобы подчеркнуть, насколько мощной является архитектура плагина:

«Что, если дизайн ваших систем основан на плагинах, таких как Vim, Emacs, Minecraft или Eclipse? Что, если бы вы могли подключить базу данных или графический интерфейс? Что, если бы вы могли подключать новые функции и отключать старые?

Что, если бы поведение вашей системы в значительной степени контролировалось конфигурацией ее плагинов? Какую силу это дало бы вам?

Насколько легко было бы добавить новые функции, новые пользовательские интерфейсы или новые интерфейсы машина/машина? Насколько легко было бы добавить или удалить SOA? Насколько легко было бы добавить или удалить REST?»

Ну, это интересно, не так ли?

Вы действительно очень хорошо это понимаете?

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

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

Взгляните на этот пример:

Если у вас предыдущий пример, и ваш менеджер просит вас убрать налог. Что вы собираетесь сделать в первую очередь? Это очевидно.

Итак, есть ли смысл добавлять/удалять новый/старый функционал, не трогая существующий код? Короткий ответ — да, но это не так просто, как вы думаете. Давай продолжим.

Сложные стороны OCP

Я думаю, что OCP является наиболее неправильно понятым из пяти принципов SOLID. Однако, если вы правильно примените его в своем дизайне, он будет самым полезным, чем другие.

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

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

Вы можете заметить, что этот класс является экстремальным по абстракции. Этот класс сам по себе не имеет функциональности, и ему передается 100% его функциональности, поэтому, возможно, его никогда не потребуется изменять.

С другой стороны, взгляните на этот пример ниже:

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

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

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

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

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

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

2. Еще один момент, о котором вы должны помнить, — это работа с ошибками. Что бы вы сделали, если бы в вашем классе была ошибка? вы бы принудительно расширили его и оставили бы унаследованный код с ошибкой для слепого выполнения OCP? или вы бы просто открыли свой класс и изменили бы эту ошибку напрямую? Поэтому я считаю, что исправление ошибок должно быть исключением из OCP.

3. Мы должны быть в состоянии предсказать, где находится изменение в нашем коде, и применить к нему абстракцию. В то же время мы не хотим разрабатывать слишком сложное программное обеспечение, которое пытается угадать все возможные способы его модификации в будущем. Таким образом, предсказание является ключевым и в то же время самым сложным для выполнения этого принципа. Более краткий способ следовать OCP — ввести принцип точки изменения (PV), который гласит:

Определите точки прогнозируемой изменчивости и создайте вокруг них стабильный интерфейс.

Но будьте осторожны, предсказывая свои варианты. Затраты на неправильное предсказание вариаций могут быть высокими:

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

Итак, итог по этому пункту следующий:

  • Не абстрагируйте вещи только потому, что вы предполагаете, что они вам нужны. Имейте в виду, что разработка и поддержка абстракции имеет свою цену. Но если вы заранее убедились на 100%, что он вам понадобится, вперед.
  • Вы не должны предсказывать каждую вариацию в своем программном обеспечении, однако вы несете ответственность за определение точек вариаций в вашем программном обеспечении.
  • Как только потребность в вариациях становится реальной, самое время провести рефакторинг вашего кода и предоставить правильную абстракцию, если она вам действительно нужна.

Практические способы следовать OCP

Я попытался максимально упростить предыдущий раздел, но на случай, если вы проиграли, вот практические способы следовать OCP:

  1. Если вы не можете заранее предсказать точки вариации в своем программном обеспечении, просто начните с конкретного и простого. Затем, по мере развития программного обеспечения, начните рефакторинг и построение правильной абстракции вокруг этих точек.
  2. Если вы на 100% уверены в своем прогнозе относительно точек вариации заранее, попробуйте определить правильный уровень абстракции, необходимый вашему программному обеспечению, не слишком усложняя вещи.

После теоретического представления OCP и его ограничений давайте перейдем к практическим примерам, чтобы увидеть, как мы можем применить OCP в нашем коде.

Подходы к применению OCP

Давайте начнем с этого конкретного примера и попробуем провести его рефакторинг с учетом OCP.

1. Функциональные параметры

Этот подход является наиболее простым и интуитивно понятным способом применения OCP и в то же время идеальным выбором в Функциональном программировании.

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

Давайте рефакторим приведенный выше пример, применив OCP и запомнив этот подход:

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

2. Частичная абстракция через наследование

Как мы уже говорили, OCP впервые применил Бертран Мейер в своей книге «Object-Oriented Software Construction». На самом деле первоначальный подход Мейера заключался в использовании наследования в качестве основного механизма применения OCP.

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

Давайте реорганизуем наш пример и запомним этот подход:

Как видите, вместо изменения исходного класса Logger мы только что добавили новый подкласс AnotherLogger, который переопределяет поведение родительского класса, то есть метод log.

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

3. Полная абстракция через композицию и интерфейсы

Может быть, вы слышали раньше о Программа для интерфейсов, а не реализации или Композиция над наследованием, не так ли? Из-за ограничений наследования Роберт С. Мартин переопределяет OCP, чтобы использовать композицию и интерфейсы вместо наследования Мейера. Но как мы можем его использовать?

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

Давайте перейдем к нашему примеру и применим композицию с использованием интерфейсов:

Как видите, теперь класс Logger не зависит ни от какой сущности. Только внедренный экземпляр должен реализовать интерфейс ILogger. Таким образом, вы можете использовать AnotherLogger или любой другой регистратор, если он реализует интерфейс ILogger.

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

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

Шаблон разработки стратегии

Паттерн проектирования Стратегия — отличный пример элегантного выполнения OCP. Это один из самых полезных шаблонов проектирования. Он в основном основан на программировании интерфейсов. Посмотрим, как это работает.

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

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

Я уже писал статью о паттерне Стратегия, вы можете ознакомиться с ней здесь.

Преимущества ОКП

После этого объяснения, я думаю, у вас появилось представление о преимуществах применения OCP в вашем коде. Подытожим некоторые из них:

  1. Если у вас есть пакет, который используется многими пользователями, вы можете предоставить им возможность расширять пакет, не изменяя его. В свою очередь, это уменьшает количество развертываний, что сводит к минимуму критические изменения.
  2. Чем меньше вы меняете существующий код, тем меньше в нем будет новых ошибок.
  3. Код становится проще, менее сложным и более понятным. Посмотрите на паттерн «Стратегия» в разделе выше.
  4. Добавление новой функциональности в новый класс позволяет вам идеально спроектировать его с нуля, не загрязняя существующий код и не создавая обходные пути.
  5. Поскольку новый класс ни от чего не зависит, нужно просто протестировать его не на всем существующем коде.
  6. Прикосновение к существующему коду в устаревших кодах может быть чрезвычайно напряженным, поэтому добавление новых функций в новый класс снижает этот стресс.
  7. Любой новый класс, который вы создаете, следует принципу единой ответственности.
  8. Это позволяет идеально разделить код на модули, что, в свою очередь, экономит время и деньги.

Краткое содержание

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

Как мы видели, этот принцип является одним из самых важных принципов проектирования. Следуя ему, вы сможете создавать модульные, удобные в сопровождении, читаемые, простые в использовании и тестируемые приложения.

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

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

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

Если вы нашли эту статью полезной, ознакомьтесь также с этими статьями:

Большое спасибо, что оставались со мной до этого момента. Надеюсь, вам понравится читать эту статью.

Ресурсы

Первоначально опубликовано на https://mayallo.com.