Почему вы должны знать о принципе открытия-закрытия?

Tags: Software Development, Solid Principles

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

Принцип, созданный Бертран Мейер, гласит:

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

Позже, когда он включил его в свои принципы SOLID, Боб Мартин выразил это лучше:

“Вы должны иметь возможность расширять поведение системы без необходимости ее изменения”

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

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

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

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

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

public class TotallyAbstract<TArg, TRes>

{

private Func<TArg, TRes> f;

public TotallyAbstract(Func<TArg, TRes> f)

{

  this.f = f;

}

public TRes Apply(TArg a)

{

  return f(a);

}

}

Вы заметите, что он на самом деле не имеет функциональности вообще! Вот как раз наоборот: вы можете заставить его сделать что-то одно, и вам придется изменить его, чтобы сделать что-нибудь еще:

public class TotallyConcrete

{

public int Apply()

{

  return 2 + 2;

}

}

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

Функция или программа могут иметь свою функциональность, изменяемую параметрами и данными конфигурации, которые она получает. Принцип Open Closed, похоже, не говорит об этой адаптации функциональности, когда он говорит о «расширении». Это звучит, как написание кода, а не данных: однако граница между ними всегда размыта в большей или меньшей степени. Конфигурация модуля может быть выполнена с использованием файлов данных или кода.

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

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

https://8thlight.com/blog/uncle-bob/2014/05/12/TheOpenClosedPrinciple.html.

Это, конечно, пример инверсии зависимостей, D в SOLID. На самом деле все расширение модуля с кодом является формой инверсии зависимостей: разница в том, какую абстракцию вы используете для указания зависимости, которая будет введена:

  • Параметры функции (без данных)

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

  • Абсолютно абстрактные параметры класса (интерфейсы или полностью абстрактные базовые классы)

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

  • Частично абстрактные параметры класса (наследование реализации)

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

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

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



No Comments

Add a Comment