Richtiges Design für eine Klasse mit einer Methode, die von Kunde zu Kunde unterschiedlich sein kann

12

Ich habe eine Klasse, die zur Verarbeitung von Kundenzahlungen verwendet wird. Mit einer Ausnahme sind alle Methoden dieser Klasse für jeden Kunden gleich, mit Ausnahme einer, die (zum Beispiel) berechnet, wie viel der Benutzer des Kunden schuldet. Dies kann von Kunde zu Kunde sehr unterschiedlich sein und es gibt keine einfache Möglichkeit, die Logik der Berechnungen in so etwas wie einer Eigenschaftendatei zu erfassen, da es eine beliebige Anzahl benutzerdefinierter Faktoren geben kann.

Ich könnte hässlichen Code schreiben, der basierend auf der Kunden-ID wechselt:

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

Dies erfordert jedoch, dass die Klasse jedes Mal neu aufgebaut wird, wenn wir einen neuen Kunden erhalten. Was ist der bessere Weg?

[Bearbeiten] Der "doppelte" Artikel ist völlig anders. Ich frage nicht, wie ich eine switch-Anweisung vermeiden soll, sondern nach dem modernen Design, das für diesen Fall am besten geeignet ist - das ich mit einer switch-Anweisung lösen könnte, wenn ich Dinosaurier-Code schreiben möchte. Die dort bereitgestellten Beispiele sind allgemein gehalten und nicht hilfreich, da sie im Wesentlichen sagen: "Hey, der Schalter funktioniert in einigen Fällen ziemlich gut, in anderen nicht."


[Bearbeiten] Ich habe mich aus folgenden Gründen für die Antwort mit dem höchsten Rang entschieden (erstellen Sie eine separate "Kunden" -Klasse für jeden Kunden, der eine Standardschnittstelle implementiert):

  1. Konsistenz: Ich kann eine Schnittstelle erstellen, die sicherstellt, dass alle Kundenklassen die gleiche Ausgabe erhalten und zurückgeben, auch wenn sie von einem anderen Entwickler erstellt wurden

  2. Wartbarkeit: Der gesamte Code ist in der gleichen Sprache (Java) geschrieben, sodass niemand eine separate Programmiersprache erlernen muss, um das beizubehalten, was eine absolut einfache Funktion sein sollte.

  3. Wiederverwendung: Falls im Code ein ähnliches Problem auftritt, kann ich die Customer-Klasse erneut verwenden, um eine beliebige Anzahl von Methoden zum Implementieren der "benutzerdefinierten" Logik zu speichern.

  4. Vertrautheit: Ich weiß bereits, wie man das macht, damit ich es schnell erledigen und mich anderen, dringenderen Themen widmen kann.

Nachteile:

  1. Für jeden neuen Kunden ist eine Kompilierung der neuen Kundenklasse erforderlich, wodurch die Kompilierung und Bereitstellung von Änderungen möglicherweise etwas komplexer wird.

  2. Jeder neue Kunde muss von einem Entwickler hinzugefügt werden - ein Support-Mitarbeiter kann die Logik nicht einfach einer Eigenschaftendatei hinzufügen. Das ist nicht ideal ... aber dann war ich mir auch nicht sicher, wie ein Support-Mitarbeiter die erforderliche Geschäftslogik aufschreiben kann, insbesondere wenn sie (wie wahrscheinlich) mit vielen Ausnahmen komplex ist.

  3. Es wird nicht gut skalieren, wenn wir viele, viele neue Kunden hinzufügen. Dies ist nicht zu erwarten, aber wenn es passiert, müssen wir viele andere Teile des Codes sowie diesen einen überdenken.

Für die von Ihnen Interessierten können Sie Java Reflection verwenden, um eine Klasse nach Namen aufzurufen:

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);
Andrew
quelle
1
Vielleicht sollten Sie die Art der Berechnungen näher erläutern. Wird nur ein Rabatt berechnet oder handelt es sich um einen anderen Arbeitsablauf?
qwerty_so
@ ThomasKilian Es ist nur ein Beispiel. Stellen Sie sich ein Payment-Objekt vor und die Berechnungen können so etwas wie "Multiplizieren Sie den Payment.percentage mit der Payment.total - aber nicht, wenn Payment.id mit 'R' beginnt" sein. Diese Art von Granularität. Jeder Kunde hat seine eigenen Regeln.
Andrew
Haben Sie so etwas wie versucht diese . Festlegen unterschiedlicher Formeln für jeden Kunden.
Laiv
1
Die vorgeschlagenen Plug-Ins aus verschiedenen Antworten funktionieren nur, wenn Sie ein Produkt haben, das für jeden Kunden bestimmt ist. Wenn Sie eine Serverlösung haben, bei der Sie die Kunden-ID dynamisch überprüfen, funktioniert dies nicht. Was ist deine Umgebung?
Qwerty_so

Antworten:

14

Ich habe eine Klasse, die zur Verarbeitung von Kundenzahlungen verwendet wird. Mit einer Ausnahme sind alle Methoden dieser Klasse für jeden Kunden gleich, mit Ausnahme einer, die (zum Beispiel) berechnet, wie viel der Benutzer des Kunden schuldet.

Mir fallen zwei Möglichkeiten ein.

Option 1: Machen Sie Ihre Klasse zu einer abstrakten Klasse, wobei die Methode, die zwischen Kunden variiert, eine abstrakte Methode ist. Dann legen Sie für jeden Kunden eine Unterklasse an.

Option 2: Erstellen Sie eine CustomerKlasse oder eine ICustomerSchnittstelle, die die gesamte kundenabhängige Logik enthält. Anstatt Ihre Zahlungsverarbeitungsklasse eine Kunden-ID akzeptieren zu lassen, lassen Sie sie ein Customeroder ein ICustomerObjekt akzeptieren . Immer wenn es etwas kundenabhängiges tun muss, ruft es die entsprechende Methode auf.

Tanner Swett
quelle
Die Kundenklasse ist keine schlechte Idee. Es muss für jeden Kunden eine Art benutzerdefiniertes Objekt geben, aber mir war nicht klar, ob es sich um einen DB-Datensatz, eine Eigenschaftendatei (in irgendeiner Form) oder eine andere Klasse handeln soll.
Andrew
10

Möglicherweise möchten Sie die benutzerdefinierten Berechnungen als "Plug-Ins" für Ihre Anwendung schreiben. Anschließend teilen Sie Ihrem Programm anhand einer Konfigurationsdatei mit, welches Berechnungs-Plugin für welchen Kunden verwendet werden soll. Auf diese Weise muss Ihre Hauptanwendung nicht für jeden neuen Kunden neu kompiliert werden. Sie muss lediglich eine Konfigurationsdatei lesen (oder erneut lesen) und neue Plug-Ins laden.

FrustratedWithFormsDesigner
quelle
Vielen Dank. Wie würde ein Plug-In in einer Java-Umgebung funktionieren? Zum Beispiel möchte ich die Formel "result = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue" als Eigenschaft für den Kunden speichern und einfügen in der entsprechenden Methode?
Andrew
@ Andrew, Eine Möglichkeit, wie ein Plugin funktionieren kann, besteht darin, Reflection zu verwenden, um eine Klasse nach Namen zu laden. Die Konfigurationsdatei würde die Namen der Klassen für Kunden auflisten. Natürlich müssen Sie eine Möglichkeit haben, zu identifizieren, welches Plugin für welchen Kunden bestimmt ist (z. B. anhand seines Namens oder einiger Metadaten, die irgendwo gespeichert sind).
Kat
@Kat Danke, wenn du meine bearbeitete Frage liest, mache ich das tatsächlich so. Der Nachteil ist, dass ein Entwickler eine neue Klasse erstellen muss, die die Skalierbarkeit einschränkt. Ich würde es vorziehen, wenn eine Support-Person nur eine Eigenschaftendatei bearbeiten könnte, aber ich denke, dass die Komplexität zu groß ist.
Andrew
@ Andrew: Möglicherweise können Sie Ihre Formeln in eine Eigenschaftendatei schreiben, aber dann benötigen Sie eine Art Ausdrucksparser. Und eines Tages könnten Sie auf eine Formel stoßen, die zu komplex ist, um sie (leicht) als einzeiligen Ausdruck in eine Eigenschaftendatei zu schreiben. Wenn Sie wirklich möchten, dass Nicht-Programmierer daran arbeiten können, benötigen Sie einen benutzerfreundlichen Ausdruckseditor, der Code für Ihre Anwendung generiert. Dies kann getan werden (ich habe es gesehen), aber es ist nicht trivial.
FrustratedWithFormsDesigner
4

Ich würde mit einem Regelsatz gehen, um die Berechnungen zu beschreiben. Dies kann in einem beliebigen Persistenzspeicher gespeichert und dynamisch geändert werden.

Beachten Sie alternativ Folgendes:

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

Wo customerOpsIndexder richtige Operationsindex berechnet wurde (Sie wissen, welcher Kunde welche Behandlung benötigt).

qwerty_so
quelle
Wenn ich einen umfassenden Regelsatz erstellen könnte, würde ich. Aber jeder neue Kunde hat möglicherweise seine eigenen Regeln. Auch würde ich es vorziehen, wenn das Ganze im Code enthalten wäre, wenn es ein Entwurfsmuster dafür gibt. Mein switch {} Beispiel macht das ganz gut, aber es ist nicht sehr flexibel.
Andrew
Sie können die Vorgänge, die für einen Kunden aufgerufen werden sollen, in einem Array speichern und anhand der Kunden-ID indizieren.
Qwerty_so
Das sieht vielversprechender aus. :)
Andrew
3

So etwas wie das Folgende:

Beachten Sie, dass Sie immer noch die switch-Anweisung im Repo haben. Es gibt jedoch kein wirkliches Umgehen, irgendwann müssen Sie der erforderlichen Logik eine Kunden-ID zuordnen. Sie können clever werden und es in eine Konfigurationsdatei verschieben, das Mapping in ein Wörterbuch stellen oder dynamisch in die Logikbaugruppen laden, aber es läuft im Wesentlichen darauf hinaus, zu wechseln.

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
quelle
2

Klingt so, als hätten Sie eine 1: 1-Zuordnung von Kunden zu benutzerdefiniertem Code. Da Sie eine kompilierte Sprache verwenden, müssen Sie Ihr System jedes Mal neu erstellen, wenn Sie einen neuen Kunden gewinnen

Probieren Sie eine eingebettete Skriptsprache aus.

Wenn sich Ihr System beispielsweise in Java befindet, können Sie JRuby einbetten und dann für jeden Kunden einen entsprechenden Ruby-Codeausschnitt speichern. Idealerweise irgendwo unter Versionskontrolle, entweder im selben oder in einem separaten Git-Repo. Und werten Sie dieses Snippet dann im Kontext Ihrer Java-Anwendung aus. JRuby kann jede Java-Funktion aufrufen und auf jedes Java-Objekt zugreifen.

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!";
    }

}

Dies ist ein sehr verbreitetes Muster. Beispielsweise sind viele Computerspiele in C ++ geschrieben, verwenden jedoch eingebettete Lua-Skripte, um das Kundenverhalten jedes Gegners im Spiel zu definieren.

Wenn Sie dagegen eine 1: 1-Zuordnung von Kunden zu benutzerdefiniertem Code haben, verwenden Sie einfach das bereits vorgeschlagene "Strategie" -Muster.

Wenn die Zuordnung nicht auf der Benutzer-ID make basiert, fügen Sie matchjedem Strategieobjekt eine Funktion hinzu und treffen Sie eine geordnete Auswahl der zu verwendenden Strategie.

Hier ist ein Pseudocode

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)
akuhn
quelle
2
Ich würde diesen Ansatz vermeiden. Die Tendenz ist, all diese Skript-Schnipsel in eine Datenbank zu schreiben, und dann verlieren Sie die Quellcodeverwaltung, die Versionierung und das Testen.
Ewan
Meinetwegen. Sie bewahren sie am besten in einem separaten Git-Repo oder wo auch immer auf. Aktualisierte meine Antwort, um zu sagen, dass Snippets idealerweise unter Versionskontrolle stehen sollten.
Akuhn
Könnten sie in so etwas wie einer Eigenschaftendatei gespeichert werden? Ich versuche zu vermeiden, dass feste Kundeneigenschaften an mehr als einem Ort existieren, da es sonst leicht ist, sich in nicht zu wartende Spaghetti zu verwandeln.
Andrew
2

Ich werde gegen den Strom schwimmen.

Ich würde versuchen, meine eigene Ausdruckssprache mit ANTLR zu implementieren .

Bisher basieren alle Antworten stark auf der Anpassung des Codes. Die Implementierung konkreter Klassen für jeden Kunden scheint mir zu einem späteren Zeitpunkt nicht gut skalierbar zu sein. Die Wartung wird teuer und schmerzhaft sein.

Mit Antlr besteht die Idee darin, Ihre eigene Sprache zu definieren. Dass Sie Benutzern (oder Entwicklern) erlauben können, Geschäftsregeln in einer solchen Sprache zu schreiben.

Nehmen Sie Ihren Kommentar als Beispiel:

Ich möchte die Formel "result = if (Payment.id.startsWith (" R ")) schreiben? Payment.percentage * Payment.total: Payment.otherValue"

Mit Ihrer EL sollte es Ihnen möglich sein, Sätze wie:

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

Dann...

Speichern Sie das als eine Eigenschaft für den Kunden und fügen Sie es in die entsprechende Methode ein?

Du könntest. Es ist eine Zeichenfolge, die Sie als Eigenschaft oder Attribut speichern können.

Ich werde nicht lügen. Es ist ziemlich kompliziert und schwer. Noch schwieriger ist es, wenn auch die Geschäftsregeln kompliziert sind.

Hier einige SO-Fragen, die Sie interessieren könnten:


Hinweis: ANTLR generiert auch Code für Python und Javascript. Das kann helfen, Proofs of Concept zu schreiben, ohne zu viel Aufwand.

Wenn Sie feststellen, dass Antlr zu schwierig ist, können Sie es mit Bibliotheken wie Expr4J, JEval, Parsii versuchen. Diese arbeiten mit einer höheren Abstraktionsebene.

Laiv
quelle
Es ist eine gute Idee, aber es bereitet dem nächsten Kerl Wartungsarbeiten. Aber ich werde keine Idee verwerfen, bis ich alle Variablen ausgewertet und abgewogen habe.
Andrew
1
Je mehr Möglichkeiten Sie haben, desto besser. Ich wollte nur noch einen geben
Laiv
Es ist nicht zu leugnen, dass dies ein gültiger Ansatz ist. Schauen Sie sich nur die Websphere oder andere Standard-Unternehmenssysteme an, die „die Endbenutzer anpassen können“. Aber wie @akuhns Antwort ist der Nachteil, dass Sie jetzt in zwei Sprachen programmieren und Ihre Versionierung verlieren / Quellcodeverwaltung / Änderungskontrolle
Ewan
@Ewan Entschuldigung, ich fürchte, ich habe Ihre Argumente nicht verstanden. Die Sprachbeschreibungen und die automatisch generierten Klassen können Teil des Projekts sein oder nicht. Ich verstehe aber auf keinen Fall, warum ich die Versionierung / Quellcodeverwaltung verlieren könnte. Auf der anderen Seite scheint es 2 verschiedene Sprachen zu geben, aber das ist nur vorübergehend. Sobald die Sprache fertig ist, legen Sie nur noch Zeichenfolgen fest. Wie Cron-Ausdrücke. Auf lange Sicht gibt es viel weniger Grund zur Sorge.
Laiv
"Wenn paymentID startWith 'R' dann (paymentPercentage / paymentTotal) sonst paymentOther" wo speichern Sie das? welche version ist das In welcher Sprache ist es geschrieben? Welche Unit-Tests hast du dafür?
Ewan
1

Sie können den Algorithmus zumindest externalisieren, damit sich die Kundenklasse nicht ändern muss, wenn ein neuer Kunde hinzugefügt wird, indem Sie ein Entwurfsmuster mit der Bezeichnung "Strategiemuster" verwenden (es befindet sich in der Vierergruppe).

Aus dem Snippet, das Sie gegeben haben, ist es fraglich, ob das Strategiemuster weniger oder mehr wartbar ist, aber es würde zumindest das Wissen der Kundenklasse darüber beseitigen, was getan werden muss (und würde Ihren Switch-Fall beseitigen).

Ein StrategyFactory-Objekt erstellt einen StrategyIntf-Zeiger (oder eine Referenz) basierend auf der CustomerID. Die Factory kann eine Standardimplementierung für nicht spezielle Kunden zurückgeben.

Die Kundenklasse muss nur die Fabrik nach der richtigen Strategie fragen und sie dann anrufen.

Dies ist ein sehr kurzes Pseudo-C ++, um Ihnen zu zeigen, was ich meine.

class Customer
{
public:

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


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

Der Nachteil dieser Lösung besteht darin, dass Sie für jeden neuen Kunden, der eine spezielle Logik benötigt, eine neue Implementierung der CalculationsStrategyIntf erstellen und die Factory aktualisieren müssen, um sie für den / die entsprechenden Kunden zurückzugeben. Dies erfordert auch das Kompilieren. Aber Sie würden zumindest den ständig wachsenden Spaghetti-Code in der Kundenklasse vermeiden.

Matthew James Briggs
quelle
Ja, ich glaube, jemand hat in einer anderen Antwort ein ähnliches Muster erwähnt, damit jeder Kunde die Logik in einer separaten Klasse kapselt, die die Kundenschnittstelle implementiert, und diese Klasse dann aus der PaymentProcessor-Klasse aufruft. Es ist vielversprechend, aber es bedeutet, dass wir jedes Mal, wenn wir einen neuen Kunden gewinnen, eine neue Klasse schreiben müssen - keine große Sache, aber nichts, was ein Support-Mitarbeiter tun kann. Trotzdem ist es eine Option.
Andrew
-1

Erstellen Sie eine Schnittstelle mit einer einzelnen Methode und verwenden Sie Lamdas in jeder Implementierungsklasse. Oder Sie können Sie anonyme Klasse, um die Methoden für verschiedene Clients zu implementieren

Zubair A.
quelle