Укрощение классов «служебных функций»

15

В нашей кодовой базе Java я вижу следующий шаблон:

/**
 This is a stateless utility class
 that groups useful foo-related operations, often with side effects.
*/
public class FooUtil {
  public int foo(...) {...}
  public void bar(...) {...}
}

/**
 This class does applied foo-related things.
*/
class FooSomething {
  int DoBusinessWithFoo(FooUtil fooUtil, ...) {
    if (fooUtil.foo(...)) fooUtil.bar(...);
  }
}

Что меня беспокоит, так это то, что мне приходится проходить экземпляр FooUtilвезде, потому что тестирование.

  • Я не могу сделать FooUtilметоды статичными, потому что я не смогу имитировать их для тестирования обоих FooUtilклассов и их клиентов.
  • Я не могу создать экземпляр FooUtilв месте потребления с помощью new, опять же, потому что я не смогу имитировать его для тестирования.

Я полагаю, что лучше всего использовать инъекцию (и я это делаю), но это добавляет свой набор неприятностей. Кроме того, передача нескольких экземпляров util увеличивает размер списков параметров метода.

Есть ли способ справиться с этим лучше, что я не вижу?

Обновление: поскольку служебные классы не сохраняют состояния, я мог бы, вероятно, добавить статический одноэлементный INSTANCEчлен или статический getInstance()метод, сохранив при этом возможность обновлять базовое статическое поле класса для тестирования. Тоже не кажется супер-чистым.

9000
источник
4
И ваше предположение о том, что все зависимости должны быть проверены для эффективного выполнения модульного тестирования, неверно (хотя я все еще ищу вопрос, который обсуждает это).
Теластин
4
Почему эти методы не являются членами класса, данными которого они манипулируют?
Жюль
3
Иметь отдельный набор Junit, который вызывает статический fns, если FooUtility. Тогда вы можете использовать его без насмешек. Если это что-то, что вам нужно издеваться, то я подозреваю, что это не просто утилита, а какой-то ввод-вывод или бизнес-логика, и на самом деле он должен быть ссылкой на член класса и инициализироваться через конструктор, метод init или setter.
tgkprog
2
+1 за комментарий Жюля. Утилиты - это кодовый запах. В зависимости от семантики должно быть лучшее решение. Может быть, FooUtilметоды должны быть введены FooSomething, может быть, есть свойства, FooSomethingкоторые должны быть изменены / расширены, чтобы FooUtilметоды больше не нужны. Пожалуйста, дайте нам более конкретный пример того, как FooSomethingпомочь нам дать хороший ответ на эти вопросы.
Valenterry
13
@valenterry: единственная причина, по которой классы util - это запах кода, заключается в том, что Java не обеспечивает хороший способ иметь автономные функции. Есть очень веские причины иметь функции, которые не являются специфичными для классов.
Роберт Харви

Ответы:

16

Прежде всего позвольте мне сказать, что существуют разные подходы к программированию для разных задач. Для своей работы я предпочитаю сильный дизайн ОО для организации и обслуживания, и мой ответ отражает это. У функционального подхода будет совсем другой ответ.

Я склонен считать, что большинство служебных классов создаются, потому что объект, на котором должны быть методы, не существует. Это почти всегда происходит из одного из двух случаев: кто-то раздает коллекции или кто-то раздает бины (их часто называют POJO, но я считаю, что группу сеттеров и геттеров лучше всего описать как бин).

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

Мое предложение состоит в том, чтобы обернуть ваши коллекции (или bean-компоненты) любыми другими тесно связанными данными в новые классы и поместить код «полезности» в реальные объекты.

Вы можете расширить коллекцию и добавить туда методы, но таким образом вы потеряете контроль над состоянием вашего объекта - я предлагаю вам полностью его инкапсулировать и предоставлять только те методы бизнес-логики, которые необходимы (подумайте: «Не спрашивайте ваши объекты об их данных, дайте вашим объектам команды и дайте им действовать на основе их личных данных ", важного OO-арендатора, который, когда вы об этом думаете, требует подхода, который я предлагаю здесь).

Это «обертывание» полезно для любых объектов библиотеки общего назначения, которые содержат данные, но вы не можете добавить код к ним. Помещение кода вместе с данными, которыми он манипулирует, является еще одной важной концепцией в ОО-дизайне.

Есть много стилей кодирования, где эта концепция переноса была бы плохой идеей - кто хочет написать целый класс, просто реализуя одноразовый скрипт или тест? Но я думаю, что для ОО обязательна инкапсуляция любых базовых типов / коллекций, которые вы не можете добавить к себе в код.

Билл К
источник
Этот ответ такой потрясающий. У меня была эта проблема (очевидно), я прочитал этот ответ с некоторым сомнением, вернулся, посмотрел на мой код и спросил: «Какой объект отсутствует, который должен иметь эти методы». И вот, элегантное решение найдено и пробел в объектной модели заполнен.
GreenAsJade
@Deduplicator За 40 лет программирования я редко (никогда?) Видел случай, когда меня блокировали слишком много уровней косвенности (кроме случайной борьбы с моей IDE за переход к реализации, а не к интерфейсу), но я ' Мы часто (очень часто) видели случай, когда люди писали ужасный код, а не создавали явно необходимый класс. Хотя я полагаю, что слишком много косвенных указаний могло бы укусить некоторых людей, я бы ошибался из-за правильных принципов ОО, таких как каждый класс хорошо выполняет одно
Билл К
@BillK В общем, хорошая идея. Но без аварийного люка инкапсуляция действительно может помешать. И есть определенная прелесть в бесплатных алгоритмах, которые можно сопоставить с любым типом, который вы хотите, хотя по общему признанию в языках со строгим OO все, кроме OO, довольно неудобно.
Дедупликатор
@Deduplicator При написании «служебных» функций, которые должны быть написаны вне конкретных бизнес-требований, вы правы. Классы утилит, полные функций (таких как коллекции), просто неуклюжи в ОО, потому что они не связаны с данными. Это «Escape Hatch», который используют люди, которые не знакомы с ОО (также поставщики инструментов должны использовать их для очень общей функциональности). Мое предложение выше заключалось в том, чтобы обернуть эти хаки в классы, когда вы работаете над бизнес-логикой, чтобы вы могли заново связать свой код с вашими данными.
Билл К
1

Вы можете использовать шаблон поставщика и выполнение вашего FooSomethingс FooUtilProviderобъектом (может быть в конструкторе, только один раз).

FooUtilProviderбудет иметь только один метод: FooUtils get();. Затем класс будет использовать любой FooUtilsэкземпляр, который он предоставляет.

Теперь это один шаг от использования инфраструктуры внедрения зависимостей, когда вам нужно будет только дважды подключить DI cointainer: для производственного кода и для вашего набора тестов. Вы просто привязываете FooUtilsинтерфейс к любому RealFooUtilsили к MockedFooUtils, а остальное происходит автоматически. Каждый объект, от которого зависит, FooUtilsProviderполучает правильную версию FooUtils.

Конрад Моравский
источник