Design corretto per una classe con un metodo che può variare tra i clienti

12

Ho una classe utilizzata per elaborare i pagamenti dei clienti. Tutti tranne uno dei metodi di questa classe sono gli stessi per ogni cliente, ad eccezione di uno che calcola (ad esempio) quanto deve l'utente del cliente. Questo può variare notevolmente da cliente a cliente e non esiste un modo semplice per acquisire la logica dei calcoli in qualcosa di simile a un file di proprietà, in quanto può esserci un numero qualsiasi di fattori personalizzati.

Potrei scrivere un codice brutto che cambia in base a 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
}

ma ciò richiede la ricostruzione della classe ogni volta che riceviamo un nuovo cliente. Qual è il modo migliore?

[Modifica] L'articolo "duplicato" è completamente diverso. Non sto chiedendo come evitare un'istruzione switch, sto chiedendo il design moderno più adatto a questo caso - che potrei risolvere con una dichiarazione di switch se volessi scrivere il codice del dinosauro. Gli esempi forniti sono generici e non utili, poiché in pratica dicono "Ehi, l'interruttore funziona abbastanza bene in alcuni casi, non in altri."

[Modifica] Ho deciso di andare con la risposta top-class (creare una classe "Cliente" separata per ogni cliente che implementa un'interfaccia standard) per i seguenti motivi:

  1. Consistenza: posso creare un'interfaccia che garantisca che tutte le classi cliente ricevano e restituiscano lo stesso output, anche se create da un altro sviluppatore

  2. Mantenibilità: tutto il codice è scritto nella stessa lingua (Java), quindi non è necessario che qualcun altro apprenda un linguaggio di codifica separato per mantenere quella che dovrebbe essere una caratteristica semplice.

  3. Riutilizzo: nel caso in cui un problema simile emerga nel codice, posso riutilizzare la classe Customer per contenere un numero qualsiasi di metodi per implementare la logica "personalizzata".

  4. Familiarità: so già come farlo, così posso farlo rapidamente e passare ad altri problemi più urgenti.

Svantaggi:

  1. Ogni nuovo cliente richiede una compilazione della nuova classe Customer, che può aggiungere una certa complessità al modo in cui compiliamo e implementiamo le modifiche.

  2. Ogni nuovo cliente deve essere aggiunto da uno sviluppatore - una persona di supporto non può semplicemente aggiungere la logica a qualcosa come un file di proprietà. Questo non è l'ideale ... ma poi non ero nemmeno sicuro di come una persona di supporto sarebbe in grado di scrivere la logica di business necessaria, specialmente se è complessa con molte eccezioni (come è probabile).

  3. Non si scalerà bene se aggiungiamo molti, molti nuovi clienti. Questo non è previsto, ma se dovesse succedere dovremo ripensare a molte altre parti del codice e anche a questo.

Per quelli di voi interessati, è possibile utilizzare Java Reflection per chiamare una classe per nome:

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);
    
posta Andrew 21.12.2016 - 22:51
fonte

8 risposte

14

I have a class used to process customer payments. All but one of the methods of this class are the same for every customer, except for one that calculates (for example) how much the customer's user owes.

Mi vengono in mente due opzioni.

Opzione 1: fai della tua classe una classe astratta, in cui il metodo che varia tra i clienti è un metodo astratto. Quindi crea una sottoclasse per ciascun cliente.

Opzione 2: crea una classe Customer o un'interfaccia ICustomer , contenente tutta la logica dipendente dal cliente. Invece di fare in modo che la classe di elaborazione dei pagamenti accetti un ID cliente, accetta un oggetto Customer o ICustomer . Ogni volta che deve fare qualcosa dipendente dal cliente, chiama il metodo appropriato.

    
risposta data 22.12.2016 - 01:06
fonte
10

Potresti voler esaminare i calcoli personalizzati come "plug-in" per la tua applicazione. Quindi, dovresti usare un file di configurazione per dire al tuo programma quale plugin di calcolo dovrebbe essere usato per quale cliente. In questo modo, la tua applicazione principale non avrebbe bisogno di essere ricompilata per ogni nuovo cliente - ha solo bisogno di leggere (o rileggere) un file di configurazione e caricare nuovi plug-in.

    
risposta data 21.12.2016 - 22:59
fonte
4

Vado con una serie di regole per descrivere i calcoli. Questo può essere conservato in qualsiasi archivio di persistenza e modificato dinamicamente.

In alternativa, considera questo:

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

Dove customerOpsIndex ha calcolato l'indice operativo corretto (sai quale cliente ha bisogno di quale trattamento).

    
risposta data 21.12.2016 - 22:53
fonte
3

Qualcosa di simile al seguente:

notate che avete ancora l'istruzione switch nel repository. Non è vero, però, a questo punto, è necessario mappare un ID cliente alla logica richiesta. Puoi diventare intelligente e spostarlo in un file di configurazione, inserire la mappatura in un dizionario o caricarlo dinamicamente negli assiemi logici, ma in pratica si riduce a passare.

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();
    }
}
    
risposta data 22.12.2016 - 10:55
fonte
2

Sembra che tu abbia una mappatura 1 a 1 dai clienti al codice personalizzato e poiché stai utilizzando un linguaggio compilato devi ricostruire il tuo sistema ogni volta che ricevi un nuovo cliente

Prova un linguaggio di scripting incorporato.

Ad esempio, se il tuo sistema è in Java, potresti incorporare JRuby e quindi per ogni negozio cliente uno snippet corrispondente di codice Ruby. Idealmente da qualche parte sotto il controllo della versione, sia nello stesso che in un repository git separato. E poi valuta quel frammento nel contesto della tua applicazione Java. JRuby può chiamare qualsiasi funzione Java e accedere a qualsiasi oggetto 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!";
    }

}

Questo è un modello molto comune. Ad esempio, molti giochi per computer sono scritti in C ++ ma usano script Lua incorporati per definire il comportamento del cliente di ciascun avversario nel gioco.

D'altra parte, se hai un numero di mapping da molti a 1 dai clienti al codice personalizzato, usa il modello "Strategia" come già suggerito.

Se la mappatura non si basa sulla creazione di ID utente, aggiungi una funzione match a ciascun oggetto strategia e fai una scelta ordinata di quale strategia utilizzare.

Ecco qualche pseudo codice

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)
    
risposta data 22.12.2016 - 10:32
fonte
2

Ho intenzione di nuotare contro corrente.

Vorrei provare a implementare la mia lingua di espressione con ANTLR .

Finora, tutte le risposte sono strongmente basate sulla personalizzazione del codice. L'implementazione di corsi concreti per ciascun cliente mi sembra che, in futuro, non sarà in grado di scalare bene. La manutenzione sarà costosa e dolorosa.

Quindi, con Antlr, l'idea è di definire la tua lingua. Che puoi consentire agli utenti (o agli sviluppatori) di scrivere regole commerciali in tale lingua.

Prendendo il tuo commento come esempio:

I want to write the formula "result = if (Payment.id.startsWith("R")) ? Payment.percentage * Payment.total : Payment.otherValue"

Con il tuo EL, dovrebbe essere possibile dichiarare frasi come:

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

Quindi ...

save that as a property for the customer, and insert it in the appropriate method?

Potresti. È una stringa, puoi salvarla come proprietà o attributo.

Non mentirò. È piuttosto complicato e difficile. È ancora più difficile se anche le regole aziendali sono complicate.

Ecco alcune domande su SO che potrebbero interessarti:

Nota: ANTLR genera anche il codice per Python e Javascript. Questo può aiutare a scrivere prove di concetto senza troppi sovraccarichi.

Se trovi che Antlr è troppo difficile, puoi provare con lib come Expr4J, JEval, Parsii. Funzionano con un livello più alto di astrazione.

    
risposta data 23.12.2016 - 00:02
fonte
1

Puoi almeno esternalizzare l'algoritmo in modo che la classe Cliente non debba cambiare quando un nuovo cliente viene aggiunto utilizzando un modello di progettazione chiamato Pattern di strategia (è nella Banda dei Quattro).

Dallo snippet che hai fornito, è discutibile se il modello di strategia sarebbe meno gestibile o più gestibile, ma eliminerebbe almeno la conoscenza della classe Cliente su ciò che deve essere fatto (e eliminerebbe il tuo caso di cambiamento).

Un oggetto StrategyFactory creerebbe un puntatore (o riferimento) StrategyIntf basato su CustomerID. The Factory potrebbe restituire un'implementazione predefinita per i clienti che non sono speciali.

La classe Cliente deve solo chiedere alla Factory la strategia corretta e poi chiamarla.

Questo è molto breve C ++ per mostrarti cosa intendo.

class Customer
{
public:

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


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

Lo svantaggio di questa soluzione è che, per ogni nuovo cliente che necessita di una logica speciale, è necessario creare una nuova implementazione di CalculationsStrategyIntf e aggiornare la fabbrica per restituirla per i clienti appropriati. Ciò richiede anche la compilazione. Ma eviterai almeno un codice spaghetti sempre crescente nella classe del cliente.

    
risposta data 23.12.2016 - 18:19
fonte
-1

Crea un'interfaccia con un singolo metodo e usa lamdas in ogni classe di implementazione. Oppure puoi fare una lezione anonima per implementare i metodi per diversi client

    
risposta data 28.12.2016 - 07:39
fonte

Leggi altre domande sui tag