Progettazione corretta per una classe con un metodo che può variare tra i clienti


12

Ho una classe utilizzata per elaborare i pagamenti dei clienti. Tutti i metodi tranne uno di questa classe sono gli stessi per tutti i clienti, tranne uno che calcola (ad esempio) quanto deve il cliente. Ciò 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à, poiché possono esserci un numero qualsiasi di fattori personalizzati.

Potrei scrivere un brutto codice che cambia in base all'ID cliente:

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 otteniamo 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 che si applica meglio a questo caso - che potrei risolvere con un'istruzione switch se volessi scrivere un codice dinosauro. Gli esempi forniti sono generici e non utili, dal momento che essenzialmente dicono "Ehi, l'interruttore funziona abbastanza bene in alcuni casi, non in altri".


[Modifica] Ho deciso di scegliere la risposta più votata (creare una classe "Cliente" separata per ciascun cliente che implementa un'interfaccia standard) per i seguenti motivi:

  1. Coerenza: posso creare un'interfaccia che assicuri che tutte le classi di clienti ricevano e restituiscano lo stesso output, anche se creato da un altro sviluppatore

  2. Manutenibilità: tutto il codice è scritto nella stessa lingua (Java), quindi non è necessario che nessun altro impari un linguaggio di codifica separato per mantenere quella che dovrebbe essere una funzionalità semplicissima.

  3. Riutilizzo: nel caso si verifichi un problema simile 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, quindi 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 delle proprietà. Questo non è l'ideale ... ma poi non ero sicuro di come una persona del Supporto sarebbe in grado di scrivere la logica aziendale necessaria, specialmente se è complessa con molte eccezioni (come è probabile).

  3. Non si ridimensionerà bene se aggiungiamo molti, molti nuovi clienti. Ciò non è previsto, ma in tal caso dovremo ripensare molte altre parti del codice, oltre a questa.

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

1
Forse dovresti approfondire il tipo di calcoli. Si sta calcolando solo uno sconto o è un flusso di lavoro diverso?
qwerty_so,

@ThomasKilian È solo un esempio. Immagina un oggetto Payment, e i calcoli possono essere qualcosa del tipo "moltiplica il Payment.percentage per il Payment.total - ma non se Payment.id inizia con 'R'". Quel tipo di granularità. Ogni cliente ha le sue regole.
Andrew,


Hai provato qualcosa di simile a questo . Impostazione di formule diverse per ciascun cliente.
Laiv,

1
I plug-in suggeriti da varie risposte funzioneranno solo se si dispone di un prodotto dedicato per cliente. Se si dispone di una soluzione server in cui si verifica l'ID cliente in modo dinamico, non funzionerà. Allora, qual è il tuo ambiente?
qwerty_so,

Risposte:


14

Ho una classe utilizzata per elaborare i pagamenti dei clienti. Tutti i metodi tranne uno di questa classe sono gli stessi per tutti i clienti, tranne uno che calcola (ad esempio) quanto deve il cliente.

Mi vengono in mente due opzioni.

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

Opzione 2: creare una Customerclasse o ICustomerun'interfaccia contenente tutta la logica dipendente dal cliente. Invece di fare in modo che la tua classe di elaborazione dei pagamenti accetti un ID cliente, fallo accettare a Customero ICustomeroggetto. Ogni volta che deve fare qualcosa che dipende dal cliente, chiama il metodo appropriato.


La classe del cliente non è una cattiva idea. Dovrà esserci una sorta di oggetto personalizzato per ciascun cliente, ma non ero chiaro se questo dovesse essere un record DB, un file delle proprietà (di qualche tipo) o un'altra classe.
Andrew,

10

Potresti voler esaminare la scrittura dei calcoli personalizzati come "plug-in" per la tua applicazione. Quindi, useresti un file di configurazione per dirti al programma quale plugin di calcolo dovrebbe essere usato per quale cliente. In questo modo, l'applicazione principale non dovrebbe essere ricompilata per ogni nuovo cliente - deve solo leggere (o rileggere) un file di configurazione e caricare nuovi plug-in.


Grazie. Come funzionerebbe un plug-in in un ambiente Java? Ad esempio, voglio scrivere la formula "result = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue" salvarlo come proprietà per il cliente e inserirlo nel metodo appropriato?
Andrew,

@Andrew, un modo in cui un plugin può funzionare è usare la reflection per caricare in una classe per nome. Il file di configurazione elenca i nomi delle classi per i clienti. Ovviamente, dovresti avere un modo per identificare quale plugin è per quale cliente (ad esempio, con il suo nome o alcuni metadati memorizzati da qualche parte).
Kat,

@Kat Grazie, se hai letto la mia domanda modificata in realtà è così che lo sto facendo. Lo svantaggio è che uno sviluppatore deve creare una nuova classe che limiti la scalabilità: preferirei che una persona di supporto potesse semplicemente modificare un file delle proprietà ma penso che ci sia troppa complessità.
Andrew,

@Andrew: potresti essere in grado di scrivere le tue formule in un file delle proprietà, ma allora avresti bisogno di una sorta di parser di espressioni. E un giorno potresti imbatterti in una formula troppo complessa per (facilmente) scrivere come espressione di una riga in un file delle proprietà. Se vuoi davvero che i non programmatori siano in grado di lavorare su questi, avrai bisogno di una sorta di editor di espressioni user-friendly che possano generare codice per la tua applicazione. Questo può essere fatto (l'ho visto), ma non è banale.
FrustratedWithFormsDesigner

4

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

In alternativa, considerare questo:

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

Dove customerOpsIndexcalcolato il giusto indice di funzionamento (sai quale cliente ha bisogno di quale trattamento).


Se potessi creare un set di regole completo lo farei. Ma ogni nuovo cliente potrebbe avere il proprio set di regole. Inoltre, preferirei che l'intera cosa fosse contenuta nel codice, se esiste un modello di progettazione per farlo. Il mio esempio switch {} lo fa abbastanza bene, ma non è molto flessibile.
Andrew,

È possibile archiviare le operazioni da chiamare per un cliente in un array e utilizzare l'ID cliente per indicizzarlo.
qwerty_so,

Sembra più promettente. :)
Andrew,

3

Qualcosa come il seguente:

nota che hai ancora l'istruzione switch nel repository. Tuttavia, non è possibile aggirare il problema, a un certo punto è necessario mappare un ID cliente sulla logica richiesta. Puoi diventare intelligente e spostarlo in un file di configurazione, inserire la mappatura in un dizionario o caricare dinamicamente negli assembly logici, ma sostanzialmente si riduce per 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();
    }
}

2

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

Prova un linguaggio di scripting incorporato.

Ad esempio, se il tuo sistema è in Java, puoi incorporare JRuby e quindi per ogni cliente memorizzare un frammento corrispondente di codice Ruby. Idealmente da qualche parte sotto il controllo della versione, nello stesso o in un repository git separato. Quindi 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 utilizzano gli script Lua incorporati per definire il comportamento del cliente di ciascun avversario nel gioco.

D'altra parte, se si dispone di una mappatura da molti a 1 dai clienti al codice personalizzato, utilizzare semplicemente il modello "Strategia" come già suggerito.

Se la mappatura non si basa sulla creazione dell'ID utente, aggiungere una matchfunzione a ciascun oggetto strategia ed effettuare una scelta ordinata della strategia da utilizzare.

Ecco qualche pseudo codice

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

2
Eviterei questo approccio. La tendenza è quella di mettere tutti quei frammenti di script in un db e quindi si perde il controllo del codice sorgente, il controllo delle versioni e i test.
Ewan,

Giusto. È meglio conservarli in un repository git separato o ovunque. Aggiornato la mia risposta per dire che gli snippet dovrebbero idealmente essere sotto il controllo della versione.
Akuhn

Potrebbero essere memorizzati in qualcosa come un file delle proprietà? Sto cercando di evitare che esistano proprietà fisse del Cliente in più di una posizione, altrimenti è facile trasformarsi in spaghetti non mantenibili.
Andrew,

2

Vado a nuotare contro corrente.

Vorrei provare a implementare il mio linguaggio di espressioni con ANTLR .

Finora, tutte le risposte sono fortemente basate sulla personalizzazione del codice. L'implementazione di classi concrete per ciascun cliente mi sembra che, ad un certo punto in futuro, non si ridimensionerà bene. La manutenzione sarà costosa e dolorosa.

Quindi, con Antlr, l'idea è quella 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:

Voglio scrivere la formula "risultato = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue"

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

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

Poi...

salvarlo come proprietà per il cliente e inserirlo nel metodo appropriato?

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

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

Ecco alcune domande SO che potrebbero interessarti:


Nota: ANTLR genera codice anche per Python e Javascript. Ciò può aiutare a scrivere prove concettuali senza troppe spese generali.

Se trovi che Antlr sia troppo difficile, potresti provare con librerie come Expr4J, JEval, Parsii. Funziona con un livello più alto di astrazione.


È una buona idea, ma un mal di testa di mantenimento per il prossimo ragazzo lungo la linea. Ma non ho intenzione di scartare alcuna idea fino a quando non ho valutato e valutato tutte le variabili.
Andrew,

1
Le molte opzioni che hai di meglio. Volevo solo dare un altro
Laiv il

Non si può negare che si tratti di un approccio valido, basta guardare websphere o altri sistemi aziendali standard che "gli utenti finali possono personalizzare" Ma come la risposta di @akuhn il rovescio della medaglia è che ora stai programmando in 2 lingue e perdi il tuo controllo delle versioni / controllo sorgente / controllo modifica
Ewan,

@Ewan scusa, temo di non aver capito i tuoi argomenti. I descrittori di lingua e le classi autogenerate possono far parte o meno del progetto. Ma in ogni caso non vedo perché potrei perdere il controllo delle versioni / gestione del codice sorgente. D'altra parte, potrebbe sembrare che ci siano 2 lingue diverse, ma è temporaneo. Una volta terminata la lingua, imposti solo le stringhe. Come le espressioni di Cron. A lungo termine c'è molto meno motivo di preoccuparsi.
Laiv,

"Se paymentID inizia Con 'R' allora (paymentPercentage / paymentTotal) else paymentAltro" dove lo memorizzi? che versione è? in che lingua è scritto? quali unit test hai per questo?
Ewan,

1

Puoi almeno esternalizzare l'algoritmo in modo che la classe Cliente non debba cambiare quando viene aggiunto un nuovo cliente usando un modello di progettazione chiamato Modello di strategia (è nella banda di quattro).

Dallo snippet che hai fornito, è discutibile se il modello di strategia sarebbe meno gestibile o più mantenibile, ma almeno eliminerebbe la conoscenza della classe del cliente su ciò che deve essere fatto (ed eliminerebbe la tua custodia).

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

La classe del cliente deve solo chiedere alla fabbrica la strategia corretta e quindi chiamarla.

Questo è un breve pseudo 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 ai clienti appropriati. Ciò richiede anche la compilazione. Ma almeno eviteresti un codice spaghetti sempre crescente nella classe dei clienti.


Sì, penso che qualcuno abbia menzionato un modello simile in un'altra risposta, per ogni cliente di incapsulare la logica in una classe separata che implementa l'interfaccia del cliente e quindi chiamare quella classe dalla classe PaymentProcessor. È promettente ma significa che ogni volta che attiriamo un nuovo cliente dobbiamo scrivere una nuova classe - non è un grosso problema, ma non qualcosa che una persona dell'assistenza può fare. Comunque è un'opzione.
Andrew,

-1

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.