Правильный дизайн для класса с одним методом, который может варьироваться между клиентами

12

У меня есть класс, используемый для обработки платежей клиентов. Все методы этого класса, кроме одного, одинаковы для каждого клиента, за исключением одного, который вычисляет (например), сколько должен пользователь пользователя. Это может сильно варьироваться от клиента к клиенту, и нет простого способа записать логику вычислений в нечто вроде файла свойств, поскольку может быть любое количество пользовательских факторов.

Я мог бы написать некрасивый код, который переключается на основе customerID:

switch(customerID) {
 case 101:
  .. do calculations for customer 101
 case 102:
  .. do calculations for customer 102
 case 103:
  .. do calculations for customer 103
 etc
}

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

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


[Править] Я решил использовать самый лучший ответ (создать отдельный класс «Клиент» для каждого клиента, который реализует стандартный интерфейс) по следующим причинам:

  1. Согласованность: я могу создать интерфейс, который гарантирует, что все классы Customer получают и возвращают один и тот же вывод, даже если он создан другим разработчиком

  2. Поддержка: весь код написан на одном и том же языке (Java), поэтому больше нет необходимости изучать отдельный язык кодирования, чтобы поддерживать то, что должно быть очень простой функцией.

  3. Повторное использование: в случае возникновения подобной проблемы в коде я могу повторно использовать класс Customer для хранения любого количества методов для реализации «пользовательской» логики.

  4. Знакомство: я уже знаю, как это сделать, поэтому я могу сделать это быстро и перейти к другим, более актуальным вопросам.

Недостатки:

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

  2. Каждый новый клиент должен быть добавлен разработчиком - специалист по поддержке не может просто добавить логику в нечто вроде файла свойств. Это не идеально ... но тогда я также не был уверен, как специалист службы поддержки сможет выписать необходимую бизнес-логику, особенно если она сложна со многими исключениями (что вполне вероятно).

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

Для тех, кто заинтересован, вы можете использовать Java Reflection для вызова класса по имени:

Payment payment = getPaymentFromSomewhere();

try {
    String nameOfCustomClass = propertiesFile.get("customClassName");
    Class<?> cpp = Class.forName(nameOfCustomClass);
    CustomPaymentProcess pp = (CustomPaymentProcess) cpp.newInstance();

    payment = pp.processPayment(payment);
} catch (Exception e) {
    //handle the various exceptions
} 

doSomethingElseWithThePayment(payment);
Андрей
источник
1
Может быть, вы должны уточнить тип расчетов. Это просто скидка или другой рабочий процесс?
qwerty_so
@ThomasKilian Это просто пример. Представьте себе объект «Платеж», и вычисления могут выглядеть примерно так: «умножьте процент Payment.percentage на Payment.total, но не в том случае, если Payment.id начинается с« R »». Такая гранулярность. У каждого клиента свои правила.
Андрей
Пытались ли вы что - то вроде этого . Настройка различных формул для каждого клиента.
Laiv
1
Предложенные плагины из разных ответов будут работать только при наличии продукта для каждого клиента. Если у вас есть серверное решение, в котором вы динамически проверяете идентификатор клиента, оно не будет работать. Итак, какова ваша среда?
qwerty_so

Ответы:

14

У меня есть класс, используемый для обработки платежей клиентов. Все методы этого класса, кроме одного, одинаковы для каждого клиента, за исключением одного, который вычисляет (например), сколько должен пользователь пользователя.

Два варианта приходят на ум.

Вариант 1: Сделайте ваш класс абстрактным классом, где метод, который варьируется между клиентами, является абстрактным методом. Затем создайте подкласс для каждого клиента.

Вариант 2. Создайте Customerкласс или ICustomerинтерфейс, содержащий всю логику, зависящую от клиента. Вместо того, чтобы ваш класс обработки платежей принимал идентификатор клиента, пусть он принимает объект Customerили ICustomer. Всякий раз, когда ему нужно сделать что-то зависящее от клиента, он вызывает соответствующий метод.

Таннер Светт
источник
Класс Customer - неплохая идея. Должен быть какой-то особый объект для каждого Заказчика, но я не знал, должна ли это быть запись в БД, файл свойств (какого-то рода) или другой класс.
Андрей
10

Возможно, вы захотите написать собственные вычисления как «плагины» для своего приложения. Затем вы будете использовать файл конфигурации, чтобы сообщить программе, какой плагин расчета должен использоваться для какого клиента. Таким образом, ваше основное приложение не нужно будет перекомпилировать для каждого нового клиента - ему нужно только прочитать (или перечитать) файл конфигурации и загрузить новые плагины.

FrustratedWithFormsDesigner
источник
Благодарю. Как плагин будет работать в среде Java? Например, я хочу написать формулу "result = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue" сохранить это как свойство для клиента и вставить его в соответствующем методе?
Андрей
@ Андрей, Один из способов работы плагина - использовать отражение для загрузки класса по имени. В файле конфигурации будут перечислены имена классов для клиентов. Конечно, у вас должен быть какой-то способ определить, какой плагин для какого клиента (например, по его имени или метаданным, хранящимся где-то).
Kat
@Kat Спасибо, если ты читаешь мой отредактированный вопрос, на самом деле я так и делаю. Недостатком является то, что разработчик должен создать новый класс, который ограничивает масштабируемость - я бы предпочел, чтобы специалист по поддержке мог просто отредактировать файл свойств, но я думаю, что это слишком сложно.
Андрей
@Andrew: Возможно, вы сможете написать свои формулы в файле свойств, но тогда вам понадобится какой-то синтаксический анализатор выражений. И однажды вы можете столкнуться с формулой, которая слишком сложна, чтобы (легко) записать ее как однострочное выражение в файле свойств. Если вы действительно хотите, чтобы непрограммисты могли работать с ними, вам понадобится какой-то удобный для них редактор выражений, который будет генерировать код для вашего приложения. Это можно сделать (я видел это), но это не тривиально.
FrustratedWithFormsDesigner
4

Я бы пошел с набором правил для описания расчетов. Это может быть сохранено в любом постоянном хранилище и изменено динамически.

В качестве альтернативы рассмотрим это:

customerOps = [oper1, oper2, ..., operN]; // array with customer specific operations
index = customerOpsIndex(customer);
customerOps[index](parms);

Где customerOpsIndexрассчитывается правильный операционный индекс (вы знаете, какому клиенту нужно какое лечение).

qwerty_so
источник
Если бы я мог создать полный набор правил, я бы. Но у каждого нового клиента могут быть свои правила. Также я бы предпочел, чтобы все это содержалось в коде, если для этого есть шаблон проектирования. Мой пример switch {} делает это довольно хорошо, но он не очень гибкий.
Андрей
Вы можете сохранить операции, которые будут вызваны для клиента, в массиве и использовать идентификатор клиента для его индексации.
qwerty_so
Это выглядит более перспективным. :)
Андрей
3

Что-то вроде ниже:

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

public interface ICustomer
{
    int Calculate();
}
public class CustomerLogic101 : ICustomer
{
    public int Calculate() { return 101; }
}
public class CustomerLogic102 : ICustomer
{
    public int Calculate() { return 102; }
}

public class CustomerRepo
{
    public ICustomer GetCustomerById(
        string id)
    {
        var data;//get data from db
        if (data.logicType == "101")
        {
            return new CustomerLogic101();
        }
        if (data.logicType == "102")
        {
            return new CustomerLogic102();
        }
    }
}
public class Calculator
{
    public int CalculateCustomer(string custId)
    {
        CustomerRepo repo = new CustomerRepo();
        var cust = repo.GetCustomerById(custId);
        return cust.Calculate();
    }
}
Ewan
источник
2

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

Попробуйте встроенный язык сценариев.

Например, если ваша система работает на Java, вы можете встроить JRuby, а затем для каждого клиента сохранить соответствующий фрагмент кода Ruby. В идеале где-то под контролем версий, либо в том же, либо в отдельном git-репо. А затем оцените этот фрагмент в контексте вашего Java-приложения. JRuby может вызвать любую функцию Java и получить доступ к любому объекту Java.

package com.example;

import org.jruby.embed.LocalVariableBehavior;
import org.jruby.embed.ScriptingContainer;

public class Main {

    private ScriptingContainer ruby;

    public static void main(String[] args) {
        new Main().run();
    }

    public void run() {
        ruby = new ScriptingContainer(LocalVariableBehavior.PERSISTENT);
        // Assign the Java objects that you want to share
        ruby.put("main", this);
        // Execute a script (can be of any length, and taken from a file)
        Object result = ruby.runScriptlet("main.hello_world");
        // Use the result as if it were a Java object
        System.out.println(result);
    }

    public String getHelloWorld() {
        return "Hello, worlds!";
    }

}

Это очень распространенная модель. Например, многие компьютерные игры написаны на C ++, но используют встроенные сценарии Lua для определения поведения каждого оппонента в игре.

С другой стороны, если у вас есть многозначное соответствие между клиентами и пользовательским кодом, просто используйте шаблон «Стратегия», как уже предлагалось.

Если отображение не основано на идентификаторе пользователя make, добавьте matchфункцию к каждому объекту стратегии и сделайте упорядоченный выбор используемой стратегии.

Вот немного псевдокода

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)
akuhn
источник
2
Я бы избежал такого подхода. Тенденция состоит в том, чтобы поместить все эти фрагменты скриптов в базу данных, и тогда вы потеряете контроль версий, управление версиями и тестирование.
Эван
Справедливо. Лучше всего хранить их в отдельном git-репо или где угодно. Обновил мой ответ, сказав, что в идеале фрагменты должны находиться под контролем версий.
Akuhn
Могут ли они храниться в чем-то вроде файла свойств? Я стараюсь не использовать фиксированные свойства Customer в нескольких местах, в противном случае их легко превратить в неуправляемые спагетти.
Андрей
2

Я собираюсь плыть против течения.

Я бы попробовал реализовать свой собственный язык выражений с помощью ANTLR .

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

Итак, с Antlr идея состоит в том, чтобы определить свой собственный язык. Вы можете позволить пользователям (или разработчикам) писать бизнес-правила на таком языке.

Принимая ваш комментарий в качестве примера:

Я хочу написать формулу "result = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue"

С вашим EL, вы сможете сформулировать такие предложения, как:

If paymentID startWith 'R' then (paymentPercentage / paymentTotal) else paymentOther

Потом...

сохранить это как свойство для клиента и вставить его соответствующим способом?

Вы могли бы. Это строка, вы можете сохранить ее как свойство или атрибут.

Я не буду лгать. Это довольно сложно и сложно. Еще сложнее, если бизнес-правила тоже сложны.

Вот некоторые вопросы, которые могут вас заинтересовать:


Примечание: ANTLR генерирует код для Python и Javascript тоже. Это может помочь написать доказательства концепции без лишних затрат.

Если вы находите Antlr слишком сложным, вы можете попробовать с такими библиотеками, как Expr4J, JEval, Parsii. Эти работы с более высоким уровнем абстракции.

LAIV
источник
Это хорошая идея, но головная боль от обслуживания следующего парня в очереди. Но я не собираюсь отказываться от какой-либо идеи, пока не оценил и не взвесил все переменные.
Андрей
1
Чем больше вариантов, тем лучше. Я просто хотел дать еще один
Laiv
Нет никаких сомнений в том, что это правильный подход, просто посмотрите на веб-сферу или другие готовые корпоративные системы, которые «конечные пользователи могут настраивать». Но, как и ответ @akuhn, недостатком является то, что теперь вы программируете на 2 языках и теряете свои версии / контроль источника / контроль изменений
Ewan
@ Простите, боюсь, я не поняла ваши аргументы. Языковые дескрипторы и автоматически сгенерированные классы могут быть частью проекта или нет. Но в любом случае я не понимаю, почему я могу потерять управление версиями / исходным кодом. С другой стороны, может показаться, что есть 2 разных языка, но это временно. Как только язык готов, вы устанавливаете только строки. Как выражения Cron. В конце концов, есть куда меньше беспокоиться.
Laiv
«Если paymentID запускается с 'R', то (paymentPercentage / paymentTotal) else paymentOther", где вы храните это? какая это версия? на каком языке это написано? какие юнит-тесты у тебя есть для этого?
Эван
1

Вы можете, по крайней мере, экстернализовать алгоритм, чтобы класс Customer не нуждался в изменении при добавлении нового клиента, используя шаблон проектирования, называемый шаблоном стратегии (он находится в «банде четырех»).

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

Объект StrategyFactory создаст указатель StrategyIntf (или ссылку) на основе CustomerID. Фабрика может вернуть реализацию по умолчанию для клиентов, которые не являются особенными.

Класс Customer должен только спросить у Factory правильную стратегию и затем вызвать ее.

Это очень краткое псевдо-C ++, чтобы показать вам, что я имею в виду.

class Customer
{
public:

    void doCalculations()
    {
        CalculationsStrategyIntf& strategy = CalculationsStrategyFactory::instance().getStrategy(*this);
        strategy.doCalculations();
    }
};


class CalculationsStrategyIntf
{
public:
    virtual void doCalculations() = 0;
};

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

Мэтью Джеймс Бриггс
источник
Да, я думаю, что кто-то упомянул похожий шаблон в другом ответе, чтобы каждый клиент инкапсулировал логику в отдельный класс, реализующий интерфейс клиента, а затем вызвал этот класс из класса PaymentProcessor. Это многообещающе, но это означает, что каждый раз, когда мы привлекаем нового Клиента, мы должны написать новый класс - не очень большое дело, но не то, что может сделать сотрудник службы поддержки. Тем не менее это вариант.
Андрей
-1

Создайте интерфейс с одним методом и используйте lamdas в каждом классе реализации. Или вы можете анонимный класс для реализации методов для разных клиентов

Зубаир А.
источник