Статьи

Fun с модулями

Тесная связь между модулями — плохая идея, и худшая форма связи — циклические зависимости между модулями. К счастью, есть несколько методов, которые мы можем использовать, чтобы разорвать циклы. Это Callback, Escalation и Demotion, и я собираюсь пройтись по некоторым примерам, которые показывают каждый из них в действии.

Затем, когда зависимости будут нарушены, мы рассмотрим еще два метода, которые позволяют нам полностью изменить и устранить отношения. Код для каждого из примеров можно найти в проекте edcie в моем репозитории кода Google . Каждый пример включает в себя сценарий сборки и простой контрольный пример. Для их выполнения вам понадобится GraphViz, если вы хотите использовать JarAnalyzer . Чтобы вызвать сценарии сборки без вызова JarAnalyzer, вы можете просто набрать:

ant compile

Имейте в виду, что каждый вариант системы имеет одинаковое поведение!

Пример

Пример, который мы собираемся использовать для обсуждения оставшейся части нашего обсуждения, невероятно прост. У нас есть класс Customer и Bill, которые мы собираемся объединить в два отдельных модуля — cust.jar и bill.jar. Есть также тестовый пример с именем PaymentTest,
который служит образцом клиента для управления взаимодействиями между двумя классами. Тестовый пример включен в модуль billtest.jar. Начальная диаграмма классов видна справа. Обратите внимание на двунаправленную связь между двумя классами.

По мере продвижения мы будем добавлять в систему больше классов и абстракций, чтобы повысить модульность. Кроме того, мы собираемся использовать JarAnalyzer, чтобы проиллюстрировать взаимосвязи между модулями, а также помочь нам оценить качество нашего дизайна. Ниже приведена структура модуля, созданная JarAnalyzer. Вы можете увидеть, как использовать JarAnalyzer в сборке, просмотрев файл сборки . Опять же, наша цель состоит в том, чтобы разорвать циклическую зависимость между cust.jar и bill.jar, и мы собираемся рассмотреть три различных способа сделать это, прежде чем перейти к рассмотрению различных способов массажа ациклических связей между модулями.

Исходная структура модуля с циклическими зависимостями

эскалация

Первая техника, которую мы собираемся применить, называется Эскалация. С помощью Escalation мы разрушаем циклические зависимости, увеличивая причину зависимости до объекта более высокого уровня. Прежде чем мы это сделаем, нам нужно более полно понять, почему в этом примере существует циклическая зависимость. Эта причина следует:

У Клиента есть список экземпляров Билла. Когда вызывается метод оплаты по Биллу, Биллу необходимо определить, следует ли применять скидку. Скидка — это продукт Клиента, которому принадлежит счет, а не обязательно Счет. Поэтому класс Bill вызывает метод Customer для определения соответствующей суммы скидки. Подумайте об этом так … Клиент представляет получателя, и мы договариваемся о скидке с каждым получателем. Расчет этой суммы со скидкой заключен внутри Клиента.

Чтобы разорвать эту зависимость, мы хотим расширить причину зависимости до класса более высокого уровня — CustomerMediator . Посредник теперь инкапсулирует расчет скидки и передает его классу счетов. Лучший способ увидеть это изменение — взглянуть на измененный
класс
PaymentTest . Теперь я изменил скрипт сборки и связал медиатор в его собственный модуль, как показано ниже. Если вы немного углубитесь в структуру класса, вы удивитесь, почему я не просто передал сумму скидки от Клиента в Билл. Не беспокойся об этом. Этот пример немного надуманный, потому что эскалация не лучший способ решить проблему такого типа. Ключевым выводом является то, что мы увеличили зависимость до пакета mediator.jar, нарушив циклическую зависимость.

Эскалация причины циклической зависимости

понижение в должности

Немного лучший способ решить этот конкретный тип циклической зависимости (где у нас действительно сложная связь между Клиентом и Биллом) — это использовать понижение в должности. С понижением в должности мы выдвигаем причину зависимости в модуль более низкого уровня. Точно противоположное эскалации. Мы делаем это, вводя класс DiscountCalculator, который будет передаваться в класс Bill . Наш модифицированный
класс
PaymentTest создаст калькулятор и передаст его нам. Класс Customer будет служить фабрикой для DiscountCalculator, поскольку именно Клиент знает, какая скидка должна применяться. Новая структура классов видна справа.

Теперь мы изменим наш скрипт сборки, чтобы создать дополнительный пакет calc.jar, который будет содержать класс DiscountCalculator. Наша полученная структура модуля показана ниже.

Понижение причины циклической зависимости

Уже сейчас вы можете увидеть, как это более естественное решение, чем эскалация для этого конкретного типа проблемы циклической зависимости. Какое ключевое отличие вы можете спросить? С эскалацией обратите внимание, как я смогу развернуть модули cust.jar и bill.jar независимо друг от друга. Хотя понижение в этом случае является более естественным решением, оно также означает, что для развертывания bill.jar или cust.jar мне также необходимо развернуть calc.jar. Правильное решение всегда будет контекстным, и идеальное решение может измениться на протяжении всего жизненного цикла разработки.

Перезвоните

Использование Callback похоже на шаблон Observer . При таком подходе мы проведем рефакторинг нашего класса DiscountCalculator в интерфейс , а затем изменим класс Customer для реализации этого интерфейса. Эту новую структуру классов можно увидеть справа.

Как это происходит в этой конкретной ситуации, использование обратного вызова представляет собой сочетание понижения в должности и нашего первоначального решения. Мы вернемся к передаче Клиента в счет , но передадим его как тип DiscountCalculator. В то время как в примере Demotion мы упаковали DiscountCalculator в отдельный модуль, теперь мы просто включим его в наш модуль bill.jar. Обратите внимание, что размещение DiscountCalculator в модуле cust.jar представит циклическую зависимость, от которой мы пытаемся избавиться. Новая структура модуля, которая напоминает нашу оригинальную версию за вычетом циклической зависимости, показана ниже.

Использование обратного вызова для устранения циклической зависимости

Инвертирующие отношения

Теперь мы немного поиграемся с отношениями модуля. Хотя Callback кажется наиболее логичным решением, что если мы захотим использовать модуль cust.jar без модуля bill.jar? Обратный вызов, поскольку он реализован, не позволяет нам сделать это. Но с небольшой хитростью я могу на самом деле инвертировать отношения между модулями cust.jar и bill.jar.

Я начну с рефакторинга класса Билла в интерфейс . Затем, чтобы избежать разделения пакетов (когда классы в одном пакете объединены в отдельные модули), я перемещаю класс Bill в тот же пакет, что и класс Customer . Новая диаграмма классов показана справа, а структура перевернутого модуля показана ниже.

Инвертированная структура модуля

Ликвидация отношений

Инвертирование отношений позволяет нам развертывать модуль cust.jar независимо от модуля bill.jar. Опять же, это все о необходимости. Но я хотел бы изучить другой вариант, основанный на другой важной необходимости — способности тестировать модули независимо. Прежде чем инвертировать отношения, я могу самостоятельно протестировать модуль bill.jar. После инвертирования отношений я могу протестировать модуль cust.jar самостоятельно. Но что, если я хочу протестировать (или развернуть) оба модуля независимо? Для этого мне нужно полностью устранить отношения вообще.

Оказывается, поскольку после инвертирования отношений у меня появилась довольно гибкая структура классов, я могу сделать это, просто объединив два интерфейса — Bill и DiscountCalculator — в отдельный модуль. Никаких других изменений кодирования не требуется. Я начну с перемещения их в новый пакет под названием base . Затем я модифицирую свой скрипт сборки,
чтобы связать эти два интерфейса в отдельный модуль base.jar, и мы успешно устранили связь между модулями bill.jar и cust.jar, как показано ниже.

Устранение отношений между модулями

Завершение

Мы прошли долгий путь от нашей оригинальной версии системы. Два модуля с циклическим отношением к структуре модуля, где исходные модули не имеют никакого отношения друг к другу. Это означает, что эти модули могут быть протестированы и развернуты независимо друг от друга. Если вы читаете этот блог, вы знаете, что я много писал о компромиссах
между гибкостью и сложностью, использованием и повторным использованием, а также о многих других архитектурных и дизайнерских задачах. Я надеюсь, что это небольшое упражнение помогло реализовать некоторые из этих концепций.

В заключение, для более глубокого изучения некоторых из этих проектных решений и более объективного изучения компромиссов я призываю вас запустить сборки для каждого из проектов и изучить файл dependencies.html в каталоге stats. , Для этого вам нужно убедиться, что JarAnalyzer запущен, для чего требуется GraphViz. Как вы увидите, сравнивая исходную версию с окончательной версией, мы добились значительного прогресса в улучшении качества дизайна.

С http://techdistrict.kirkk.com