Исправить ситуацию можно, применив принцип инверсии зависимостей (DIP). Для этого определим интерфейс US и поместим его в компонент с именем UServer. Этот интерфейс должен объявлять все методы, используемые классом U. Затем реализуем этот интерфейс в классе C, как показано на рис. 14.11. Это разорвет зависимость Stable от Flexible и вынудит оба компонента зависеть от UServer. UServer очень устойчив (I = 0), а Flexible сохранит желаемую неустойчивость (I = 1). Теперь все зависимости простираются в сторону уменьшения I.
Рис. 14.11. Класс C реализует интерфейс US
Абстрактные компоненты
Кому-то может показаться странным, что мы создали компонент — в данном примере UService, — не содержащий ничего, кроме интерфейса. То есть компонент не содержит выполняемого кода! Однако, как оказывается, это весьма распространенная и единственно возможная тактика в языках со статической системой типов, таких как Java и C#. Такие абстрактные компоненты очень устойчивы и поэтому служат идеальной целью для зависимостей в менее устойчивых компонентах.
В языках с динамической системой типов, таких как Ruby или Python, подобные абстрактные компоненты вообще отсутствуют, так же как зависимости, которые можно было бы нацелить на них. Структура зависимостей в этих языках намного проще, потому что для инверсии зависимостей не требуется объявлять или наследовать интерфейсы.
Принцип устойчивости абстракций
Устойчивость компонента пропорциональна его абстрактности.
Куда поместить высокоуровневые правила?
Некоторые части программных систем должны меняться очень редко. Эти части представляют высокоуровневые архитектурные и другие важные решения. Никто не желает, чтобы такие решения были изменчивыми. Поэтому программное обеспечение, инкапсулирующее высокоуровневые правила, должно находиться в устойчивых компонентах (I = 0). Неустойчивые (I = 1) должны содержать только изменчивый код — код, который можно было бы легко и быстро изменить.
Но если высокоуровневые правила поместить в устойчивые компоненты, это усложнит изменение исходного кода, реализующего их. Это может сделать всю архитектуру негибкой. Как компонент с максимальной устойчивостью (I = 0) сделать гибким настолько, чтобы он сохранял устойчивость при изменениях? Ответ заключается в соблюдении принципа открытости/закрытости (OCP). Этот принцип говорит, что можно и нужно создавать классы, достаточно гибкие, чтобы их можно было наследовать (расширять) без изменения. Какие классы соответствуют этому принципу? Абстрактные.
Введение в принцип устойчивости абстракций
Принцип устойчивости абстракций (Stable Abstractions Principle; SAP) устанавливает связь между устойчивостью и абстрактностью. С одной стороны, он говорит, что устойчивый компонент также должен быть абстрактным, чтобы его устойчивость не препятствовала расширению, с другой — он говорит, что неустойчивый компонент должен быть конкретным, потому что неустойчивость позволяет легко изменять его код.
То есть стабильный компонент должен состоять из интерфейсов и абстрактных классов, чтобы его легко было расширять. Устойчивые компоненты, доступные для расширения, обладают достаточной гибкостью, чтобы не накладывать чрезмерные ограничения на архитектуру.
Принципы устойчивости абстракций (SAP) и устойчивых зависимостей (SDP) вместе соответствуют принципу инверсии зависимостей (DIP) для компонентов. Это верно, потому что принцип SDP требует, чтобы зависимости были направлены в сторону устойчивости, а принцип SAP утверждает, что устойчивость подразумевает абстрактность. То есть зависимости должны быть направлены в сторону абстрактности.
Однако принцип DIP сформулирован для классов, и в случае с классами нет никаких полутонов. Класс либо абстрактный, либо нет. Принципы SDP и SAP действуют в отношении компонентов и допускают ситуацию, когда компонент частично абстрактный или частично устойчивый.
Мера абстрактности
Мерой абстрактности компонента служит метрика A. Ее значение определяется простым отношением количества интерфейсов и абстрактных классов к общему числу классов в компоненте.
• Nc: число классов в компоненте.
• Na: число абстрактных классов и интерфейсов в компоненте.
• A: абстрактность. A = Na ÷ Nc.
Значение метрики A изменяется в диапазоне от 0 до 1. 0 означает полное отсутствие абстрактных классов в компоненте, а 1 означает, что компонент не содержит ничего, кроме абстрактных классов.
Главная последовательность
Теперь мы можем определить зависимость между устойчивостью (I) и абстрактностью (A). Для этого нарисуем график со значениями A по вертикальной оси и значениями I — по горизонтальной (рис. 14.12). Если нанести на график «хорошие» компоненты обоих видов, обнаружится, что максимально устойчивые и абстрактные находятся слева вверху, в точке с координатами (0, 1), а максимально неустойчивые и конкретные — справа внизу, в точке (1, 0).
Рис. 14.12. График I/A
Но не все компоненты попадают в эти две точки, потому что компоненты имеют разные степени абстрактности и устойчивости. Например, очень часто один абстрактный класс наследует другой абстрактный класс. В результате получается абстрактный класс, имеющий зависимость. Поэтому, несмотря на абстрактность, он не будет максимально устойчивым. Зависимость уменьшает устойчивость.
Так как нельзя потребовать, чтобы все компоненты находились в двух точках (0, 1) или (1, 0), мы должны предположить, что на графике A/I имеется некоторое множество точек, определяющих оптимальные позиции для компонентов. Вывести это множество можно, определив области, где компоненты не должны находиться, — иными словами, определив зоны исключения (рис. 14.13).
Рис. 14.13. Зоны исключения
Зона боли
Рассмотрим компонент в точке (0, 0). Это очень устойчивый и конкретный компонент. Такие компоненты нежелательны, потому что слишком жесткие. Их нельзя расширить, потому что они неабстрактные, и очень трудно изменить из-за большой устойчивости. Поэтому правильно спроектированные компоненты обычно не должны находиться рядом с точкой (0, 0). Область вокруг точки (0, 0) — это зона исключения, которую называют зоной боли.