Diseño adecuado para una clase con un método que puede variar entre los clientes

12

Tengo una clase utilizada para procesar los pagos de los clientes. Todos menos uno de los métodos de esta clase son los mismos para todos los clientes, excepto uno que calcula (por ejemplo) cuánto debe el usuario del cliente. Esto puede variar mucho de un cliente a otro y no hay una manera fácil de capturar la lógica de los cálculos en algo así como un archivo de propiedades, ya que puede haber cualquier número de factores personalizados.

Podría escribir un código feo que cambia según el ID de 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
}

pero esto requiere reconstruir la clase cada vez que obtengamos un nuevo cliente. ¿Cuál es la mejor manera?

[Editar] El artículo "duplicado" es completamente diferente. No estoy preguntando cómo evitar una declaración de cambio, estoy preguntando por el diseño moderno que mejor se aplique a este caso, que podría resolver con una declaración de cambio Si quisiera escribir un código de dinosaurio. Los ejemplos proporcionados allí son genéricos, y no son útiles, ya que esencialmente dicen "Hey, el interruptor funciona bastante bien en algunos casos, no en otros".

[Editar] Decidí ir con la respuesta mejor clasificada (crear una clase de "Cliente" por cada cliente que implemente una interfaz estándar) por las siguientes razones:

  1. Coherencia: puedo crear una interfaz que garantice que todas las clases del Cliente reciban y devuelvan el mismo resultado, incluso si es creado por otro desarrollador

  2. Mantenimiento: todo el código está escrito en el mismo lenguaje (Java), por lo que no es necesario que nadie más aprenda un lenguaje de codificación separado para mantener lo que debería ser una función sencilla.

  3. Reutilización: en caso de que surja un problema similar en el código, puedo reutilizar la clase Cliente para mantener cualquier número de métodos para implementar la lógica "personalizada".

  4. Familiaridad: ya sé cómo hacerlo, así que puedo hacerlo rápidamente y pasar a otros temas más urgentes.

Inconvenientes:

  1. Cada nuevo cliente requiere una compilación de la nueva clase de clientes, lo que puede agregar cierta complejidad a la forma en que compilamos e implementamos los cambios.

  2. Cada nuevo cliente debe ser agregado por un desarrollador: una persona de soporte no puede simplemente agregar la lógica a algo como un archivo de propiedades. Esto no es ideal ... pero tampoco estaba seguro de cómo una persona de Soporte podría escribir la lógica de negocios necesaria, especialmente si es compleja con muchas excepciones (como es probable).

  3. No se escalará bien si agregamos muchos, muchos nuevos clientes. Esto no se espera, pero si sucede, tendremos que repensar muchas otras partes del código, así como esta.

Para aquellos de ustedes interesados, puede usar Java Reflection para llamar a una clase por su nombre:

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

8 respuestas

14
  

Tengo una clase utilizada para procesar los pagos de los clientes. Todos menos uno de los métodos de esta clase son los mismos para todos los clientes, excepto uno que calcula (por ejemplo) cuánto debe el usuario del cliente.

Dos opciones vienen a mi mente.

Opción 1: convierta su clase en una clase abstracta, donde el método que varía entre los clientes es un método abstracto. Luego crea una subclase para cada cliente.

Opción 2: cree una clase Customer , o una interfaz ICustomer , que contenga toda la lógica dependiente del cliente. En lugar de que su clase de procesamiento de pagos acepte un ID de cliente, haga que acepte un objeto Customer o ICustomer . Cada vez que necesita hacer algo dependiente del cliente, llama al método apropiado.

    
respondido por el Tanner Swett 22.12.2016 - 01:06
10

Es posible que desee ver cómo escribir los cálculos personalizados como "complementos" para su aplicación. Luego, usaría un archivo de configuración para indicarle qué programa de complementos de cálculo se debe usar para cada cliente. De esta manera, su aplicación principal no tendría que volver a compilarse para cada nuevo cliente, solo necesita leer (o releer) un archivo de configuración y cargar nuevos complementos.

    
respondido por el FrustratedWithFormsDesigner 21.12.2016 - 22:59
4

Iría con un conjunto de reglas para describir los cálculos. Esto puede guardarse en cualquier almacén de persistencia y modificarse dinámicamente.

Como alternativa, considere esto:

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

Donde customerOpsIndex calculó el índice de operación correcto (usted sabe qué cliente necesita qué tratamiento).

    
respondido por el Thomas Kilian 21.12.2016 - 22:53
3

Algo como el siguiente:

note que todavía tiene la instrucción switch en el repositorio. Sin embargo, no hay una solución real a esto, en algún momento debe asignar un ID de cliente a la lógica requerida. Puede ser inteligente y moverlo a un archivo de configuración, colocar la asignación en un diccionario o cargar dinámicamente en los ensamblajes lógicos, pero esencialmente se reduce al cambio.

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();
    }
}
    
respondido por el Ewan 22.12.2016 - 10:55
2

Parece que tiene una asignación 1 a 1 de clientes a código personalizado y porque está usando un lenguaje compilado, tiene que reconstruir su sistema cada vez que obtiene un nuevo cliente

Prueba un lenguaje de scripting incorporado.

Por ejemplo, si su sistema está en Java, podría incrustar JRuby y luego, para cada cliente, almacenar un fragmento correspondiente del código Ruby. Idealmente en algún lugar bajo el control de versiones, ya sea en el mismo o en un repositorio de git separado. Y luego evalúa ese fragmento en el contexto de tu aplicación Java. JRuby puede llamar a cualquier función de Java y acceder a cualquier objeto de 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!";
    }

}

Este es un patrón muy común. Por ejemplo, muchos juegos de computadora están escritos en C ++ pero usan scripts Lua incrustados para definir el comportamiento del cliente de cada oponente en el juego.

Por otra parte, si tiene una asignación de muchos a 1 de clientes a código personalizado, simplemente use el patrón "Estrategia" como ya se sugirió.

Si la asignación no se basa en la identificación del usuario, agregue una función match a cada objeto de estrategia y haga una elección ordenada de qué estrategia usar.

Aquí hay un pseudo código

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)
    
respondido por el akuhn 22.12.2016 - 10:32
2

Voy a nadar contra la corriente.

Intentaría implementar mi propio lenguaje de expresión con ANTLR .

Hasta ahora, todas las respuestas se basan en gran medida en la personalización del código. La implementación de clases concretas para cada cliente me parece que, en algún momento en el futuro, no va a escalar bien. El mantenimiento será costoso y doloroso.

Entonces, con Antlr, la idea es definir su propio idioma. Que puede permitir a los usuarios (o desarrolladores) escribir reglas de negocios en dicho idioma.

Tomando tu comentario como ejemplo:

  

Quiero escribir la fórmula "result = if (Payment.id.startsWith (" R "))   ? Payment.percentage * Payment.total: Payment.otherValue "

Con su EL, debería ser posible que establezca oraciones como:

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

Entonces ...

  

¿Guardar eso como una propiedad para el cliente e insertarlo en el método apropiado?

Podrías Es una cadena, puede guardarla como propiedad o atributo.

No mentiré. Es bastante complicado y difícil. Es aún más difícil si las reglas comerciales también son complicadas.

Aquí hay algunas preguntas de SO que pueden ser de su interés:

Nota: ANTLR genera código para Python y Javascript también. Eso puede ayudar a escribir pruebas de concepto sin demasiados gastos generales.

Si encuentra que Antlr es demasiado difícil, puede probar con libs como Expr4J, JEval, Parsii. Estos trabajos con un mayor nivel de abstracción.

    
respondido por el Laiv 23.12.2016 - 00:02
1

Al menos puede externalizar el algoritmo para que no sea necesario cambiar la clase de Cliente cuando se agrega un nuevo cliente utilizando un patrón de diseño denominado Patrón de Estrategia (está en la Banda de Cuatro).

Desde el fragmento que dio, es discutible si el patrón de estrategia sería menos mantenible o más mantenible, pero al menos eliminaría el conocimiento de la clase del Cliente sobre lo que se debe hacer (y eliminaría su caso de cambio).

Un objeto StrategyFactory crearía un puntero (o referencia) StrategyIntf basado en el CustomerID. La fábrica podría devolver una implementación predeterminada para los clientes que no son especiales.

La clase del Cliente solo necesita solicitar a la Fábrica la estrategia correcta y luego llamarla.

Este es un pseudo C ++ muy breve para mostrarte lo que quiero decir.

class Customer
{
public:

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


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

El inconveniente de esta solución es que, para cada nuevo cliente que necesite una lógica especial, deberá crear una nueva implementación de CalculationsStrategyIntf y actualizar la fábrica para devolverla a los clientes adecuados. Esto también requiere compilar. Pero al menos evitaría el creciente código de espagueti en la clase del cliente.

    
respondido por el Matthew James Briggs 23.12.2016 - 18:19
-1

Cree una interfaz con un solo método y use lamdas en cada clase de implementación. O puede anonear la clase para implementar los métodos para diferentes clientes

    
respondido por el Zubair A. 28.12.2016 - 07:39

Lea otras preguntas en las etiquetas