-
-
Давайте рассмотрим последние три принципа проектирования списка Роберта Мартина, касающиеся проектирования модулей. Эти принципы позволяют управлять связностью (coupling) ваших модулей. Как вы, наверно, помните из поста про метрики кода, чем меньше связность, тем лучше. Однако, полностью избавиться от зависимостей между модулями все равно нельзя. Если модули должны взаимодействовать, то между ними должна быть связь. Давайте разберемся, каких зависимостей следует избегать, а какие стоит использовать.
ADP. The Acyclic Dependencies Principle
Граф зависимостей между модулями приложения не должен содержать циклов.
Давайте рассмотрим пример. Мы разрабатываем приложение, которое состоит из нескольких модулей. В соответствии с принципом REP, каждый модуль и ничего не знает про исходники модулей, от которых он зависит.
Когда, например, разработчики модуля Task1 выпустят новую версию, то достаточно просто определить какие модули теперь нуждаются в обновлении: Task Window и Application. Разработчики этих модулей должны будут решить, когда они проинтегрируют свои модули с новой версией Task1.
Кроме того процесс сборки всего приложения тоже не вызывает затруднений. Достаточно пройтись по графу зависимостей снизу вверх и собрать последовательно каждый модуль.
Допустим, у нас появляются новые требования, и для их реализации мы добавляем в Task2 ссылку на Application.
В графе зависимостей у нас появился цикл. Теперь все модули, которые зависят от Task2, зависят еще и от Application. Теперь не получится просто пройтись по графу, чтобы собрать все приложение. Получается для того чтобы собрать Application нужно собрать все модули под ним, включая Task2. И одновременно, чтобы собрать Task2, нужно иметь уже собранный Application.
Одновременно – здесь ключевое слово. Получилось, что циклическая ссылка объединила все модули, попавшие в цикл, в один большой виртуальный модуль. Объединение их в один реальный модуль – это один из вариантов избавления от циклических ссылок. Но этот вариант не самый удачный.
Второй вариант избавления от циклов – вынесение функционала, от которого зависят Task2 и Application в отдельный новый модуль.
И наконец, третий вариант борьбы с циклами – применение принципа инверсии зависимостей (DIP). Для этого надо объявить в Task2 интерфейс, описывающий все, что Task2 хочет получать из Application. А в Application создать имплементацию этого интерфейса. Это позволит развернуть зависимость между Task2 и Application.
Стабильность
На картинке ниже изображена схема зависимостей между классами. Вы видите, что здесь применен принцип инверсии зависимостей. Вместо того чтобы явно ссылаться на KeyboardReader и PrinterWriter из класса Copy, мы объявили два интерфейса IReader и IWriter, поместили ссылки на них в класс Copy, и реализовали эти интерфейсы в KeyboardReader и PrinterWriter соответственно.
Как вы думаете, для каких из приведенных на рисунке типов вероятность изменения будет наименьшей? Наименьшей она будет для интерфейсов IReader и IWriter. По двум причинам.
Во-первых, интерфейсы приведенные на схеме ни от кого не зависят. Т.е. в нашей системе нет таких типов внесение изменений в которые, вызвало бы необходимость изменять IReader или IWriter. Типы, которые ни от кого не зависят, Мартин называет «независимыми» (independent).
Во-вторых, на интерфейсы ссылаются и те, кто их реализует, и те, кто их использует. Прежде чем вносить изменения в интерфейс стоит семь раз подумать хотите ли затем вносить изменения во все типы зависящие от этого интерфейса. Именно этот большой объем работы по внесению изменений во все зависимые типы, как раз и является тем фактором, что уменьшает вероятность изменения интерфейсов. Типы, от которых много кто зависит, Мартин называет «ответственными» (responsible).
Понятия независимости и ответственности для типов, как раз формируют определение стабильности. Наиболее стабильными являются независимые и ответственные типы, т.к. они не имеют причин для изменения и имеют много причин не изменяться. Соответственно чем меньше у типа ответственности и чем менее он независим, тем менее он стабилен, тем выше для него вероятность измениться.
SDP. The Stable Dependencies Principle
Зависимость должна быть в сторону большей стабильности. Модуль должен зависеть только от модулей более стабильных, чем он сам.
А как определить кто более стабильный, а кто нет? Как известно, для того чтобы что-то оценить, надо его изменить. Нестабильность любого пакета можно посчитать достаточно просто:
-
Сe (Efferent couplings) – число типов внутри пакета, которые зависят от типов из других пакетов.
-
Ca (Afferent couplings) – число типов в других пакетах, которые зависят от типов внутри пакета.
-
I (Instability) – Нестабильность пакета. Как видно из формулы эта метрика имеет диапазон [0, 1]. При 0 мы имеем идеально стабильный пакет, абсолютно независимый, и при этом ответственный. А при 1, наоборот, максимально нестабильный пакет, от которого никто не зависит, но который сам имеет много зависимостей.
Почему же зависимости от более стабильных модулей являются более предпочтительными, чем зависимости от менее стабильных модулей? Потому что такие зависимости уменьшают вероятность того что вам придется изменять свой модуль только из-за того что поменялся модуль от которого вы зависите.
SAP. Stable abstraction principle
Абстракция увеличивает стабильность. Уровень абстракции определяет уровень стабильности модуля.
Стабильные модули должны быть абстрактными. При этом их стабильность не будет препятствовать их расширению. И наоборот. Нестабильные модули должны быть конкретными. Это будет позволять изменять их.
Вспомните наш предыдущий пример. Интерфейсы IReader и IWriter – это абстракции. Они очень стабильны, т.к. ни от кого не зависят, и наоборот много кто зависит от них. Они могут быть легко расширены за счет новых реализаций и при этом останутся стабильными.
Стабильные пакеты более абстрактны, то есть имеют больше абстрактных классов и интерфейсов, редко изменяются. От них все зависят, а сами они мало зависят от остальных пакетов.
Принципы SDP и SAP вместе являются аналогом принципа Dependency Inversion. SDP рекомендует создавать связи только от менее стабильных пакетов к более стабильным, а SAP говорит, что стабильность подразумевает абстрактность.
В отличие от типов, которые однозначно либо абстрактные, либо нет, для модулей существует градация уровня абстрактности. Абстрактность модуля определяется отношением определенных в нем абстрактных типов к общему числу его типов.