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:
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ę
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.
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.
Znajomość: już wiem, jak to zrobić, więc mogę to zrobić szybko i przejść do innych, bardziej palących problemów.
Wady:
Każdy nowy klient wymaga kompilacji nowej klasy Customer, co może nieco komplikować sposób, w jaki kompilujemy i wdrażamy zmiany.
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).
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);
źródło
Odpowiedzi:
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
Customer
klasę lubICustomer
interfejs zawierający całą logikę zależną od klienta. Zamiast akceptować identyfikator klienta w klasie przetwarzania płatności, poproś o akceptację obiektuCustomer
lubICustomer
obiektu. Ilekroć musi zrobić coś zależnego od klienta, wywołuje odpowiednią metodę.źródło
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.
źródło
Wybrałbym zestaw reguł opisujących obliczenia. Można to przechowywać w dowolnym magazynie trwałości i modyfikować dynamicznie.
Alternatywnie rozważ to:
W przypadku
customerOpsIndex
obliczenia właściwego wskaźnika operacji (wiesz, który klient potrzebuje odpowiedniego leczenia).źródło
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.
źródło
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.
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
match
funkcję do każdego obiektu strategii i dokonaj uporządkowanego wyboru strategii, której chcesz użyć.Oto pseudo kod
źródło
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:
W przypadku EL powinno być możliwe określenie zdań takich jak:
Następnie...
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.
źródło
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.
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.
źródło
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
źródło