Если у меня есть полный комплект модульных тестов для приложения, должен ли я по-прежнему применять принцип Open/Closed (OCP)?
Статья в Википедии об OCP гласит (выделено мое):
... принцип открытого / закрытого состояния гласит: "программные объекты (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации"... Это особенно ценно в производственной среде, где изменения в исходном коде может потребоваться проверка кода, модульные тесты и другие подобные процедуры, чтобы квалифицировать его для использования в продукте:код, подчиняющийся принципу, не изменяется при расширении и, следовательно, не требует таких усилий.
Итак, правильно ли я понимаю, что OCP был бы полезен, если нет автоматического модульного тестирования, но не обязательно, если он есть? Или статья в Википедии неправильная?
8 ответов
Модульные тесты, по определению!, относятся к поведению внутри модуля (как правило, одного класса): чтобы сделать их правильно, вы стараетесь изолировать тестируемый модуль от его взаимодействия с другими модулями (например, с помощью насмешек, внедрения зависимостей и скоро).
OCP - это поведение между единицами ("программными объектами"): если объект A использует объект B, он может расширять его, но не может его изменять. (Я думаю, что акцент в статье в Википедии исключительно на изменениях исходного кода неуместен: проблема относится ко всем изменениям, независимо от того, получены ли они с помощью изменений исходного кода или другими средствами времени выполнения).
Если A изменил B в процессе его использования, то на несвязанную сущность C, которая также использует B, впоследствии может быть оказано негативное влияние. Надлежащие модульные тесты, как правило, НЕ улавливают поломку в этом случае, потому что она не ограничена единицей: это зависит от тонкой, определенной последовательности взаимодействий между единицами, при этом A использует B, а затем C также пытается использовать B. Интеграция, регрессия или приемочные тесты МОГУТ его поймать, но вы никогда не можете положиться на такие тесты, обеспечивающие идеальное покрытие возможных путей кода (даже в модульных тестах достаточно сложно обеспечить идеальное покрытие в пределах одного объекта / объекта!-).
Я думаю, что в какой-то степени наиболее яркой иллюстрацией этого является противоречивая практика исправления обезьян, разрешенная в динамических языках и популярная в некоторых сообществах практиков таких языков (не все!-). Патч-обезьяна (MP) - это изменение поведения объекта во время выполнения без изменения его исходного кода, поэтому оно показывает, почему я думаю, что вы не можете объяснить OCP исключительно с точки зрения изменений исходного кода.
Депутат хорошо показывает пример, который я только что привел. Модульные тесты для A и C могут каждый проходить с летающими цветами (даже если они оба используют реальный класс B вместо того, чтобы издеваться над ним), потому что каждая единица, как таковая, действительно работает отлично; даже если вы тестируете ОБА (так что это уже далеко от тестирования UNIT), но бывает так, что вы тестируете С до А, все выглядит хорошо. Но, скажем, A обезьяна исправляет B, устанавливая метод B.foo, чтобы он возвращал 23 (как нужно A) вместо 45 (как документально поставляет B, а C полагается). Теперь это нарушает OCP: B должен быть закрыт для модификации, но A не соблюдает это условие и язык его не применяет. Затем, если A использует (и изменяет) B, а затем очередь C, C работает в состоянии, в котором он никогда не тестировался - то, где B.foo, недокументировано и удивительно, возвращает 23 (в то время как он всегда возвращал 45 во всем все испытания...!-).
Единственная проблема с использованием MP в качестве канонического примера нарушения OCP заключается в том, что оно может породить ложное чувство безопасности среди пользователей языков, которые явно не допускают MP; на самом деле, через конфигурационные файлы и опции, базы данных (где каждая реализация SQL позволяет ALTER TABLE
и тому подобное;-), удаленное взаимодействие, и т. д., и т. д., каждый достаточно большой и сложный проект должен следить за нарушениями OCP, даже если он написан на Eiffel или Haskell (и тем более, если якобы "статический" язык действительно позволяет программисты втыкают в память все, что хотят, до тех пор, пока у них есть правильные заклинания приведения, как это делают C и C++ - теперь это именно то, что вы определенно хотите отловить в обзорах кода;-).
"Закрыто для модификации" - это цель проекта - это не значит, что вы не можете изменить исходный код объекта, чтобы исправить ошибки, если такие ошибки найдены (и тогда вам понадобятся обзоры кода, дополнительное тестирование, включая регрессионные тесты для исправление ошибок и т. д., конечно).
Единственная ниша, где я видел "немодифицируемое после выпуска", широко применяемое, это интерфейсы для компонентных моделей, таких как старый добрый COM от Microsoft - ни один из опубликованных COM -интерфейсов никогда не может изменяться (так что в итоге вы получите IWhateverEx
, IWhatever2
, IWhateverEx2
и тому подобное, когда исправления интерфейса оказываются необходимыми - никогда не изменяются на исходные IWhatever
!-).
Даже в этом случае гарантированная неизменность применима только к интерфейсам - реализациям этих интерфейсов всегда разрешено иметь исправления ошибок, настройки оптимизации производительности и т. П. ("Делай правильно с первого раза" просто не работает при разработке ПО).: если бы вы могли выпускать программное обеспечение только тогда, когда на 100% уверены, что оно имеет 0 ошибок и максимально возможную и необходимую производительность на каждой платформе, на которой оно когда-либо будет использоваться, вы бы никогда ничего не выпустили, конкуренты съели бы ваш ланч, и вы буду банкротом;-). Опять же, такие исправления ошибок и оптимизации потребуют обзоров кода, тестов и т. Д., Как обычно.
Я полагаю, что дебаты в вашей команде происходят не из-за исправлений ошибок (кто-нибудь спорит за то, чтобы запретить их?), Или даже из-за оптимизации производительности, а скорее из-за вопроса о том, где размещать новые функции - должны ли мы добавить новый метод? foo
к существующему классу A
или точнее продлить A
в B
и добавить foo
в B
только так, чтобы A
остается "закрытым для модификации"? Модульные тесты сами по себе еще не отвечают на этот вопрос, потому что они могут не использовать все существующие A
(A
может быть выдумано, чтобы изолировать другую сущность, когда эта сущность будет проверена...), поэтому вам нужно пойти на один уровень глубже и посмотреть, что foo
точно, или может быть, делает.
Если foo
просто аксессор, и никогда не изменяет экземпляр A
на котором это называется, то добавление это явно безопасно; если foo
может изменить состояние экземпляра и последующее поведение как наблюдаемое другими существующими методами, тогда у вас возникнут проблемы. Если вы уважаете OCP и ставите foo
в отдельном подклассе ваши изменения очень безопасны и рутинны; если вы хотите простоту положить foo
прямо в A
тогда вам нужны подробные обзоры кода, легкие тесты "парной интеграции компонентов", проверяющие все варианты использования A
, и так далее. Это не ограничивает ваше архитектурное решение, но оно четко указывает на различные затраты, связанные с любым выбором, поэтому вы можете планировать, оценивать и расставлять приоритеты соответствующим образом.
Дикта и принципы Мейера не являются Священной Книгой, но, с должным критическим отношением, их очень стоит изучить и обдумать в свете ваших конкретных, конкретных обстоятельств, поэтому я рекомендую вас за это в этом случае! -)
Я думаю, что вы слишком много читаете в проклятом OCP. Моя интерпретация этого слова - "трижды подумайте, прежде чем модифицировать существующий класс, от поведения которого зависит большое количество кода, который не контролируется вами".
Если единственные пользователи - это вы и ваша собака, конечно, вы можете изменить мужество вне класса, будучи очень эффективными и не испытывая никаких проблем.
Если у вас много пользователей (независимо от того, являются они внутренними или внешними), вам действительно нужно учитывать все последствия, которые могут оказать изменения внутри рабочего класса, и, если ваша пользовательская база огромна, вы просто не сможете предвидеть и будете иметь чтобы:
- рискнуть что-то сломать для кого-то
- пусть расширяют
- раздуть дизайн, расширяя его самостоятельно
выберите лучшее для вашего случая использования.
Как всегда, понимание контекста и компромиссов - это то, что делает разработку интересной. Знайте, когда выбрать правильный инструмент. Бывают случаи, когда OCP не применяется, но это не отменяет его полезность, если вы рассмотрели его и отклонили, потому что это не относится к вашему контексту для A и B.
Хорошие принципы проектирования (такие как OCP) не добавляют шансов на хорошие процессы разработки (такие как модульные тесты и TDD). Они дополняют друг друга.
В статье Википедии IMO предполагается, что вы всегда используете процессы хорошего качества, такие как модульные тесты и обзоры кода (в XP это означает TDD и парное программирование), даже при использовании OCP. Далее говорится, что с OCP вы лучше контролируете объем своих изменений, что приводит к меньшим усилиям в этих процессах качества.
Я думаю, что это все еще ценный принцип, даже если у вас есть автоматическое модульное тестирование.
Внесение изменений в фрагмент кода также может нарушить тестирование, например, путем изменения имени метода. Это то, для чего предназначен OCP - не создавайте код, где вам нужно отредактировать предыдущий код, чтобы заставить его вести себя по-другому. Вместо этого сделайте так, чтобы, если вам нужно, чтобы он действовал по-другому, вы могли сделать это, создавая расширения.
Вы можете увидеть этот вид дизайна во многих местах: (N) перехватчики Hibernate, шаблоны данных WPF и т. Д. И т. Д.:)
В этой статье из C2 Wiki обсуждается напряжение между OCP и XP.
Из некоторых комментариев в этой статье и из этой академической диссертации (раздел B.2), ответ на вопрос "с модульными тестами, OCP все еще должен применяться?" Казалось бы, нет.
Причина заключается в том, что OCP учитывает влияние функциональных изменений на рабочий код с помощью предварительного проектирования, когда абстракции создаются на ранней стадии, в надежде, что они обеспечат достаточную расширяемость позже, когда появятся новые требования.
Agile-разработка (с XP или просто TDD), с другой стороны, направлена на воздействие изменений через эволюционный дизайн (кто-нибудь? ", А не на попытки обдумать абстракции заранее). И, как мы все знаем, предварительный дизайн вряд ли работает на практике.
Модульные тесты помогают придерживаться принципа открытого / закрытого: необходимые изменения (рефакторинг плохого кода) проверяются набором модульных тестов, чтобы проверить, не было ли видимых извне изменений в поведении.
Как и большинство идей, выдвинутых Бертраном Мейером, принцип Open/Closed в значительной степени просто ошибочен. Если вашей системе нужны какие-то новые функциональные возможности, и эти функциональные возможности принадлежат существующему классу, измените этот класс. Не кладите это куда-то еще только для того, чтобы удовлетворить произвольный закон.