Шаблон стратегии с различными параметрами

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

Например:

interface ClerkResolver {
    String resolveClerk(String department);
}

class DefaultClerkResolver implements ClerkResolver {

    public String resolveClerk(String department) {
        // some stuff
    }
}

class CountryClerkResolver implements ClerkResolver {

    public String resolveClerk(String department) {
        // I do not need the department name here. What I need is the country.
    }

}

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

class StrategyParameter {

   private String department;
   private String country;

   public String getDepartment() ...
}

interface ClerkResolver {
    String resolveClerk(StrategyParameter strategyParameter);
}

Но, честно говоря, я не удовлетворен этим решением, потому что мне приходится менять класс параметров каждый раз, когда стратегия нуждается в новом/другом аргументе. А во-вторых, вызывающая сторона стратегии должна установить все параметры, потому что он не знает, какая стратегия разрешит клерка, поэтому он должен предоставить все параметры (но это не так уж плохо).

Опять же, для меня это звучит как проблема дизайна в моем коде, но я не могу найти лучшего решения.

--- EDIT

Основная проблема с этим решением заключается в создании задачи. Служба задач выглядит так:

class TaskService {

    private List<ClerkResolver> clerkResolvers;

    Task createTask(StrategyParamter ...) {

        // some stuff

       for(ClerkResolver clerkResolver : clerkResolvers) {
          String clerk = clerkResolver.resolveClerk(StrategyParameter...)
          ...
       }

       // some other stuff
    }

}

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

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

interface ClerkResolver {
    String resolveClerk(StrategyParameter strategyParameter);
    boolean canHandle(StrategyParameter strategyParameter);
}

Рискуя повторить меня, это решение для меня не подходит.

Итак, если у кого-то есть лучшее решение для этой проблемы, я был бы признателен за ее заслуги.

Спасибо за ваши комментарии!

Ответы

Ответ 1

Я думаю, что есть некоторая путаница в том, что на самом деле задача. По моему мнению, задача - это что-то, что делает клерк. Таким образом, вы можете сами создать задачу, не зная о клерке.

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

interface ClerkResolver {
    String resolveClerk(Task task);
}

Для реализации такого типа распознавателя clerk вы можете использовать шаблон стратегии, основанный на фактическом типе задачи, например.

Ответ 2

Мне очень понравилось предложение SpaceTrucker, что иногда проблемы решаются путем перемещения абстракции на другой уровень:)

Но если ваш оригинальный дизайн имеет больше смысла (о котором вы только можете сказать, исходя из вашего ощущения спецификации), то IMHO можно либо: 1) Держите свой подход "загружать все в StrategyParameter" 2) Или переложите эту ответственность на Стратегию

Для варианта (2), я предполагаю, что существует некоторая общая организация (account? customer?), из которой можно вывести отдел/страну. Затем у вас есть "CountryClerkResolver.resolveClerk(String accountId)", который будет искать страну.

ИМХО оба (1), (2) являются законными, в зависимости от контекста. Иногда (1) работает для меня, потому что все параметры (отдел + страна) дешевы для предварительной загрузки. Иногда мне даже удается заменить синтетический "StrategyParameter" на бизнес-интуитивно понятный объект (например, учетную запись). Иногда (2) работает лучше для меня, например. если "отдел" и "страна" требовали отдельных и дорогостоящих поисков. Это особенно заметно при наличии сложных параметров. если стратегия выбирает клерков на основе их оценок в обзорах удовлетворенности клиентов, то сложная структура, которую нельзя загружать для более простых стратегий.

Ответ 3

Давайте начнем с предположения, что ваш код основан на простых блоках if-else-if.

В таком случае вам все равно придется иметь все необходимые входные данные. Об этом не обойти.

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

Просто наличие этого дизайна недостаточно хорош, потому что вам все равно нужно иметь блок if-else-if.

На этом этапе вы можете посмотреть следующие изменения дизайна:

  • Используйте шаблон factory, чтобы загрузить все доступные стратегии из этой системы. Это может быть основано на метаинформации, например, Service Loader, который доступен в JDK.

  • Определите стратегию, с помощью которой вы можете запросить доступные реализации, чтобы узнать, могут ли они обрабатывать заданный набор параметров ввода. Это может быть так же просто, как canYouResolve (input)!= Null. Делая это, мы переходим от блока if-else-if к циклу for-each.

  • В вашем случае у вас есть реализация по умолчанию. Итак, скажем, что реализация по умолчанию является частью вашего модуля, а другие стратегии поступают из других банок (которые загружаются через ServiceLoader из пункта 1).

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

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

Теперь, придя к входным параметрам, могут ли эти параметры быть получены из некоторого входного объекта? Если да, то почему бы не отправить этот объект ввода самому распознавателю.

Примечание. Это очень похоже на то, как работает JavaEE ELResolver. В этом случае код маркирует EL как разрешенный, тем самым сообщая корневому классу, что разрешение завершено.

Примечание. Если вы считаете, что загрузчик услуг слишком тяжелый, посмотрите на поиск всех файлов META-INF/some-file-that-you-like, чтобы определить распознаватели, доступные в системе.

По собственному опыту, в большинстве случаев, вы в конечном итоге пишете код, который смешивает шаблоны для достижения подходящего варианта.

Надеюсь, это поможет вашему сценарию.

Ответ 4

Поскольку Java статически типизирована, одним из хороших способов имитации динамических объектов является использование карты. Я бы сделал это для передачи динамических параметров моим резольверам:

class StrategyParameter extends Map {} 
// Map could be used directly, but this make the code more readable

Затем мой шаблон стратегии будет выглядеть следующим образом: интерфейс ClerkResolver {   String resolveClerk (StrategyParameter strategyParameter); }

class DefaultClerkResolver implements ClerkResolver {

    public String resolveClerk(StrategyParameter strategyParameter) {
        // strategyParameter.get("department");
    }
}

class CountryClerkResolver implements ClerkResolver {

    public String resolveClerk(StrategyParameter strategyParameter) {
        // strategyParameter.get("country");
    }

}