Design adequado para uma classe com um método que pode variar entre os clientes

12

Eu tenho uma classe usada para processar pagamentos de clientes. Todos os métodos desta classe, exceto um, são os mesmos para todos os clientes, exceto um que calcula (por exemplo) quanto o usuário do cliente deve. Isso pode variar muito de cliente para cliente e não há uma maneira fácil de capturar a lógica dos cálculos em algo como um arquivo de propriedades, pois pode haver vários fatores personalizados.

Eu poderia escrever um código feio que alterna com base no 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
}

mas isso requer a reconstrução da classe toda vez que obtemos um novo cliente. Qual é a melhor maneira?

[Editar] O artigo "duplicado" é completamente diferente. Não estou perguntando como evitar uma declaração de troca, estou pedindo o design moderno que melhor se aplica a este caso - que eu poderia resolver com uma declaração de troca se quisesse escrever código de dinossauro. Os exemplos fornecidos são genéricos e não são úteis, pois dizem essencialmente "Ei, a opção funciona muito bem em alguns casos, não em outros".


[Editar] Decidi ir com a resposta mais bem classificada (criar uma classe "Cliente" separada para cada cliente que implemente uma interface padrão) pelos seguintes motivos:

  1. Consistência: posso criar uma interface que garanta que todas as classes de clientes recebam e retornem a mesma saída, mesmo se criadas por outro desenvolvedor

  2. Manutenção: Todo o código é escrito na mesma linguagem (Java), portanto não é necessário que ninguém mais aprenda uma linguagem de codificação separada para manter o que deveria ser um recurso simples.

  3. Reutilização: caso um problema semelhante apareça no código, eu posso reutilizar a classe Customer para armazenar qualquer número de métodos para implementar a lógica "personalizada".

  4. Familiaridade: eu já sei como fazer isso, para que eu possa fazer isso rapidamente e passar para outras questões mais prementes.

Desvantagens:

  1. Cada novo cliente requer uma compilação da nova classe Customer, o que pode adicionar alguma complexidade à maneira como compilamos e implantamos as alterações.

  2. Cada novo cliente deve ser adicionado por um desenvolvedor - uma pessoa de suporte não pode simplesmente adicionar a lógica a algo como um arquivo de propriedades. Isso não é o ideal ... mas também não tinha certeza de como uma pessoa de Suporte seria capaz de escrever a lógica comercial necessária, especialmente se ela for complexa com muitas exceções (como é provável).

  3. Não será bem dimensionado se adicionarmos muitos, muitos novos clientes. Isso não é esperado, mas se isso acontecer, teremos que repensar muitas outras partes do código, bem como esta.

Para os interessados, você pode usar o Java Reflection para chamar uma classe pelo 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);
Andrew
fonte
1
Talvez você deva elaborar o tipo de cálculo. É apenas um desconto sendo calculado ou um fluxo de trabalho diferente?
Qwerty_so
@ThomasKilian É apenas um exemplo. Imagine um objeto Pagamento, e os cálculos podem ser algo como "multiplique a porcentagem Payment.otal pelo Payment.total - mas não se Payment.id começar com 'R'". Esse tipo de granularidade. Todo cliente tem suas próprias regras.
21816 Andrew Andrew
Possível duplicata das declarações
amigos estão
Você já tentou algo parecido com isso . Definir fórmulas diferentes para cada cliente.
LAIV
1
Os plug-ins sugeridos de várias respostas só funcionarão se você tiver um produto dedicado por cliente. Se você tiver uma solução de servidor em que verifica dinamicamente a identificação do cliente, ela não funcionará. Então, qual é o seu ambiente?
qwerty_so

Respostas:

14

Eu tenho uma classe usada para processar pagamentos de clientes. Todos os métodos desta classe, exceto um, são os mesmos para todos os clientes, exceto um que calcula (por exemplo) quanto o usuário do cliente deve.

Duas opções me vêm à mente.

Opção 1: torne sua classe uma classe abstrata, onde o método que varia entre os clientes é um método abstrato. Em seguida, crie uma subclasse para cada cliente.

Opção 2: Crie uma Customerclasse ou uma ICustomerinterface contendo toda a lógica dependente do cliente. Em vez de sua classe de processamento de pagamentos aceitar um ID de cliente, aceite um Customerou ICustomerobjeto. Sempre que precisa fazer algo dependente do cliente, chama o método apropriado.

Tanner Swett
fonte
A classe Customer não é uma má ideia. Terá de haver algum tipo de objeto personalizado para cada Cliente, mas não fiquei claro se esse deve ser um registro de banco de dados, um arquivo de propriedades (de algum tipo) ou outra classe.
22716 Andrew
10

Você pode querer escrever os cálculos personalizados como "plug-ins" para seu aplicativo. Em seguida, você usaria um arquivo de configuração para informar a você qual plugin de cálculo deve ser usado para qual cliente. Dessa forma, seu aplicativo principal não precisaria ser recompilado para cada novo cliente - ele só precisa ler (ou reler) um arquivo de configuração e carregar novos plug-ins.

FrustratedWithFormsDesigner
fonte
Obrigado. Como um plug-in funcionaria, por exemplo, em um ambiente Java? Por exemplo, quero escrever a fórmula "result = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue" salve isso como uma propriedade para o cliente e insira-o no método apropriado?
22416 Andrew
@ Andrew, Uma maneira de um plug-in funcionar é usar a reflexão para carregar uma classe pelo nome. O arquivo de configuração listaria os nomes das classes para os clientes. Obviamente, você teria que encontrar uma maneira de identificar qual plug-in é para qual cliente (por exemplo, pelo nome ou por alguns metadados armazenados em algum lugar).
30416 Kat
@ Kat Obrigado, se você ler minha pergunta editada, é assim que eu estou fazendo. A desvantagem é que um desenvolvedor precisa criar uma nova classe que limite a escalabilidade - eu preferiria que uma pessoa de suporte pudesse editar um arquivo de propriedades, mas acho que há muita complexidade.
Andrew
@ Andrew: Você poderá escrever suas fórmulas em um arquivo de propriedades, mas precisará de algum tipo de analisador de expressão. E um dia você pode encontrar uma fórmula muito complexa para (facilmente) escrever como uma expressão de uma linha em um arquivo de propriedades. Se você realmente deseja que os não programadores possam trabalhar com eles, precisará de algum tipo de editor de expressões amigável para eles usarem, o que irá gerar código para o seu aplicativo. Isso pode ser feito (eu já vi), mas não é trivial.
FrustratedWithFormsDesigner
4

Eu usaria uma regra definida para descrever os cálculos. Isso pode ser mantido em qualquer armazenamento de persistência e modificado dinamicamente.

Como alternativa, considere isso:

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

Onde foi customerOpsIndexcalculado o índice de operação correto (você sabe qual cliente precisa de qual tratamento).

qwerty_so
fonte
Se eu pudesse criar um conjunto de regras abrangente, o faria. Mas cada novo cliente pode ter seu próprio conjunto de regras. Também eu preferiria que tudo estivesse contido no código, se houver um padrão de design para fazer isso. Meu exemplo de switch {} faz isso muito bem, mas não é muito flexível.
Andrew
Você pode armazenar as operações a serem chamadas para um cliente em uma matriz e usar o ID do cliente para indexá-lo.
precisa saber é o seguinte
Isso parece mais promissor. :)
Andrew
3

Algo como o abaixo:

observe que você ainda tem a instrução switch no repositório. No entanto, não há como contornar isso, em algum momento você precisa mapear um ID de cliente para a lógica necessária. Você pode ser inteligente e movê-lo para um arquivo de configuração, colocar o mapeamento em um dicionário ou carregar dinamicamente nos conjuntos lógicos, mas basicamente se resume a alternar.

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

Parece que você tem um mapeamento de 1 para 1 dos clientes para o código personalizado e, como você está usando uma linguagem compilada, precisa reconstruir o sistema sempre que recebe um novo cliente

Tente uma linguagem de script incorporada.

Por exemplo, se seu sistema estiver em Java, você poderá incorporar o JRuby e, em seguida, para cada cliente, armazenar um snippet correspondente do código Ruby. Idealmente, em algum lugar sob controle de versão, no mesmo ou em um repositório Git separado. E avalie esse trecho no contexto do seu aplicativo Java. O JRuby pode chamar qualquer função Java e acessar qualquer 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 é um padrão muito comum. Por exemplo, muitos jogos de computador são escritos em C ++, mas usam scripts Lua incorporados para definir o comportamento do cliente de cada oponente no jogo.

Por outro lado, se você tiver um mapeamento de 1 para 1 dos clientes para o código personalizado, use o padrão "Estratégia", como já sugerido.

Se o mapeamento não se basear na identificação do usuário, adicione uma matchfunção a cada objeto de estratégia e faça uma escolha ordenada de qual estratégia usar.

Aqui está algum pseudo-código

strategy = strategies.find { |strategy| strategy.match(customer) }
strategy.apply(customer, ...)
akuhn
fonte
2
Eu evitaria essa abordagem. A tendência é colocar todos esses trechos de script em um banco de dados e, em seguida, você perde o controle de origem, a versão e o teste.
Ewan
Justo. É melhor armazená-los em um repositório Git separado ou em qualquer outro lugar. Atualizei minha resposta para dizer que idealmente os snippets devem estar sob controle de versão.
akuhn
Eles poderiam ser armazenados em algo como um arquivo de propriedades? Estou tentando evitar que propriedades fixas do Cliente existam em mais de um local, caso contrário, é fácil transformar-se em espaguete não sustentável.
22416 Andrew
2

Eu vou nadar contra a corrente.

Eu tentaria implementar minha própria linguagem de expressão com o ANTLR .

Até o momento, todas as respostas são fortemente baseadas na personalização do código. A implementação de classes concretas para cada cliente parece-me que, em algum momento no futuro, não será bem dimensionado. A manutenção vai ser cara e dolorosa.

Então, com o Antlr, a idéia é definir seu próprio idioma. Que você pode permitir que usuários (ou desenvolvedores) escrevam regras de negócios nesse idioma.

Tomando seu comentário como exemplo:

Quero escrever a fórmula "result = if (Payment.id.startsWith (" R "))? Payment.percentage * Payment.total: Payment.otherValue"

Com o seu EL, deve ser possível indicar frases como:

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

Então...

salvar isso como uma propriedade para o cliente e inseri-lo no método apropriado?

Você poderia. É uma string, você pode salvá-la como propriedade ou atributo.

Eu não vou mentir. É bem complicado e difícil. É ainda mais difícil se as regras de negócios também forem complicadas.

Aqui estão algumas perguntas de SO que podem ser do seu interesse:


Nota: O ANTLR também gera código para Python e Javascript. Isso pode ajudar a escrever provas de conceito sem muita sobrecarga.

Se você achar que o Antlr é muito difícil, tente com bibliotecas como Expr4J, JEval, Parsii. Isso funciona com um nível mais alto de abstração.

Laiv
fonte
É uma boa idéia, mas uma dor de cabeça de manutenção para o próximo cara na fila. Mas não vou descartar nenhuma idéia até avaliar e pesar todas as variáveis.
22416 Andrew
1
As muitas opções que você tem, melhor. Eu só queria dar mais uma
Laiv
Não há como negar que essa é uma abordagem válida, basta olhar para o websphere ou outros sistemas corporativos que 'os usuários finais podem personalizar'. / source control / change control
Ewan
@ Ewan desculpe, tenho medo de não ter entendido seus argumentos. Os descritores de linguagem e as classes geradas automaticamente podem fazer parte ou não do projeto. Mas, de qualquer forma, não vejo por que perdi o controle de versão / código-fonte. Por outro lado, pode parecer que existem 2 idiomas diferentes, mas isso é temporário. Uma vez terminado o idioma, você define apenas as strings. Como expressões de Cron. A longo prazo, há muito menos coisas com que se preocupar.
LAIV
"Se paymentID começar com 'R', então (paymentPercentage / paymentTotal) else paymentOther", onde você armazena isso? que versão é essa? em que idioma está escrito? que testes de unidade você tem para isso?
Ewan
1

Você pode pelo menos externalizar o algoritmo para que a classe Customer não precise ser alterada quando um novo cliente for adicionado usando um padrão de design chamado Padrão de Estratégia (é o Gang of Four).

A partir do snippet que você forneceu, é discutível se o padrão de estratégia seria menos sustentável ou mais sustentável, mas pelo menos eliminaria o conhecimento da classe Customer sobre o que precisa ser feito (e eliminaria seu caso de troca).

Um objeto StrategyFactory criaria um ponteiro StrategyIntf (ou referência) com base no CustomerID. O Factory pode retornar uma implementação padrão para clientes que não são especiais.

A classe Customer precisa apenas pedir à fábrica a estratégia correta e depois chamá-la.

Este é um pseudo C ++ muito breve para mostrar o que quero dizer.

class Customer
{
public:

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


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

A desvantagem dessa solução é que, para cada novo cliente que precise de lógica especial, você precisará criar uma nova implementação do CalculationsStrategyIntf e atualizar a fábrica para devolvê-la aos clientes apropriados. Isso também requer compilação. Mas você pelo menos evitaria o crescente código de espaguete na classe do cliente.

Matthew James Briggs
fonte
Sim, acho que alguém mencionou um padrão semelhante em outra resposta, para cada cliente encapsular a lógica em uma classe separada que implementa a interface do Cliente e depois chamar essa classe da classe PaymentProcessor. É promissor, mas significa que toda vez que contratamos um novo Cliente, precisamos escrever uma nova aula - não é realmente um grande negócio, mas não é algo que uma pessoa de Suporte possa fazer. Ainda é uma opção.
23416 Andrew Andrew
-1

Crie uma interface com método único e use lamdas em cada classe de implementação. Ou você pode classe anônima para implementar os métodos para diferentes clientes

Zubair A.
fonte