Conception appropriée pour une classe avec une méthode qui peut varier selon les clients

12

J'ai une classe utilisée pour traiter les paiements des clients. Toutes les méthodes de cette classe, sauf une, sont les mêmes pour chaque client, à l'exception de celle qui calcule (par exemple) le montant dû par l'utilisateur du client. Cela peut varier considérablement d'un client à l'autre et il n'y a pas de moyen facile de capturer la logique des calculs dans quelque chose comme un fichier de propriétés, car il peut y avoir un certain nombre de facteurs personnalisés.

Je pourrais écrire du code laid qui change en fonction de 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
}

mais cela nécessite de reconstruire la classe chaque fois que nous obtenons un nouveau client. Quelle est la meilleure façon?

[Modifier] L'article "en double" est complètement différent. Je ne demande pas comment éviter une déclaration de commutateur, je demande le design moderne qui s'applique le mieux à ce cas - que je pourrais résoudre avec une déclaration de commutateur si je voulais écrire du code de dinosaure. Les exemples qui y sont fournis sont génériques et ne sont pas utiles, car ils disent essentiellement "Hé, le commutateur fonctionne assez bien dans certains cas, pas dans d'autres."


[Modifier] J'ai décidé de choisir la réponse la mieux classée (créer une classe "Client" distincte pour chaque client qui implémente une interface standard) pour les raisons suivantes:

  1. Cohérence: je peux créer une interface qui garantit que toutes les classes clients reçoivent et retournent la même sortie, même si elles ont été créées par un autre développeur

  2. Maintenabilité: Tout le code est écrit dans le même langage (Java), il n'est donc pas nécessaire que quiconque apprenne un langage de codage distinct afin de maintenir ce qui devrait être une fonctionnalité simple et morte.

  3. Réutilisation: dans le cas où un problème similaire apparaît dans le code, je peux réutiliser la classe Customer pour contenir un nombre illimité de méthodes pour implémenter une logique "personnalisée".

  4. Familiarité: je sais déjà comment procéder, je peux donc le faire rapidement et passer à d'autres problèmes plus urgents.

Désavantages:

  1. Chaque nouveau client nécessite une compilation de la nouvelle classe Customer, ce qui peut ajouter de la complexité à la façon dont nous compilons et déployons les modifications.

  2. Chaque nouveau client doit être ajouté par un développeur - une personne de support ne peut pas simplement ajouter la logique à quelque chose comme un fichier de propriétés. Ce n'est pas idéal ... mais je ne savais pas non plus comment une personne de support serait en mesure d'écrire la logique métier nécessaire, surtout si elle est complexe à de nombreuses exceptions près (comme cela est probable).

  3. Cela n'évolue pas bien si nous ajoutons de très nombreux nouveaux clients. Ce n'est pas prévu, mais si cela se produit, nous devrons repenser de nombreuses autres parties du code ainsi que celle-ci.

Pour ceux d'entre vous qui sont intéressés, vous pouvez utiliser Java Reflection pour appeler une classe par son nom:

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
la source
1
Vous devriez peut-être préciser le type de calculs. S'agit-il simplement d'une remise en cours de calcul ou s'agit-il d'un flux de travail différent?
qwerty_so
@ThomasKilian Ce n'est qu'un exemple. Imaginez un objet Payment, et les calculs peuvent être quelque chose comme "multipliez le Payment.percentage par le Payment.total - mais pas si Payment.id commence par" R "". Ce genre de granularité. Chaque client a ses propres règles.
Andrew
Duplicata possible des instructions de commutateur
moucher
Avez-vous essayé quelque chose comme ça . Définition de formules différentes pour chaque client.
Laiv
1
Les plug-ins suggérés à partir de diverses réponses ne fonctionneront que si vous avez un produit dédié par client. Si vous avez une solution serveur où vous vérifiez dynamiquement l'ID client, cela ne fonctionnera pas. Alors, quel est votre environnement?
qwerty_so

Réponses:

14

J'ai une classe utilisée pour traiter les paiements des clients. Toutes les méthodes de cette classe, sauf une, sont les mêmes pour chaque client, à l'exception de celle qui calcule (par exemple) le montant dû par l'utilisateur du client.

Deux options me viennent à l'esprit.

Option 1: Faites de votre classe une classe abstraite, où la méthode qui varie selon les clients est une méthode abstraite. Créez ensuite une sous-classe pour chaque client.

Option 2: créez une Customerclasse, ou une ICustomerinterface, contenant toute la logique dépendante du client. Au lieu que votre classe de traitement des paiements accepte un identifiant client, faites-le accepter un Customerou un ICustomerobjet. Chaque fois qu'il doit faire quelque chose de dépendant du client, il appelle la méthode appropriée.

Tanner Swett
la source
La classe Client n'est pas une mauvaise idée. Il devra y avoir une sorte d'objet personnalisé pour chaque client, mais je ne savais pas clairement s'il devait s'agir d'un enregistrement DB, d'un fichier de propriétés (d'une sorte) ou d'une autre classe.
Andrew
10

Vous souhaiterez peut-être envisager d'écrire les calculs personnalisés en tant que «plug-ins» pour votre application. Ensuite, vous utiliseriez un fichier de configuration pour indiquer à votre programme quel plugin de calcul doit être utilisé pour quel client. De cette façon, votre application principale n'aurait pas besoin d'être recompilée pour chaque nouveau client - elle n'a qu'à lire (ou relire) un fichier de configuration et charger de nouveaux plug-ins.

FrustratedWithFormsDesigner
la source
Merci. Comment un plug-in fonctionnerait-il dans un environnement Java? Par exemple, je veux écrire la formule "result = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue" enregistrez-le en tant que propriété pour le client et insérez-le dans la méthode appropriée?
Andrew
@Andrew, Une façon dont un plugin peut fonctionner est d'utiliser la réflexion pour charger dans une classe par son nom. Le fichier de configuration répertorie les noms des classes pour les clients. Bien sûr, vous devriez avoir un moyen d'identifier quel plugin est pour quel client (par exemple, par son nom ou certaines métadonnées stockées quelque part).
Kat
@ Kat Merci, si vous lisez ma question modifiée, c'est en fait ainsi que je le fais. L'inconvénient est qu'un développeur doit créer une nouvelle classe qui limite l'évolutivité - je préférerais qu'une personne de support puisse simplement éditer un fichier de propriétés mais je pense qu'il y a trop de complexité.
Andrew
@Andrew: Vous pourriez peut-être écrire vos formules dans un fichier de propriétés, mais vous auriez alors besoin d'une sorte d'analyseur d'expressions. Et vous pourriez un jour rencontrer une formule trop complexe pour écrire (facilement) en tant qu'expression d'une ligne dans un fichier de propriétés. Si vous voulez vraiment que les non-programmeurs puissent travailler dessus, vous aurez besoin d'une sorte d'éditeur d'expression convivial à utiliser qui générera du code pour votre application. Cela peut être fait (je l'ai vu), mais ce n'est pas anodin.
FrustratedWithFormsDesigner
4

J'irais avec un ensemble de règles pour décrire les calculs. Cela peut être conservé dans n'importe quel magasin de persistance et modifié dynamiquement.

Comme alternative, considérez ceci:

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

Où est customerOpsIndexcalculé le bon indice d'opération (vous savez quel client a besoin de quel traitement).

qwerty_so
la source
Si je pouvais créer un ensemble de règles complet, je le ferais. Mais chaque nouveau client peut avoir son propre ensemble de règles. De plus, je préférerais que tout soit contenu dans le code, s'il existe un modèle de conception pour le faire. Mon exemple de commutateur {} le fait très bien, mais ce n'est pas très flexible.
Andrew
Vous pouvez stocker les opérations à appeler pour un client dans un tableau et utiliser l'ID client pour l'indexer.
qwerty_so
Cela semble plus prometteur. :)
Andrew
3

Quelque chose comme ci-dessous:

notez que vous avez toujours l'instruction switch dans le dépôt. Il n'y a cependant pas vraiment de solution, à un moment donné, vous devez mapper un identifiant client à la logique requise. Vous pouvez devenir intelligent et le déplacer vers un fichier de configuration, mettre le mappage dans un dictionnaire ou charger dynamiquement dans les assemblages logiques, mais cela se résume essentiellement à basculer.

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
la source
2

On dirait que vous avez un mappage 1 à 1 des clients au code personnalisé et parce que vous utilisez un langage compilé, vous devez reconstruire votre système chaque fois que vous obtenez un nouveau client

Essayez un langage de script intégré.

Par exemple, si votre système est en Java, vous pouvez intégrer JRuby, puis pour chaque magasin client un extrait de code Ruby correspondant. Idéalement quelque part sous contrôle de version, dans le même ou dans un dépôt git séparé. Ensuite, évaluez cet extrait dans le contexte de votre application Java. JRuby peut appeler n'importe quelle fonction Java et accéder à n'importe quel objet 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!";
    }

}

Il s'agit d'un schéma très courant. Par exemple, de nombreux jeux informatiques sont écrits en C ++ mais utilisent des scripts Lua intégrés pour définir le comportement client de chaque adversaire dans le jeu.

D'un autre côté, si vous avez un mappage plusieurs à un entre les clients et le code personnalisé, utilisez simplement le modèle "Stratégie" comme déjà suggéré.

Si le mappage n'est pas basé sur l'ID utilisateur, ajoutez une matchfonction à chaque objet de stratégie et faites un choix ordonné de la stratégie à utiliser.

Voici un pseudo code

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)
akuhn
la source
2
J'éviterais cette approche. La tendance est de mettre tous ces extraits de script dans une base de données, puis vous perdez le contrôle des sources, la gestion des versions et les tests.
Ewan
C'est suffisant. Ils les stockent mieux dans un dépôt git séparé ou n'importe où. Mise à jour de ma réponse pour dire que les extraits devraient idéalement être sous contrôle de version.
akuhn
Pourraient-ils être stockés dans quelque chose comme un fichier de propriétés? J'essaie d'éviter que des propriétés fixes du client existent dans plus d'un emplacement, sinon il est facile de se transformer en spaghettis incontrôlables.
Andrew
2

Je vais nager à contre-courant.

J'essaierais d'implémenter mon propre langage d'expression avec ANTLR .

Jusqu'à présent, toutes les réponses sont fortement basées sur la personnalisation du code. La mise en place de classes concrètes pour chaque client me semble que, à un moment donné dans le futur, cela ne va pas bien évoluer. L'entretien va être coûteux et douloureux.

Donc, avec Antlr, l'idée est de définir votre propre langue. Que vous pouvez autoriser les utilisateurs (ou les développeurs) à écrire des règles métier dans un tel langage.

Prenons votre commentaire comme exemple:

Je veux écrire la formule "result = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue"

Avec votre EL, il devrait être possible pour vous de prononcer des phrases comme:

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

Alors...

enregistrer cela comme une propriété pour le client et l'insérer dans la méthode appropriée?

Vous pourriez. C'est une chaîne, vous pouvez l'enregistrer en tant que propriété ou attribut.

Je ne mentirai pas. C'est assez compliqué et difficile. C'est encore plus difficile si les règles métier sont également compliquées.

Voici quelques questions SO qui pourraient vous intéresser:


Remarque: ANTLR génère également du code pour Python et Javascript. Cela peut aider à rédiger des preuves de concept sans trop de frais généraux.

Si vous trouvez que Antlr est trop dur, vous pouvez essayer avec des bibliothèques comme Expr4J, JEval, Parsii. Cela fonctionne avec un niveau d'abstraction plus élevé.

Laiv
la source
C'est une bonne idée mais un casse-tête de maintenance pour le prochain gars sur la ligne. Mais je ne vais jeter aucune idée avant d'avoir évalué et pesé toutes les variables.
Andrew
1
Les nombreuses options sont les meilleures. Je voulais juste en donner un de plus
Laiv
Il ne fait aucun doute que cette approche est valable, il suffit de regarder la sphère Web ou d'autres systèmes d'entreprise standard que «les utilisateurs finaux peuvent personnaliser». Mais comme @akuhn, la réponse est que maintenant vous programmez en 2 langues et perdez votre versioning / source control / change control
Ewan
@Ewan désolé, j'ai peur de ne pas avoir compris vos arguments. Les descripteurs de langage et les classes générées automatiquement peuvent faire partie ou non du projet. Mais en tout cas, je ne vois pas pourquoi je pourrais perdre la gestion des versions / du code source. D'un autre côté, il peut sembler qu'il existe 2 langues différentes, mais c'est temporaire. Une fois la langue terminée, vous ne définissez que des chaînes. Comme les expressions Cron. À long terme, il y a beaucoup moins de raisons de s'inquiéter.
Laiv
"Si paymentID commence avec 'R', alors (paymentPercentage / paymentTotal) sinon paymentOther" où le stockez-vous? de quelle version s'agit-il? dans quelle langue est-il écrit? quels tests unitaires avez-vous pour cela?
Ewan
1

Vous pouvez au moins externaliser l'algorithme afin que la classe Customer n'ait pas besoin de changer lorsqu'un nouveau client est ajouté en utilisant un modèle de conception appelé le modèle de stratégie (c'est dans le Gang of Four).

D'après l'extrait de code que vous avez donné, on peut se demander si le modèle de stratégie serait moins maintenable ou plus maintenable, mais cela éliminerait au moins la connaissance de la classe Client de ce qui doit être fait (et éliminerait votre cas de commutateur).

Un objet StrategyFactory créerait un pointeur StrategyIntf (ou référence) basé sur le CustomerID. L'usine pourrait renvoyer une implémentation par défaut pour les clients qui ne sont pas spéciaux.

La classe Client n'a qu'à demander à l'usine la bonne stratégie, puis à l'appeler.

C'est un pseudo C ++ très bref pour vous montrer ce que je veux dire.

class Customer
{
public:

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


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

L'inconvénient de cette solution est que, pour chaque nouveau client qui a besoin d'une logique spéciale, vous devez créer une nouvelle implémentation de CalculationsStrategyIntf et mettre à jour la fabrique pour la renvoyer au (x) client (s) approprié (s). Cela nécessite également une compilation. Mais vous éviteriez au moins le code de spaghetti sans cesse croissant dans la classe des clients.

Matthew James Briggs
la source
Oui, je pense que quelqu'un a mentionné un modèle similaire dans une autre réponse, pour que chaque client encapsule la logique dans une classe distincte qui implémente l'interface client, puis appelle cette classe à partir de la classe PaymentProcessor. C'est prometteur mais cela signifie que chaque fois que nous recrutons un nouveau client, nous devons écrire une nouvelle classe - pas vraiment une grosse affaire, mais pas quelque chose qu'une personne de support peut faire. C'est toujours une option.
Andrew
-1

Créez une interface avec une méthode unique et utilisez des lamdas dans chaque classe d'implémentation. Ou vous pouvez vous classe anonyme pour implémenter les méthodes pour différents clients

Zubair A.
la source