Właściwy projekt dla klasy z jedną metodą, która może być różna dla różnych klientów

12

Mam klasę używaną do przetwarzania płatności od klientów. Wszystkie metody tej klasy oprócz jednej są takie same dla każdego klienta, z wyjątkiem jednej, która oblicza (na przykład), ile jest winien użytkownik klienta. Może się to znacznie różnić w zależności od klienta i nie ma łatwego sposobu na uchwycenie logiki obliczeń w pliku właściwości, ponieważ może istnieć dowolna liczba czynników niestandardowych.

Mógłbym napisać brzydki kod, który przełącza się na podstawie ID klienta:

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

ale wymaga to przebudowania klasy za każdym razem, gdy pozyskujemy nowego klienta. Jaki jest lepszy sposób?

[Edytuj] „Duplikat” artykułu jest zupełnie inny. Nie pytam, jak uniknąć instrukcji switch, proszę o nowoczesny projekt, który najlepiej pasuje do tego przypadku - który mógłbym rozwiązać za pomocą instrukcji switch, gdybym chciał napisać kod dinozaura. Podane tam przykłady są ogólne i nie są pomocne, ponieważ w zasadzie mówią „Hej, przełącznik działa całkiem dobrze w niektórych przypadkach, a nie w innych”.


[Edytuj] Zdecydowałem się na najwyższą odpowiedź (utwórz osobną klasę „Klient” dla każdego klienta, który implementuje standardowy interfejs) z następujących powodów:

  1. Spójność: mogę utworzyć interfejs, który zapewnia, że ​​wszystkie klasy klientów odbierają i zwracają takie same dane wyjściowe, nawet jeśli zostały utworzone przez innego programistę

  2. Utrzymywalność: Cały kod jest napisany w tym samym języku (Java), więc nie ma potrzeby, aby ktokolwiek uczył się oddzielnego języka kodowania, aby zachować coś, co powinno być śmiertelnie proste.

  3. Ponowne użycie: w przypadku pojawienia się podobnego problemu w kodzie, mogę ponownie użyć klasy Customer do przechowywania dowolnej liczby metod implementacji „niestandardowej” logiki.

  4. Znajomość: już wiem, jak to zrobić, więc mogę to zrobić szybko i przejść do innych, bardziej palących problemów.

Wady:

  1. Każdy nowy klient wymaga kompilacji nowej klasy Customer, co może nieco komplikować sposób, w jaki kompilujemy i wdrażamy zmiany.

  2. Każdy nowy klient musi zostać dodany przez programistę - osoba obsługująca nie może po prostu dodać logiki do czegoś takiego jak plik właściwości. Nie jest to idealne ... ale wtedy nie byłem również pewien, w jaki sposób osoba z działu pomocy technicznej byłaby w stanie napisać niezbędną logikę biznesową, szczególnie jeśli jest ona złożona z wieloma wyjątkami (co jest prawdopodobne).

  3. Nie będzie dobrze skalować, jeśli dodamy wielu nowych klientów. Nie jest to oczekiwane, ale jeśli tak się stanie, będziemy musieli przemyśleć wiele innych części kodu, jak również ten.

Dla zainteresowanych możesz użyć Java Reflection, aby wywołać klasę według nazwy:

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);
Andrzej
źródło
1
Może powinieneś rozwinąć rodzaj obliczeń. Czy to tylko obliczany rabat, czy inny przepływ pracy?
qwerty_so
@ThomasKilian To tylko przykład. Wyobraź sobie obiekt Payment, a obliczenia mogą być takie jak „pomnożenie procentu Payment.peryment przez Payment.total - ale nie, jeśli Payment.id zaczyna się na„ R ”. Ten rodzaj szczegółowości. Każdy klient ma swoje własne zasady.
Andrew
Czy próbowałeś coś jak ten . Ustawianie różnych formuł dla każdego klienta.
Laiv
1
Sugerowane wtyczki z różnych odpowiedzi będą działać tylko wtedy, gdy masz produkt dedykowany dla klienta. Jeśli masz rozwiązanie serwerowe, w którym dynamicznie sprawdzasz identyfikator klienta, nie zadziała. Jakie jest twoje środowisko?
qwerty_so

Odpowiedzi:

14

Mam klasę używaną do przetwarzania płatności od klientów. Wszystkie metody tej klasy oprócz jednej są takie same dla każdego klienta, z wyjątkiem jednej, która oblicza (na przykład), ile jest winien użytkownik klienta.

Przychodzą mi na myśl dwie opcje.

Opcja 1: uczyń swoją klasę klasą abstrakcyjną, a metoda różniąca się między klientami jest metodą abstrakcyjną. Następnie utwórz podklasę dla każdego klienta.

Opcja 2: Utwórz Customerklasę lub ICustomerinterfejs zawierający całą logikę zależną od klienta. Zamiast akceptować identyfikator klienta w klasie przetwarzania płatności, poproś o akceptację obiektu Customerlub ICustomerobiektu. Ilekroć musi zrobić coś zależnego od klienta, wywołuje odpowiednią metodę.

Tanner Swett
źródło
Klasa klienta nie jest złym pomysłem. Będzie musiał istnieć jakiś niestandardowy obiekt dla każdego klienta, ale nie byłem pewien, czy powinien to być rekord DB, plik właściwości (pewnego rodzaju), czy inna klasa.
Andrew
10

Może warto zajrzeć do pisania niestandardowych obliczeń jako „wtyczek” do aplikacji. Następnie użyjesz pliku konfiguracyjnego, aby poinformować program, która wtyczka obliczeniowa powinna być używana dla danego klienta. W ten sposób twoja główna aplikacja nie będzie musiała być ponownie kompilowana dla każdego nowego klienta - wystarczy tylko odczytać (lub ponownie odczytać) plik konfiguracyjny i załadować nowe wtyczki.

FrustratedWithFormsDesigner
źródło
Dzięki. Jak działałaby wtyczka w środowisku Java? Na przykład chcę napisać formułę „wynik = jeśli (Payment.id.startsWith („ R ”))? w odpowiedniej metodzie?
Andrew
@Andrew, Jednym ze sposobów działania wtyczki jest użycie odbicia do załadowania klasy według nazwy. Plik konfiguracyjny zawierałby nazwy klas dla klientów. Oczywiście trzeba mieć sposób na określenie, która wtyczka jest przeznaczona dla klienta (np. Według nazwy lub przechowywanych gdzieś metadanych).
Kat
@Kat Dziękuję, jeśli przeczytałeś moje edytowane pytanie, tak właśnie to robię. Wadą jest to, że programista musi stworzyć nową klasę, która ogranicza skalowalność - wolałbym, aby osoba obsługująca mogła po prostu edytować plik właściwości, ale myślę, że jest zbyt złożona.
Andrew
@Andrew: Być może będziesz mógł zapisać swoje formuły w pliku właściwości, ale wtedy potrzebujesz jakiegoś parsera wyrażeń. I pewnego dnia możesz natknąć się na formułę, która jest zbyt złożona, aby (łatwo) zapisać jako wyrażenie jednowierszowe w pliku właściwości. Jeśli naprawdę chcesz, aby nieprogramiści mogli nad nimi pracować, potrzebujesz pewnego rodzaju przyjaznego dla użytkownika edytora wyrażeń, który wygeneruje kod dla twojej aplikacji. Można to zrobić (widziałem to), ale nie jest to banalne.
FrustratedWithFormsDesigner
4

Wybrałbym zestaw reguł opisujących obliczenia. Można to przechowywać w dowolnym magazynie trwałości i modyfikować dynamicznie.

Alternatywnie rozważ to:

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

W przypadku customerOpsIndexobliczenia właściwego wskaźnika operacji (wiesz, który klient potrzebuje odpowiedniego leczenia).

qwerty_so
źródło
Gdybym mógł stworzyć kompleksowy zestaw reguł, zrobiłbym to. Ale każdy nowy klient może mieć własny zestaw reguł. Wolałbym również, aby cała rzecz była zawarta w kodzie, jeśli istnieje wzorzec projektowy, aby to zrobić. Mój przykład przełącznika {} robi to całkiem nieźle, ale nie jest zbyt elastyczny.
Andrew
Można przechowywać operacje, które mają zostać wywołane dla klienta w tablicy, i użyć identyfikatora klienta, aby go zindeksować.
qwerty_so,
To wygląda bardziej obiecująco. :)
Andrew
3

Coś jak poniżej:

zauważ, że nadal masz instrukcję switch w repozytorium. Jednak nie da się tego obejść, w pewnym momencie musisz zmapować identyfikator klienta na wymaganą logikę. Możesz być sprytny i przenieść go do pliku konfiguracyjnego, umieścić mapowanie w słowniku lub dynamicznie ładować zestawy logiczne, ale zasadniczo sprowadza się to do przełączenia.

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
źródło
2

Wygląda na to, że masz mapowanie 1: 1 od klientów do niestandardowego kodu, a ponieważ używasz skompilowanego języka, musisz przebudowywać system za każdym razem, gdy pozyskujesz nowego klienta

Wypróbuj wbudowany język skryptowy.

Na przykład, jeśli twój system jest w Javie, możesz osadzić JRuby, a następnie dla każdego klienta przechowywać odpowiedni fragment kodu Ruby. Idealnie gdzieś pod kontrolą wersji, w tym samym lub w osobnym repozytorium git. Następnie oceń ten fragment w kontekście aplikacji Java. JRuby może wywoływać dowolne funkcje Java i uzyskiwać dostęp do dowolnego obiektu 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!";
    }

}

Jest to bardzo powszechny wzór. Na przykład wiele gier komputerowych jest napisanych w C ++, ale używają wbudowanych skryptów Lua, aby zdefiniować zachowanie klienta każdego przeciwnika w grze.

Z drugiej strony, jeśli masz mapowanie wielu klientów do niestandardowego kodu, po prostu użyj wzorca „Strategia”, jak już sugerowano.

Jeśli mapowanie nie jest oparte na marce ID użytkownika, dodaj matchfunkcję do każdego obiektu strategii i dokonaj uporządkowanego wyboru strategii, której chcesz użyć.

Oto pseudo kod

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)
akuhn
źródło
2
Unikałbym tego podejścia. Tendencja polega na umieszczaniu wszystkich fragmentów skryptu w bazie danych, a następnie tracisz kontrolę nad źródłami, wersjonowanie i testowanie.
Ewan
Słusznie. Najlepiej przechowują je w osobnym repozytorium git lub gdziekolwiek. Zaktualizowałem moją odpowiedź, aby powiedzieć, że fragmenty powinny idealnie podlegać kontroli wersji.
akuhn
Czy mogą być przechowywane w czymś w rodzaju pliku właściwości? Staram się unikać posiadania ustalonych właściwości klienta w więcej niż jednej lokalizacji, w przeciwnym razie łatwo przekształcić się w niemożliwe do utrzymania spaghetti.
Andrew
2

Idę pływać pod prąd.

Spróbowałbym zaimplementować własny język wyrażeń za pomocą ANTLR .

Jak dotąd wszystkie odpowiedzi są silnie oparte na dostosowaniu kodu. Wdrożenie konkretnych klas dla każdego klienta wydaje mi się, że w pewnym momencie w przyszłości nie będzie dobrze skalować. Konserwacja będzie kosztowna i bolesna.

Tak więc w Antlr chodzi o zdefiniowanie własnego języka. Możesz pozwolić użytkownikom (lub programistom) pisać reguły biznesowe w takim języku.

Biorąc twój komentarz jako przykład:

Chcę napisać formułę „wynik = jeśli (Payment.id.startsWith („ R ”))?? Procent płatności * Payment.total: Payment.otherValue”

W przypadku EL powinno być możliwe określenie zdań takich jak:

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

Następnie...

zapisać to jako właściwość dla klienta i wstawić w odpowiednią metodę?

Mógłbyś. To ciąg znaków, który możesz zapisać jako właściwość lub atrybut.

Nie będę kłamać To dość skomplikowane i trudne. Jeszcze trudniej, jeśli reguły biznesowe są również skomplikowane.

Oto niektóre SO pytania, które mogą Cię zainteresować:


Uwaga: ANTLR generuje również kod dla Pythona i Javascript. To może pomóc napisać proof of concept bez nadmiernego obciążenia.

Jeśli uważasz, że Antlr jest zbyt trudny, możesz wypróbować biblioteki takie jak Expr4J, JEval, Parsii. Działa z wyższym poziomem abstrakcji.

Laiv
źródło
To dobry pomysł, ale problem z utrzymaniem głowy dla następnego faceta. Ale nie odrzucę żadnego pomysłu, dopóki nie ocenię i nie zważę wszystkich zmiennych.
Andrew
1
Im więcej opcji, tym lepiej. Chciałem tylko dać jeszcze jeden
Laiv
Nie można zaprzeczyć, że jest to prawidłowe podejście, wystarczy spojrzeć na sferę internetową lub inne gotowe systemy korporacyjne, które „użytkownicy końcowi mogą dostosować”. Ale jak odpowiedź @akuhn, wadą jest to, że teraz programujesz w dwóch językach i tracisz wersję. / kontrola źródła / kontrola zmiany
Ewan
@Ewan przepraszam, obawiam się, że nie zrozumiałem twoich argumentów. Opisy języków i automatycznie generowane klasy mogą być częścią projektu lub nie. Ale w każdym razie nie rozumiem, dlaczego mogłem stracić zarządzanie wersjami / kodem źródłowym. Z drugiej strony może się wydawać, że istnieją 2 różne języki, ale jest to tymczasowe. Po zakończeniu języka ustawiasz tylko łańcuchy. Jak wyrażenia Crona. Na dłuższą metę jest o wiele mniej powodów do zmartwień.
Laiv
„Jeśli paymentID zaczyna się od„ R ”, to (paymentPercentage / paymentTotal) else paymentOther”, gdzie to przechowujesz? jaka to wersja? w jakim języku jest napisane? jakie masz testy jednostkowe?
Ewan
1

Możesz przynajmniej uzewnętrznić algorytm, aby klasa klienta nie musiała się zmieniać po dodaniu nowego klienta za pomocą wzorca projektowego zwanego Wzorzec Strategiczny (znajduje się w Gangu Czterech).

Z fragmentu, który podałeś, można dyskutować, czy wzorzec strategii byłby mniej konserwowalny czy bardziej konserwowalny, ale przynajmniej wyeliminowałby wiedzę klasy klienta o tym, co należy zrobić (i wyeliminowałby przypadek zmiany przełącznika).

Obiekt StrategyFactory utworzyłby wskaźnik (lub odwołanie) StrategyIntf na podstawie CustomerID. Fabryka może zwrócić domyślną implementację dla klientów, którzy nie są specjalni.

Klasa klienta musi tylko poprosić fabrykę o prawidłową strategię, a następnie zadzwonić.

To jest bardzo krótki pseudo C ++, aby pokazać ci, co mam na myśli.

class Customer
{
public:

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


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

Wadą tego rozwiązania jest to, że dla każdego nowego klienta, który potrzebuje specjalnej logiki, należy utworzyć nową implementację CalculationsStrategyIntf i zaktualizować fabrykę, aby zwrócić ją odpowiedniemu klientowi (klientom). Wymaga to również kompilacji. Ale przynajmniej uniknąłbyś stale rosnącego kodu spaghetti w klasie klientów.

Matthew James Briggs
źródło
Tak, myślę, że ktoś wspomniał o podobnym wzorcu w innej odpowiedzi, dla każdego klienta, aby zawrzeć logikę w osobnej klasie, która implementuje interfejs klienta, a następnie wywołać tę klasę z klasy PaymentProcessor. To obiecujące, ale oznacza to, że za każdym razem, gdy pozyskujemy nowego Klienta, musimy napisać nową klasę - nie jest to naprawdę wielka sprawa, ale nie coś, co może zrobić osoba Wsparcia. Nadal jest to opcja.
Andrew
-1

Utwórz interfejs za pomocą jednej metody i użyj lamdas w każdej klasie implementacji. Lub możesz zrobić anonimową klasę, aby zaimplementować metody dla różnych klientów

Zubair A.
źródło