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


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
}

але це вимагає перебудови класу щоразу, коли ми отримуємо нового замовника. Який кращий спосіб?

[Редагувати] "Дублікат" статті зовсім інший. Я не запитую, як уникнути заяви переключення, я прошу сучасний дизайн, який найкраще стосується даного випадку - який я міг би вирішити за допомогою перемикача, якби хотів написати код динозавра. Наведені приклади є загальними, а не корисними, оскільки вони, по суті, говорять "Ей, комутатор працює досить добре в деяких випадках, а не в інших".


[Редагувати] Я вирішив перейти з найкращим рейтингом (створити окремий клас "Клієнт" для кожного клієнта, який реалізує стандартний інтерфейс) з наступних причин:

  1. Послідовність: я можу створити інтерфейс, який забезпечує всі класу клієнтів отримувати та повертати однаковий результат, навіть якщо він створений іншим розробником

  2. Ремонтопридатність: весь код написаний однією і тією ж мовою (Java), тому більше нікому не потрібно вивчати окрему мову кодування, щоб підтримувати те, що повинно бути просто мертвою функцією.

  3. Повторне використання: Якщо в коді виникає аналогічна проблема, я можу повторно використовувати клас клієнтів, щоб мати будь-яку кількість методів для реалізації "користувацької" логіки.

  4. Ознайомлення: Я вже знаю, як це зробити, тому можу швидко зробити це та перейти до інших, більш гострих питань.

Недоліки:

  1. Кожен новий клієнт вимагає компіляції нового класу клієнтів, що може додати певної складності в тому, як ми компілюємо та розгортаємо зміни.

  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.total - але не, якщо Payment.id починається з" R "". Такого роду деталізація. У кожного замовника є свої правила.
Андрій


Намагалися чи ви що - щось на зразок цього . Встановлення різних формул для кожного клієнта.
Лаїв

1
Запропоновані плагіни з різних відповідей працюватимуть лише у тому випадку, якщо у вас є продукт, призначений для клієнта. Якщо у вас є серверне рішення, де ви динамічно перевіряєте ідентифікатор клієнта, воно не працюватиме. То яке ваше оточення?
qwerty_so

Відповіді:


14

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

На думку мені приходять два варіанти.

Варіант 1: Зробіть свій клас абстрактним класом, де метод, який різниться між клієнтами, є абстрактним методом. Потім створіть підклас для кожного клієнта.

Варіант 2: Створіть Customerклас або ICustomerінтерфейс, що містить всю логіку, залежну від клієнта. Замість того, щоб ваш клас обробки платежів прийняв ідентифікатор клієнта, прийняти його Customerабо ICustomerоб'єкт. Щоразу, коли потрібно зробити щось залежне від клієнта, він викликає відповідний метод.


Клас замовника - не погана ідея. Для кожного Замовника повинен бути якийсь спеціальний об'єкт, але мені не було зрозуміло, чи це повинен бути запис БД, файл властивостей (якогось типу) чи інший клас.
Андрій

10

Ви можете розглянути можливість написання спеціальних розрахунків як "плагінів" для вашої програми. Потім ви скористаєтеся конфігураційним файлом, щоб повідомити програмі, який плагін для розрахунку слід використовувати для якого замовника. Таким чином, ваше основне додаток не потрібно буде перекомпілювати для кожного нового клієнта - йому потрібно лише прочитати (або перечитати) файл конфігурації та завантажити нові плагіни.


Дякую. Як би плагін працював у середовищі Java? Наприклад, я хочу написати формулу "результат = якщо (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue", збережіть це як властивість для клієнта та вставте його у відповідному методі?
Андрій

@Andrew, Один із способів роботи плагіна - це використання відображення для завантаження в клас по імені. Конфігураційний файл перелічить назви класів для клієнтів. Звичайно, вам доведеться певним чином визначити, який плагін є для якого клієнта (наприклад, за його іменем або якимись десь збереженими метаданими).
Кет

@Kat Дякую, якщо ти прочитаєш моє відредаговане запитання, саме так я це роблю. Недолік полягає в тому, що розробник повинен створити новий клас, який обмежує масштабованість - я вважаю за краще, щоб людина, яка працює з підтримкою, могла просто відредагувати файл властивостей, але я думаю, що тут занадто багато складності.
Андрій

@Andrew: Можливо, ви зможете записати свої формули у файл властивостей, але тоді вам знадобиться якийсь синтаксичний аналізатор виразів. І ви можете одного дня зіткнутися з формулою, яка є надто складною для (легко) запису як однорядковий вираз у файл властивостей. Якщо ви дійсно хочете, щоб непрограмісти могли працювати над ними, вам знадобиться якийсь зручний редактор виразів, для якого вони можуть генерувати код для вашої програми. Це можна зробити (я це бачив), але це не банально.
FrustratedWithFormsDesigner

4

Я б пішов з набором правил для опису обчислень. Це може бути проведено в будь-якому магазині та збережено динамічно.

В якості альтернативи розглянемо це:

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

Де customerOpsIndexобчислюється правильний індекс операції (ви знаєте, який клієнт потребує лікування).


Якби я міг створити комплексний набір правил, я б. Але кожен новий клієнт може мати свій власний набір правил. Також я вважаю за краще, щоб вся справа містилася в коді, якщо для цього є модель дизайну. Мій приклад переключення {} робить це досить непогано, але це не дуже гнучко.
Андрій

Ви можете зберігати операції для виклику клієнта в масиві та використовувати ідентифікатор клієнта для його індексації.
qwerty_so

Це виглядає більш перспективно. :)
Андрій

3

Щось подібне:

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

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();
    }
}

2

Здається, що у вас від 1 до 1 відображення від клієнтів до користувальницького коду, і тому, що ви використовуєте компільовану мову, вам доведеться відновлювати систему кожного разу, коли ви отримуєте нового клієнта

Спробуйте вбудовану мову сценарію.

Наприклад, якщо ваша система знаходиться на Java, ви можете вбудувати JRuby, а потім для кожного клієнта зберегти відповідний фрагмент коду Ruby. В ідеалі десь під контролем версій, або в тому самому, або в окремому git repo. А потім оцініть цей фрагмент у контексті вашої програми 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, щоб визначити поведінку клієнта кожного опонента в грі.

З іншого боку, якщо у вас є багато-1 зіставлення від клієнтів до користувацького коду, просто використовуйте шаблон "Стратегія", як уже було запропоновано.

Якщо відображення не базується на створенні ідентифікатора користувача, додайте matchфункцію до кожного об'єкта стратегії та зробіть упорядкований вибір, яку стратегію використовувати.

Ось псевдо-код

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)

2
Я б уникнув такого підходу. Тенденція полягає в тому, щоб помістити всі фрагменти сценарію в db, і тоді ви втратите контроль над джерелами, версію та тестування.
Еван

Справедливо. Їх найкраще зберігати в окремому git repo або де завгодно. Я оновив свою відповідь, сказавши, що фрагменти в ідеалі повинні знаходитись під контролем версій.
акун

Чи можуть вони зберігатися у чомусь на зразок файлу властивостей? Я намагаюся уникати фіксованих властивостей Клієнта у кількох місцях, інакше легко перетворитись на незрозумілі спагетті.
Андрій

2

Я буду плавати проти течії.

Я б спробував реалізувати власну мову вираження з ANTLR .

Поки всі відповіді сильно базуються на налаштуванні коду. Реалізація конкретних занять для кожного замовника здається мені, що в якийсь момент майбутнього не вдасться масштабувати масштаб. Технічне обслуговування буде дорогим і болісним.

Отже, з Antlr ідея полягає у визначенні власної мови. Щоб ви могли дозволити користувачам (або розробникам) писати ділові правила такою мовою.

Приклад вашого коментаря:

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

За допомогою EL ви повинні мати можливість викладати вироки, такі як:

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

Потім...

зберегти це як властивість для клієнта та вставити його у відповідний метод?

Ви можете. Це рядок, ви можете зберегти його як властивість або атрибут.

Я не брешу. Це досить складно і важко. Ще складніше, якщо правила бізнесу також є складними.

Ось декілька питань, які можуть вас зацікавити:


Примітка: ANTLR також генерує код для Python та Javascript. Це може допомогти написати докази концепції, не надто багато накладних витрат.

Якщо вам здається, що Antlr занадто важкий, ви можете спробувати з libs, як Expr4J, JEval, Parsii. Це твори з більш високим рівнем абстракції.


Це гарна ідея, але головний біль від технічного обслуговування для наступного хлопця. Але я не збираюся відкидати будь-яку ідею, поки не оціню і зважую всі змінні.
Андрій

1
Чим більше варіантів, тим краще. Я просто хотів дати ще одне
Laiv

Ви не заперечуєте, що це правильний підхід, просто подивіться на веб-сферу чи інші системи корпоративних систем, які "кінцеві користувачі можуть налаштувати". Але, як і відповідь @akuhn, зворотний бік полягає в тому, що зараз ви програмуєте на двох мовах і втрачаєте версію / контроль джерела / контроль змін
Ewan

@Ewan вибачте, боюся, що я не зрозумів ваших аргументів. Дескриптори мови та автогенеровані класи можуть бути частиною проекту чи ні. Але в будь-якому випадку я не бачу, чому я можу втратити версію / управління вихідним кодом. З іншого боку, може здатися, що є 2 різні мови, але це тимчасово. Як тільки мова виконана, ви встановлюєте лише рядки. Як і вирази Крона. Зрештою, можна менше турбуватися.
Лаїв

"Якщо PaymentID startWith 'R', то (PaymentPercentage / PaymentTotal) else paymentOther", де ви зберігаєте це? яка версія? якою мовою написано? які одиничні тести ви отримали для цього?
Еван

1

Ви можете принаймні екстерналізувати алгоритм, щоб клас клієнта не потребував змін, коли додається новий клієнт, використовуючи шаблон дизайну, який називається Strategy Pattern (це в Gang of Four).

З фрагменту, який ви надали, можна сперечатися, чи не буде стратегія стратегії менш обґрунтованою або більш рентабельною, але це принаймні усуне знання класу Клієнта про те, що потрібно зробити (і усуне ваш випадок перемикання).

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

Класу Клієнтів потрібно лише попросити Фабрику про правильну стратегію, а потім зателефонувати.

Це дуже короткий псевдо 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 у кожному класі реалізації. Або ви можете анонімний клас реалізувати методи для різних клієнтів

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.