Предположим, у вас есть график взаимодействующих объектов, например, такой:
чтобы выполнить какое-либо действие, вы должны отправить сообщение в объект Facade, который будет взаимодействовать с остальной частью графика для получения результата. Есть по крайней мере два основных способа, которыми выходные данные этого вычисления могут выйти из этого графа объекта:
- через возвращаемое значение Facade.
- Передается объекту Target, переданному при первоначальном вызове
Второй вариант работает так:
facade.doSomething(Target target, ...) // somewhere in the graph: target.accept(result);
В этой статье я хочу исследовать, какие контексты требуют первого решения, а какие — второго.
равноценность
Прежде всего, мы можем сказать, что в некоторых базовых сценариях нет большой разницы между решением: это просто вопрос стиля. Первый стиль напоминает мне о функциональном программировании, а второй — о стиле обратных вызовов приложений JavaScript, поскольку его неблокирующая природа делает целевые объекты и функции необходимыми.
Стоимость решения с точки зрения кода аналогична. В первом случае у вас есть:
- ответное заявление на фасаде
- оператор возврата для промежуточных объектов
- оператор возврата на объектах Leaf
В секунду:
- Целевой интерфейс
- дополнительный параметр на фасаде
- дополнительный параметр на промежуточных объектах
- дополнительный параметр на объектах Leaf
Не то чтобы в некоторых языках операторы return и конструкция интерфейса могли быть неявными, поэтому я не говорю о строках кода и их длине; Я рассчитываю затраты времени, которые мы должны платить каждый раз, когда читаем код: метод с 3 параметрами относительно сложнее понять по сравнению с тем же методом без одного из них; точно так же, метод void легче рассуждать и модифицировать, чем метод, который возвращает результат.
Размеры: кто производит результат
Функциональный подход, безусловно, выигрывает, когда конечное значение не может быть напрямую создано объектами Leaf.
Например, если вы представляете математическое выражение, например (1 + 2) * (3 + 4), с помощью шаблона Composite, значение всего выражения можно вычислить только в объекте Facade. В этом случае у вас есть только возможность поместить туда инструкцию возврата.
Вместо этого рассмотрим случай, когда должен быть выбран Лист, и он сам способен произвести результат; Например, вы выбираете, на какой URL перенаправлять пользователя, и каждый Лист генерирует свой собственный, в то время как остальная часть графика выбирает Лист для запроса. Здесь возможна передача Целевого объекта в Лист.
Размеры: модульное тестирование
Во втором сценарии выше, на модульное тестирование фасада и промежуточного объекта влияют. В случае операторов return у нас есть заглушка и утверждение:
Leaf leaf = mock(Leaf.class); Intermediate intermediate = new IntermediateA(leaf); allow(leaf.doSomething()).andReturnValue(23); assertEquals(23, intermediate.doSomething(input, ...);
Делегация тестирования многословна (простите мои ржавые навыки Mockito). Вместо этого рассмотрим использование целевого объекта:
Target target = mock(Target.class); Leaf leaf = mock(Leaf.class); Intermediate intermediate = new IntermediateA(leaf); intermediate.doSomething(input, ...); verify(leaf).doSomething(target);
и мы можем просто проверить, что цель пройдена.
Размеры: гибкость результата
Более того, эта простота модульного тестирования сохраняется даже для обслуживания, так как вы можете изменить интерфейс Target, не обращая внимания на объекты Facade и Intermediate. Вы можете рефакторинг от:
interface Target { public void accept(String result); }
чтобы:
interface Target { public void accept(String result, int anotherField); }
изменяя только объекты Leaf. Вы можете достичь этого в решении на основе возврата, введя дополнительный объект Value с именем Result, и оберните туда строку String, чтобы позже можно было добавить другие поля без вмешательства в типы возврата Facade и Intermediate.
Размеры: результат на всех
В некоторых случаях вы можете не получить результат. В основанном на возврате решении это требует нулевого значения или нулевого объекта:
result = facade.doSomething(...); if (result != null) { // use it }
в то время как это легче с объектом Target:
class Leaf1 { public void soSomething(Target target, ...) { // call target.accept(), or do not call it } }
и эта форма также масштабируется до нескольких результатов одного типа:
public void doSomething(Target target, ...) { target.accept(23); target.accept(42); }
Выводы
Мы видели, что разные контексты требуют разных форм кода для соответствия требованиям; более того, изменения, которые мы хотим поддержать, лучше относятся к одному из решений относительно другого. Разработка означает поиск соответствующей формы вашего кода, а не привязку к той, которую вы знаете лучше: как вы видите в примере с несколькими результатами, разрушительные изменения для одной формы очень легко сделать в другом дизайне. Не выбирай вслепую …