Статьи

Выбор дизайна: возвращаемые значения и макеты

Предположим, у вас есть график взаимодействующих объектов, например, такой:

чтобы выполнить какое-либо действие, вы должны отправить сообщение в объект 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);
  }

Выводы

Мы видели, что разные контексты требуют разных форм кода для соответствия требованиям; более того, изменения, которые мы хотим поддержать, лучше относятся к одному из решений относительно другого. Разработка означает поиск соответствующей формы вашего кода, а не привязку к той, которую вы знаете лучше: как вы видите в примере с несколькими результатами, разрушительные изменения для одной формы очень легко сделать в другом дизайне. Не выбирай вслепую …