Почему "защита от изменений" подразумевает направление зависимости?
Немного предыстории: я программист-самоучка, который начал работать на Python и изучал Java, когда я присоединился к MegaCorp(TM) 6 лет назад. Имея степень по математике, я довольно солиден (без каламбура) по алгоритмам и критическому мышлению, но я часто нахожусь с пробелом в знаниях, связанных со структурами данных, дизайном или другими основами CompSci, которые мои коллеги изучили в своей информатике курсы.
С этой целью я попросил старшего инженера в моей команде рекомендовать книгу, чтобы помочь заполнить мои пробелы, и он предложил Чистую архитектуру.
Я прохожу около трети пути и действительно смущен одним из основных мотивирующих факторов предложений. Дядя Боб представляет многие идеи и принципы (в том числе принципы SOLID, о которых я слышал ранее, хотя я все еще сталкиваюсь с принципом подстановки Лискова) как предназначенные для "защиты" некоторой части системы от требований изменить. Есть несколько примеров этого, но самый ясный на странице 73:
Если компонент A должен быть защищен от изменений в компоненте B, тогда компонент B должен зависеть от компонента A.
(Должен отметить, что в отсутствие какого-либо фактического определения, которое я вижу, я считаю "компонент" эквивалентным пакету Java, хотя я думаю, что те же мыслительные процессы могут быть применены, если "компонент" "это отдельная услуга - оба должны предоставлять пользователям стабильные используемые интерфейсы, вызываемые локально или через сеть)
Это утверждение не имеет никаких доказательств, и это не самоочевидно для меня. Рассмотрим случай класса ClassA
в компоненте (упаковке) ComponentA
какие звонки DoStuffReturn doStuff(DoStuffInput input, String someOtherArg)
в ClassB
в компоненте ComponentB
- и при этом вызов осуществляется либо через прямую зависимость, либо через зависимость от интерфейса в ComponentB
(не в ComponentA
, как советует Чистая Архитектура)
- Если изменение в B является функциональным изменением, а не изменением подписи (т. Е. Для того же
DoStuffInput
а такжеString
вход, другойDoStuffReturn
возвращается), то в A не требуется никаких изменений:ClassA
призыв кClassB.doStuff
остается действительным (те же аргументы и тип возвращаемого значения)ClassA
Юнит-тесты (которые должны быть использованы по изданиюClassB
s) должен пройти- Любые функциональные / интеграционные тесты, которые проверяют, как
ClassA
а такжеClassB
сотрудничать нужно будет обновить свои ожидания, но это не изменениеComponentA
(если эти тесты не находятся вComponentA
- Я обычно видел их вComponentAIntegrationTests
пакет, но я думаю, что они также могут быть расположены вместе. Похоже, что это не то, что предлагает книга - похоже, речь идет об изменениях в коде, а не в тестах)
- Если изменение в B является изменением подписи на метод, отличный от
doStuff
, то А не потребует никаких изменений - Если изменение в B является изменением подписи на
doStuff
, тогда A потребует изменения - но это было бы так, если бы интерфейс тоже был в A.
(Обратите внимание, что в соответствии с настройкой Чистой архитектуры, в которой интерфейс для предоставляющего класса находится в потребляющем Компоненте (A), только первый случай будет представлять собой изменение в B - так что это единственное, что нам действительно нужно беспокоиться с)
Что мне не хватает? Если ComponentA зависит от ComponentB, при каких обстоятельствах изменение класса в ComponentB потребует изменения в ComponentA?
(Обратите внимание, что я не выступаю против использования интерфейсов - у них есть множество других преимуществ, не в последнюю очередь позволяющих одновременную разработку "по обе стороны" контракта и позволяющих заменять различные реализации интерфейса).
2 ответа
Чтобы объяснить, как мы защищаем один компонент от изменений в другом, инвертируя направление зависимости, давайте сначала определим компонент и зависимость. В контексте чистой архитектуры компоненты - это файлы JAR, а зависимости - это ссылки между классами.
Компоненты - это единицы развертывания. Это самые маленькие объекты, которые могут быть развернуты как часть системы. В Java это файлы jar. - страница 96
Первое, на что нужно обратить внимание, это то, что все зависимости являются зависимостями исходного кода. Стрелка, указывающая из класса A в класс B, означает, что в исходном коде класса A упоминается имя класса B, а в классе B ничего не говорится о классе A. - стр. 72
Тогда возникает вопрос, от каких изменений мы хотим защитить компоненты (банки)? И ответ таков: перекомпиляция, перераспределение и переходные зависимости.
Если компонент A
зависит от компонента B
тогда любое изменение в B
(даже если это изменение не влияет на API, который A
потребляет) требует A
перекомпилировать. Кроме того, если изменение в B
добавляет, удаляет или изменяет зависимость B
, затем A
должны согласовать новую транзитивную зависимость, и поэтому изменение распространяется.
Эта зависимость означает, что изменение исходного кода [
B
] заставит [A
] для перекомпиляции и повторного развертывания, хотя ничего, о чем оно заботилось, на самом деле не изменилось. - страница 84
Транзитивные зависимости являются нарушением общего принципа, согласно которому программные объекты не должны зависеть от того, что они не используют напрямую. - страница 75
Я объединяю элементы более чем одного из принципов SOLID здесь; но они имеют общую общность, когда дело доходит до обработки изменений.
Что касается моего прошлого, у меня есть степень по компьютерным наукам и более 20 лет профессионального опыта.
Перво-наперво. Ничто не очень хорошо определено в этой области (хорошо, некоторые математические вещи). Даже базовые вещи, такие как сама объектная ориентация или инкапсуляция, единая ответственность, и более сложные вещи, такие как доменно-управляемый дизайн, REST и т. Д. Есть абсолютно разные мнения по всему. Иногда популярные интерпретации или эти вещи являются худшим вариантом.
Я пытаюсь сказать: сохраняй свой скептицизм даже перед лицом непреодолимой власти. Задавайте вопросы и всегда старайтесь найти вескую причину для поддержки чего-либо. Не просто "это более приемлемо", это также гипотеза, а не доказательство.
Мне кажется, что дядя Боб - далеко не идеальный источник информации об объектной ориентации. Вот подробная критика "Чистой архитектуры" (не книга, идея), которую я написал некоторое время назад, если вам интересно.
Вернемся к вашей проблеме. Сама цитата в основном говорит, что направление зависимости идет вразрез с распространением изменений ( здесь снова моя статья с некоторыми красивыми картинками). Это не подразумевает (и не должно), что изменение всегда течет обратно через зависимость, но это делает "знание". Всегда. Иногда изменения не нуждаются в внешнем виде, как изменение подписи. Изменения в емкости, производительности иногда ломают изменения, но не видны непосредственно в подписи. Значение метода может измениться без каких-либо видимых признаков.
Итак, вы правы, внутренние изменения могут вообще не вызывать никаких других изменений. Но единственный способ избежать потенциального изменения чего-либо - это вообще не зависеть от этого чего-либо.
Второй наилучший подход - иметь хорошую абстракцию и инкапсуляцию, что, к сожалению, "чистой архитектуре" не очень хорошо. Использование Anemic Objects и чистых данных в качестве интерфейсов, которые продвигает дядя Боб, разрушает любую абстракцию или инкапсуляцию, которые может иметь компонент.