Чистая архитектура. Искусство разработки программного обеспечения - страница 19




Рис. 5.1. Зависимости исходного кода следуют за потоком управления


Чтобы вызвать одну из функций верхнего уровня, функция main должна сослаться на модуль, содержащий эту функцию. В языке C для этой цели используется директива >#include. В Java – инструкция >import. В C# – инструкция >using. В действительности любой вызывающий код был вынужден ссылаться на модуль, содержащий вызываемый код.

Эти требования предоставляли архитектору программного обеспечения несколько вариантов. Поток управления определяется поведением системы, а зависимости исходного кода определяются этим потоком управления.

Однако когда в игру включился полиморфизм, стало возможным нечто совершенно иное (рис. 5.2).


Рис. 5.2. Инверсия зависимости


На рис. 5.2 модуль верхнего уровня >HL1 вызывает функцию >F() из модуля среднего уровня >ML1. Вызов посредством интерфейса является уловкой лишь для исходного кода. Во время выполнения интерфейсов не существует. Модуль HL1 просто вызывает >F() внутри >ML1[18].

Но обратите внимание, что направление зависимости в исходном коде (отношение наследования) между >ML1 и интерфейсом >I поменялось на противоположное по отношению к потоку управления. Этот эффект называют инверсией зависимости (dependency inversion), и он имеет далеко идущие последствия для архитекторов программного обеспечения.

Факт поддержки языками ОО надежного и удобного механизма полиморфизма означает, что любую зависимость исходного кода, где бы она ни находилась, можно инвертировать.

Теперь вернемся к дереву вызовов, изображенному на рис. 5.1, и к множеству зависимостей в его исходном коде. Любую из зависимостей в этом исходном коде можно обратить, добавив интерфейс.

При таком подходе архитекторы, работающие в системах, которые написаны на объектно-ориентированных языках, получают абсолютный контроль над направлением всех зависимостей в исходном коде. Они не ограничены только направлением потока управления. Неважно, какой модуль вызывает и какой модуль вызывается, архитектор может определить зависимость в исходном коде в любом направлении.

Какая возможность! И эту возможность открывает ОО. Собственно, это все, что дает ОО, – по крайней мере с точки зрения архитектора.

Что можно сделать, обладая этой возможностью? Можно, например, переупорядочить зависимости в исходном коде так, что база данных и пользовательский интерфейс (ПИ) в вашей системе будут зависеть от бизнес-правил (рис. 5.3), а не наоборот.


Рис. 5.3. База данных и пользовательский интерфейс зависят от бизнес-правил


Это означает, что ПИ и база данных могут быть плагинами к бизнес-правилам. То есть в исходном коде с реализацией бизнес-правил могут отсутствовать любые ссылки на ПИ или базу данных.

Как следствие, бизнес-правила, ПИ и базу данных можно скомпилировать в три разных компонента или единицы развертывания (например, jar-файлы, библиотеки DLL или файлы Gem), имеющих те же зависимости, как в исходном коде. Компонент с бизнес-правилами не будет зависеть от компонентов, реализующих ПИ и базу данных.

Как результат, появляется возможность развертывать бизнес-правила независимо от ПИ и базы данных. Изменения в ПИ или в базе данных не должны оказывать никакого влияния на бизнес-правила. То есть компоненты можно развертывать отдельно и независимо.

Проще говоря, когда реализация компонента изменится, достаточно повторно развернуть только этот компонент. Это