Когда появляется новая версия компонента, другие команды могут выбирать — сразу же задействовать новую версию или подождать. Если принято решение подождать, они просто продолжают использовать предыдущую версию. Но как только они решат, что готовы, они начинают использовать новую версию.
В результате ни одна команда не отдается на милость другим. Изменения в одном компоненте не оказывают немедленного влияния на другие команды. Каждая команда сама решает, когда начать адаптацию своего компонента для использования новой версии другого компонента. Кроме того, интеграция происходит небольшими шагами. Нет единого момента времени, когда все разработчики должны собраться вместе и интегрировать все, что они создали.
Этот очень простой и рациональный процесс получил широкое распространение. Однако чтобы добиться успеха, вы должны управлять структурой зависимостей компонентов. В ней не должно быть циклических зависимостей. Если в структуре зависимостей появятся циклы, «синдрома следующего утра» не избежать.
Взгляните на диаграмму компонентов на рис. 14.1. Она демонстрирует типичную структуру компонентов, собранных в приложение. В данном случае назначение приложения не играет роли — важна сама структура зависимостей компонентов. Обратите внимание, что структура имеет вид ориентированного (направленного) графа. Компоненты играют роль узлов, а зависимости между ними — ориентированных ребер.
Рис. 14.1. Типичная диаграмма компонентов
Обратите внимание на одну важную особенность: с какого бы компонента вы ни начали, вы не сможете пройти по связям-зависимостям и вернуться обратно в этот же компонент. Эта структура не имеет циклов. Это ациклический ориентированный граф (Directed Acyclic Graph; DAG).
Теперь представьте, что произойдет, когда одна из команд выпустит новую версию компонента Presenters. Определить, кого затронет этот факт, нетрудно, нужно лишь проследовать по стрелкам входящих зависимостей. То есть затронуты будут компоненты View и Main. В этот момент разработчики, развивающие эти компоненты, должны решить, когда начать их интеграцию с новой версией Presenters.
Отметьте также, что выпуск новой версии компонента Main не затронет никакой другой компонент в системе. Их разработчики вообще могут не знать о существовании компонента Main и никак не учитывать изменения, происходящие в нем. Это замечательно. Это означает, что выпуск новой версии Main оказывает минимальное влияние.
Когда разработчики компонента Presenters пожелают протестировать его, им просто нужно собрать свою версию Presenters с версиями компонентов Interactors и Entities, используемыми в данный момент. Никакой другой компонент в системе им не потребуется для этого. Это замечательно. Это означает, что разработчикам Presenters не придется прилагать значительных усилий для подготовки к тестированию и им достаточно учесть небольшое количество переменных.
Когда придет время выпустить новую версию всей системы, процесс будет протекать снизу вверх. Сначала будет скомпилирован, протестирован и выпущен компонент Entities. Затем те же действия будут выполнены с компонентами Database и Interactors. За ними последуют Presenters, View, Controllers и затем Authorizer. И наконец, очередь дойдет до Main. Это очевидный и легко воспроизводимый процесс. Мы знаем, в каком порядке собирать систему, потому что понимаем, как связаны зависимостями отдельные ее части.
Влияние циклов в графе зависимостей компонентов
Предположим, что появление новых требований вынудило нас изменить один из классов в компоненте Entities так, что он стал использовать класс из компонента Authorizer. Например, допустим, что класс User из Entities стал использовать класс Permissions из Authorizer. В результате образовалась циклическая зависимость, как показано на рис. 14.2.
Этот цикл немедленно приводит к появлению проблем. Например, разработчики, развивающие компонент Database, знают, что для выпуска новой версии они должны проверить совместимость с компонентом Entities. Но из-за образовавшегося цикла компонент Database теперь также должен быть совместим с Authorizer. Но Authorizer зависит от Interactors. Все это усложняет выпуск новой версии Database. Entities, Authorizer и Interactors фактически превращаются в один большой компонент — это означает, что всех разработчиков, развивающих эти компоненты, будет преследовать «синдром следующего утра». Они будут постоянно наступать друг другу на пятки из-за необходимости использования одних и тех же версий компонентов друг друга.
Рис. 14.2. Циклическая зависимость
Но список проблем этим не исчерпывается. Подумайте, что произойдет, когда нам потребуется протестировать компонент Entities. К нашему глубокому огорчению обнаружится, что мы должны собрать и интегрировать его с Authorizer и Interactors. Такая степень зависимости компонентов вызывает беспокойство, если она вообще допустима.
Возможно, вам уже приходилось задаваться вопросом, почему для простого модульного тестирования одного из классов приходится подключать так много библиотек и всякой другой всячины. Если бы вы копнули глубже, то наверняка бы обнаружили циклы в графе зависимостей. Такие циклы существенно усложняют изоляцию компонентов. Модульное тестирование и выпуск новой версии превращается в очень сложную задачу. Кроме того, проблемы, проявляющиеся во время сборки, начинают нарастать в геометрической прогрессии от количества модулей.
Более того, наличие циклов в графе зависимостей усложняет определение порядка сборки компонентов. И действительно, в этом случае нет правильного порядка. Это может повлечь другие неприятности в языках, таких как Java, которые извлекают объявления из скомпилированных двоичных файлов.
Разрыв цикла
Образовавшуюся циклическую зависимость всегда можно разорвать и привести граф зависимостей к форме ациклического ориентированного графа (DAG). Для этого используются два основных механизма:
1. Применить принцип инверсии зависимостей (Dependency Inversion Principle; DIP). В этом случае, как показано на рис. 14.3, можно было бы создать интерфейс, определяющий методы, необходимые классу User, затем поместить этот интерфейс в Entities и унаследовать его в Authorizer. Это обратило бы зависимость между Entities и Authorizer и разорвало цикл.
Рис. 14.3. Инверсия зависимости между Entities и Authorizer