Принцип открытости/закрытости (Open-Closed Principle; OCP), на мой взгляд, является главным принципом в списке принципов SOLID, в то время как все остальные в той или иной мере обеспечивают его соблюдение. Этот принцип сформулировал Бертран Мейер в своей книге «Object-Oriented Software Construction» в 1988 году следующим образом: «Программные сущности (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для изменения» (Software entities (classes, modules, functions, etc) should be open for extension, but closed for modification). Иными словами, должна иметься возможность изменять поведение программных сущностей без их изменения, как написал Роберт Мартин в книге «Чистая архитектура».
Мои рабочие процессы построены на основе практик гибкой методологии разработки программного обеспечения, и я не мог не обратить внимание на конфликт принципа открытости/закрытости и практик гибкой методологии, в частности, итеративной разработки. Последняя предполагает, что новые свойства, функции и классы в проекте появляются итеративно, а фактическое использование проекта может начаться задолго до того, как будет добавлено последнее свойство в структуры данных и реализована последняя функция. Следовательно, в рамках итеративной модели разработки принцип открытости/закрытости будет нарушаться, так как в классах и интерфейсах с течением времени будут появляться новые методы и свойства. Впрочем, нарушения можно свести к допустимому минимуму, либо полностью от них избавиться, соблюдая остальные принципы SOLID и используя шаблоны проектирования.
Несмотря на то, что принцип кажется простым, понятным и достаточно точным, в отличие от принципа единственной ответственности, он имеет два варианта: первый — классический, который так же называется принципом открытости/закрытости Мейера, второй — полиморфный принцип открытости закрытости. Предлагаю разобраться с ними подробнее.
Принцип открытости/закрытости Мейера
Для начала стоит определиться с пониманием открытости и закрытости программных сущностей. Сам Бертран Мейер в той же книге сформулировал их следующим образом:
- Можно сказать, что модуль открыт, если его можно расширить. Например, должно быть возможным добавление свойств к структурам данных, содержащихся в модуле, или новых функций, выполняемых им.
- Можно сказать, что модуль закрыт, если он доступен другим модулям. Это предполагает, что модулю дано четкое, стабильное описание (интерфейс в понимании сокрытия информации).
Классическая трактовка принципа открытости/закрытости предполагает, что однажды реализованный класс не должен изменяться с течением времени. Исключением из этого правила является исправление багов. Если требуется добавить новые свойства или методы, то это может быть достигнуто при помощи наследования — новые свойства и методы должны находиться в новом классе, который расширяет существующий. При этом новый класс может не реализовывать интерфейсы, которые реализует исходный класс, а так же может реализовывать другие интерфейсы.
Я стараюсь все классы делать либо абстрактными, если хочу их расширять, либо финальными, не финальные и не абстрактные классы в моей практике встречаются крайне редко. И это создаёт некоторый конфликт с принципом открытости/закрытости Мейера. Да, эта проблема легко решается созданием исходного абстрактного класса, но это не всегда красиво.
Полиморфный принцип открытости/закрытости
С развитием объектно-ориентированных языков программирования в 1990-х годах принцип открытости/закрытости претерпел изменения, сместив акцент с конкретных реализаций на интерфейсы и абстрактные классы (разработка «от интерфейса»), став таким образом полиморфным принципом открытости/закрытости. Он запрещает изменение интерфейсов и абстрактных классов, но позволяет вносить изменения в их реализации. В то же время допускается расширение интерфейсов и абстрактных классов при помощи наследования.
В рамках объектно-ориентированных языков программирования логичнее рассматривать именно полиморфный принцип открытости/закрытости. Да, конфликт с итеративной моделью разработки такой вариант принципа не решает, а вот классы теперь можно делать финальными.
Соблюдение принципа
В основе соблюдения полиморфного принципа открытости/закрытости лежит правило разработки «от интерфейса», когда разработчик должен опираться на интерфейсы, а не на конкретные их реализации. Так же многие шаблоны проектирования могут помочь соблюсти принцип открытости/закрытости. К таким можно отнести, например, оба варианта шаблона проектирования «адаптер», декоратор, абстрактную фабрику и стратегию.
Как я уже сказал в начале этой статьи все остальные принципы SOLID обеспечивают соблюдение принципа открытости/закрытости.
- Соблюдение принципов единственной ответственности и разделения интерфейсов снижает вероятность возникновения необходимости в изменении уже написанного кода.
- Соблюдение принципа подстановки [Барбары] Лисков гарантирует корректность работы системы при замене одной реализации интерфейса или абстрактного класса другой.
- Применение принципа инверсии зависимости организует зависимости между классами таким образом, чтобы менее значимые и более вариативные зависели от более значимых и менее вариативных.
Заключение
Применимо к проектам на объектно-ориентированных языках программирования в целом и к проектам на Java в частности старайтесь следовать полиморфному принципу открытости/закрытости через написание кода «от интерфейса», там где это возможно, а так же использование шаблонов проектирование и соблюдение остальных принципов SOLID. Соблюдение принципа открытости/закрытости позволит минимизировать шанс возникновения побочных эффектов при внесении изменений в программное обеспечение.
Для всех публичных API определяйте стабильный контракт, будь то интерфейсы, WSDL-схемы или описания OpenAPI, и не допускайте изменений в уже используемых контрактах. А в случае обнаружения багов старайтесь обеспечивать обратную совместимость при изменении контрактов. Старайтесь не удалять неиспользуемые по вашему мнению контракты сразу, вместо этого помечайте их для удаления в будущих версиях системы и отслеживайте их фактическое использование.