К какому компоненту отнести тот или иной класс? Это важное решение должно приниматься в соответствии с зарекомендовавшими себя принципами разработки программного обеспечения. К сожалению, подобные решения носят особый характер и принимаются почти исключительно исходя из контекста.
В этой главе мы обсудим три принципа, определяющих связность компонентов:
• REP: Reuse/Release Equivalence Principle — принцип эквивалентности повторного использования и выпусков;
• CCP: Common Closure Principle — принцип согласованного изменения;
• CRP: Common Reuse Principle — принцип совместного повторного использования.
Единица повторного использования есть единица выпуска.
За последнее десятилетие появилось множество инструментов управления модулями, таких как Maven, Leiningen и RVM. Эти инструменты приобрели особую важность, потому что за это же время создано огромное количество многократно используемых компонентов и библиотек компонентов. Сейчас мы живем в эпоху программного обеспечения многократного использования, когда исполнилось одно из самых древних обещаний объектно-ориентированной модели.
Принцип эквивалентности повторного использования и выпусков (Reuse/Release Equivalence Principle; REP) выглядит очевидным, по крайней мере сейчас. Люди, нуждающиеся в программных компонентах многократного использования, не смогут и не будут пользоваться компонентами, не прошедшими процесс выпуска и не получившими номер версии.
И совсем не потому, что без номера версии невозможно гарантировать совместимость всех повторно используемых компонентов. А потому, что разработчики программного обеспечения желают знать, когда появится новая версия и какие изменения в этой версии произойдут.
Нередко разработчики, знающие о приближающемся выпуске новой версии, оценивают грядущие изменения и принимают решение о продолжении использования старой версии или переходе на новую. Поэтому в процессе выпуска создатели компонента должны рассылать соответствующие извещения и описание новой версии, чтобы пользователи могли принимать обоснованные решения о том, когда и следует ли переходить на новую версию.
С точки зрения архитектуры и дизайна этот принцип означает, что классы и модули, составляющие компонент, должны принадлежать связной группе. Компонент не может просто включать случайную смесь классов и модулей; должна быть какая-то тема или цель, общая для всех модулей.
Все это, безусловно, очевидно. Однако есть еще один аспект, возможно, не такой очевидный. Классы и модули, объединяемые в компонент, должны выпускаться вместе. Объединение их в один выпуск и включение в общую документацию с описанием этой версии должны иметь смысл для автора и пользователей.
Впрочем, говорить, что что-то должно «иметь смысл» — это все равно, что размахивать руками и пытаться выглядеть авторитетно. Это не самый лучший совет, потому что сложно точно описать, что объединяет классы и модули в единый компонент. Однако слабость совета не умаляет важности самого принципа, потому что его нарушение легко определяется по «отсутствию смысла». Если вы нарушите принцип REP, ваши пользователи узнают об этом и усомнятся в вашей компетентности как архитектора.
Слабость этого принципа с лихвой компенсируется силой двух следующих принципов. В действительности принципы CCP CRP строго определяют этот принцип, хотя и в негативном ключе.
В один компонент должны включаться классы, изменяющиеся по одним причинам и в одно время. В разные компоненты должны включаться классы, изменяющиеся в разное время и по разным причинам.
Это принцип единственной ответственности (SRP), перефразированный для компонентов. Так же, как принцип SRP, гласящий, что класс не должен иметь нескольких причин для изменения, принцип согласованного изменения (CCP) требует, чтобы компонент не имел нескольких причин для изменения.
Для большинства приложений простота сопровождения важнее возможности повторного использования. Если возникнет необходимость изменить код приложения, предпочтительнее, если все изменения будут сосредоточены в одном месте, а не разбросаны по нескольким компонентам. Если изменения ограничиваются единственным компонентом, нам потребуется развернуть только один, изменившийся компонент. Другие компоненты, не зависящие от измененного, не придется повторно проверять и развертывать.
Принцип CCP требует от нас собирать вместе все классы, которые может понадобиться изменить по одной, общей причине. Если два класса тесно связаны, физически или концептуально, настолько, что всегда будут изменяться вместе, они должны принадлежать одному компоненту. Это поможет уменьшить трудозатраты, имеющие отношение к повторному выпуску, тестированию и развертыванию программного обеспечения.
Этот принцип тесно связан с принципом открытости/закрытости (Open Closed Principle; OCP). Фактически он означает «закрытость» в смысле принципа OCP. Принцип открытости/закрытости (OCP) требует, чтобы классы были закрыты для изменений, но открыты для расширения. Так как 100% закрытость невозможна, она должна носить скорее стратегический характер. Мы проектируем свои классы так, чтобы они были закрыты для изменений, наиболее типичных из ожидаемых по опыту.
Принцип согласованного изменения (CCP) развивает эту идею, предписывая объединять в один компонент только классы, закрытые для одного и того же вида изменений. То есть увеличивает вероятность, что изменение требований повлечет необходимость изменения минимального количества компонентов.
Как отмечалось выше, принцип согласованного изменения (CCP) есть форма принципа единственной ответственности (SRP) для компонентов. Принцип SRP требует выделять методы в разные классы, если они изменяются по разным причинам.
Принцип CCP аналогично требует выделять классы в разные компоненты, если они изменяются по разным причинам. Оба принципа можно привести к общей формуле:
Собирайте вместе все, что изменяется по одной причине и в одно время. Разделяйте все, что изменяется в разное время и по разным причинам.
Не вынуждайте пользователей компонента зависеть от того, чего им не требуется.
Принцип совместного повторного использования (Common Reuse Principle; CRP) — еще один принцип, помогающий определять, какие классы и модули должны включаться в компонент. Он указывает, что в компонент должны включаться классы и модули, используемые совместно.
Классы редко используются по отдельности. Обычно многократно используемые классы взаимодействуют с другими классами, являющимися частью многократно используемой абстракции. Принцип CRP указывает, что такие классы должны включаться в один компонент. Мы надеемся увидеть в компонентах классы, имеющие множественные зависимости друг от друга.
Простейшим примером могут служить класс коллекции и связанные с ним итераторы. Эти классы используются вместе, потому что они тесно связаны друг с другом. Соответственно, должны находиться в одном компоненте.
Но принцип CRP говорит не только о том, какие классы должны включаться в компонент; он также сообщает, какие классы не должны объединяться. Когда один компонент использует другой компонент, между ними образуется зависимость. Использующий компонент может нуждаться только в одном классе из используемого компонента, но это не ослабляет зависимости. Использующий компонент все так же зависит от используемого компонента.
Из-за этой зависимости изменение используемого компонента часто влечет необходимость соответствующих изменений в использующем компоненте. Даже если в использующем компоненте ничего не нужно изменять, его почти наверняка потребуется повторно скомпилировать, протестировать и развернуть. Это верно, даже если реализация использующего компонента совершенно не зависит от изменений в используемом компоненте.
То есть когда образуется зависимость от компонента, желательно, чтобы она распространялась на все классы в этом компоненте. Иначе говоря, классы, включаемые в компонент, должны быть неотделимы друг от друга — чтобы нельзя было зависеть от одних и не зависеть от других. Иначе нам придется повторно развертывать больше компонентов, чем требуется, и тратить существенно больше усилий.
Итак, принцип совместного повторного использования (CRP) в большей степени говорит о том, какие классы не должны объединяться, чем какие должны объединяться. Принцип CRP указывает, что классы, не имеющие тесной связи, не должны включаться в один компонент.
Принцип совместного повторного использования (CRP) является обобщенной версией принципа разделения интерфейсов (ISP). Принцип ISP советует не создавать зависимостей от классов, методы которых не используются. Принцип CRP советует не создавать зависимостей от компонентов, имеющих неиспользуемые классы.
Обобщая, эти советы можно объединить в один:
Не создавайте зависимостей от чего-либо неиспользуемого.
Возможно, вы уже заметили, что три принципа связности компонентов вступают в противоречие друг с другом. Принципы эквивалентности повторного использования (REP) и согласованного изменения (CCP) являются включительными: оба стремятся сделать компоненты как можно крупнее. Принцип повторного использования (CRP) — исключительный, стремящийся сделать компоненты как можно мельче. Задача хорошего архитектора — разрешить это противоречие.
На рис. 13.1 изображена диаграмма противоречий, показывающая, как три принципа связности влияют друг на друга. Ребра на диаграмме описывают цену нарушения принципа на противоположной вершине.
Архитектор, уделяющий внимание только принципам REP и CRP, обнаружит, что простое изменение вовлекает слишком большое количество компонентов. С другой стороны, архитектор, уделяющий особое внимание принципам CCP и REP, вынужден будет выпускать слишком много ненужных версий.
Рис. 13.1. Диаграмма противоречий принципов связности
Хороший архитектор найдет в этом треугольнике противоречий золотую середину, отвечающую текущим нуждам разработчиков, а также подумает об изменениях, которые могут произойти в будущем. Например, на ранних этапах разработки проекта принцип CCP намного важнее, чем REP, удобство разработки важнее удобства повторного использования.
Вообще говоря, в начале разработки наибольшую важность имеет правая сторона треугольника, когда единственной жертвой является повторное использование. Но по мере развития и интеграции в другие проекты фокус начинает смещаться влево. То есть организация компонентов в проекте может изменяться с течением времени. Это больше связано с тем, как проект разрабатывается и используется, нежели с тем, что фактически этот проект делает.
В прошлом мы смотрели на связность проще, чем предполагают принципы эквивалентности повторного использования (REP), согласованного изменения (CCP) и совместного повторного использования (CRP). Когда-то мы думали, что связность — это просто атрибут, что модуль выполняет одну и только одну функцию. Однако три принципа связности компонентов описывают намного более сложное многообразие. Выбирая классы для включения в компоненты, нужно учитывать противодействующие силы, связанные с удобством повторного использования и разработки. Поиск баланса этих сил, исходя из потребностей приложения, — непростая задача. Кроме того, баланс практически всегда постоянно смещается. То есть разбиение, считающееся удачным сегодня, может оказаться неудачным через год. Как следствие, состав компонентов почти наверняка будет изменяться с течением времени и смещением фокуса проекта с удобства разработки к удобству повторного использования.
См. раздел «Проблема с животными» в главе 27 «Службы: большие и малые».
Спасибо Тиму Оттингеру за эту идею.