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

12

Tengo una clase utilizada para procesar pagos de 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 varios factores personalizados.

Podría escribir código feo que cambie 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 tenemos 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 aplica 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 "Cliente" separada para cada cliente que implemente una interfaz estándar) por las siguientes razones:

  1. Consistencia: puedo crear una interfaz que garantice que todas las clases de Clientes reciban y devuelvan el mismo resultado, incluso si fueron creadas por otro desarrollador

  2. Mantenibilidad: 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 característica simple.

  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 hacer esto, así que puedo hacerlo rápidamente y pasar a otros problemas más urgentes.

Inconvenientes:

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

  2. Un desarrollador debe agregar a cada nuevo cliente; una persona de soporte no puede simplemente agregar la lógica a algo así 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 escalará bien si agregamos muchos, muchos clientes nuevos. Esto no se espera, pero si sucede, tendremos que repensar muchas otras partes del código además de esta.

Para aquellos de ustedes interesados, pueden 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);
Andrés
fuente
1
Tal vez deberías detallar el tipo de cálculos. ¿Es solo un reembolso que se calcula o es un flujo de trabajo diferente?
qwerty_so
@ThomasKilian Es solo un ejemplo. Imagine un objeto de Pago, y los cálculos pueden ser algo así como "multiplique el porcentaje de Pago por el total de Pago, pero no si Pago.id comienza con 'R'". Ese tipo de granularidad. Cada cliente tiene sus propias reglas.
Andrew
¿Has probado algo como esto ? Establecer diferentes fórmulas para cada cliente.
Laiv
1
Los complementos sugeridos de varias respuestas solo funcionarán si tiene un producto dedicado por cliente. Si tiene una solución de servidor donde verifica la identificación del cliente dinámicamente, no funcionará. Entonces, ¿cuál es tu entorno?
qwerty_so

Respuestas:

14

Tengo una clase utilizada para procesar pagos de 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 me vienen a la mente.

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

Opción 2: crear una Customerclase o una ICustomerinterfaz que contenga toda la lógica dependiente del cliente. En lugar de tener la clase de procesamiento de pago acepta un ID de cliente, haga que aceptar una Customero ICustomerobjeto. Siempre que necesita hacer algo dependiente del cliente, llama al método apropiado.

Tanner Swett
fuente
La clase de cliente no es una mala idea. Tendrá que haber algún tipo de objeto personalizado para cada Cliente, pero no estaba claro si debería ser un registro DB, un archivo de propiedades (de algún tipo) u otra clase.
Andrew
10

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

FrustratedWithFormsDesigner
fuente
Gracias. ¿Cómo funcionaría un complemento en, por ejemplo, un entorno Java? Por ejemplo, quiero escribir la fórmula "resultado = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue" guardar eso como una propiedad para el cliente e insertarlo en el método apropiado?
Andrew
@ Andrew, una forma en que un complemento puede funcionar es usar la reflexión para cargar en una clase por su nombre. El archivo de configuración enumeraría los nombres de las clases para los clientes. Por supuesto, tendría que tener alguna forma de identificar qué complemento es para qué cliente (por ejemplo, por su nombre o algunos metadatos almacenados en algún lugar).
Kat
@Kat Gracias, si lees mi pregunta editada, así es como lo estoy haciendo. El inconveniente es que un desarrollador tiene que crear una nueva clase que limite la escalabilidad: preferiría que una persona de soporte solo pudiera editar un archivo de propiedades, pero creo que hay demasiada complejidad.
Andrew
@ Andrew: Es posible que pueda escribir sus fórmulas en un archivo de propiedades, pero luego necesitaría algún tipo de analizador de expresiones. Y es posible que algún día te encuentres con una fórmula que sea demasiado compleja para (fácilmente) escribir como una expresión de una línea en un archivo de propiedades. Si realmente desea que los que no son programadores puedan trabajar en estos, necesitará algún tipo de editor de expresiones fácil de usar para ellos que genere código para su aplicación. Esto se puede hacer (lo he visto), pero no es trivial.
FrustratedWithFormsDesigner
4

Iría con un conjunto de reglas para describir los cálculos. Esto puede mantenerse en cualquier tienda 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 se customerOpsIndexcalculó el índice de operación correcto (usted sabe qué cliente necesita qué tratamiento).

qwerty_so
fuente
Si pudiera crear un conjunto completo de reglas, lo haría. Pero cada nuevo cliente puede tener su propio conjunto de reglas. También preferiría que todo esté contenido en el código, si hay un patrón de diseño para hacer esto. Mi ejemplo de switch {} lo hace bastante bien, pero no es muy flexible.
Andrew
Puede almacenar las operaciones que se llamarán para un cliente en una matriz y usar el ID de cliente para indexarlo.
qwerty_so
Eso parece más prometedor. :)
Andrew
3

Algo como lo siguiente:

observe que todavía tiene la declaración de cambio en el repositorio. Sin embargo, no hay forma de evitar esto, en algún momento debe asignar una identificación 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 ensamblados lógicos, pero esencialmente se reduce a cambiar.

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

Parece que tiene un mapeo 1 a 1 de clientes a código personalizado y debido a que está utilizando un lenguaje compilado, debe reconstruir su sistema cada vez que obtiene un nuevo cliente

Pruebe con un lenguaje de script incrustado.

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

Por otro lado, si tiene una asignación de muchos a 1 de los clientes al 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 matchfunción a cada objeto de estrategia y elija una estrategia ordenada de qué estrategia utilizar.

Aquí hay un pseudocódigo

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)
akuhn
fuente
2
Evitaría este enfoque. La tendencia es poner todos esos fragmentos de script en una base de datos y luego perder el control de origen, el control de versiones y las pruebas.
Ewan
Lo suficientemente justo. Lo mejor es almacenarlos en un repositorio de git separado o donde sea. Actualicé mi respuesta para decir que los fragmentos deberían estar idealmente bajo control de versión.
akuhn
¿Podrían almacenarse en algo así como un archivo de propiedades? Estoy tratando de evitar que las propiedades fijas del Cliente existan en más de una ubicación, de lo contrario es fácil convertirlas en espaguetis imposibles de mantener.
Andrew
2

Voy a nadar contra la corriente.

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

Hasta ahora, todas las respuestas están fuertemente basadas 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 tu propio idioma. Que puede permitir que los usuarios (o desarrolladores) escriban reglas comerciales en dicho idioma.

Tomando tu comentario como ejemplo:

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

Con su EL, debería ser posible para usted enunciar oraciones como:

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

Luego...

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

Tú 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 de negocio también son complicadas.

Aquí algunas preguntas 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 bibliotecas como Expr4J, JEval, Parsii. Estos trabajos con un mayor nivel de abstracción.

Laiv
fuente
Es una buena idea, pero un dolor de cabeza de mantenimiento para el próximo tipo en el futuro. Pero no voy a descartar ninguna idea hasta que haya evaluado y evaluado todas las variables.
Andrew
1
Las muchas opciones que tienes son mejores. Solo quería dar uno más
Laiv
No se puede negar que este es un enfoque válido, solo mire la webphere u otros sistemas empresariales listos para usar que 'los usuarios finales pueden personalizar' Pero como la respuesta de @akuhn, la desventaja es que ahora está programando en 2 idiomas y pierde su versión / control de fuente / control de cambios
Ewan
@Ewan lo siento, me temo que no entendí tus argumentos. El lenguaje descriptivo y las clases autogeneradas pueden ser parte o no del proyecto. Pero, en cualquier caso, no veo por qué podría perder el control de versiones / gestión del código fuente. Por otro lado, puede parecer que hay 2 idiomas diferentes, pero eso es temporal. Una vez que el idioma está listo, solo establece cadenas. Como las expresiones de Cron. A la larga, hay mucho menos de qué preocuparse.
Laiv
"Si el ID de pago comienza con 'R', entonces (porcentaje de pago / Total de pago) más pago Otro" ¿dónde almacena esto? que versión es ¿En qué idioma está escrito? ¿Qué pruebas unitarias tienes para ello?
Ewan
1

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

Desde el fragmento que proporcionó, 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 debe hacerse (y eliminaría su caso de cambio).

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

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

Este es un breve pseudo C ++ para mostrarle 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 necesita una lógica especial, necesitaría crear una nueva implementación de CalculationsStrategyIntf y actualizar la fábrica para devolverla a los clientes apropiados. Esto también requiere compilación. Pero al menos evitaría el código de espagueti cada vez mayor en la clase de clientes.

Matthew James Briggs
fuente
Sí, creo que alguien mencionó un patrón similar en otra respuesta, para que cada cliente encapsule la lógica en una clase separada que implemente la interfaz del Cliente y luego llame a esa clase desde la clase PaymentProcessor. Es prometedor, pero significa que cada vez que traemos un nuevo Cliente tenemos que escribir una nueva clase, no es realmente un gran problema, pero no es algo que una persona de Soporte pueda hacer. Aún así es una opción.
Andrew
-1

Cree una interfaz con un método único y use lamdas en cada clase de implementación. O puede usar una clase anónima para implementar los métodos para diferentes clientes

Zubair A.
fuente