Estratégias para versionamento customizado por cliente

Caros,

Temos um sistema base e 10 clientes diferentes. Para cada cliente, são necessárias algumas alterações no sistema base: alterações na interface, utilização de recursos, entre outros. Atualmente, fazemos este controle manualmente.

Vocês já se depararam com esta situação? Qual a melhor estratégia?

Você quer saber qual é a melhor estratégia, a solução BOA mesmo?

Procure melhorar a arquitetura para ter um único código fonte.
Seria mais ou menos assim: as partes customizáveis implementadas como se fossem módulos “plugáveis”, mas tudo fazendo parte do mesmo projeto.

Vou colocar um exemplo aqui, eu sei que no sistema real as coisas devem ser bem mais complexas, mas é só para pegar a idéia:

1- Partes diferenciadas no código-fonte:

Imaginemos um método que calcula os acréscimos para um pagamento atrasado.

BigDecimal calcularValorPagamento(Pagamento p, Date dataPagamento) {
      int diasAtraso = calcularDiferencaDias(dataPagamento, p.getDataVencimento());
      BigDecimal taxaDiariaAtraso = dao.consultarTaxaAtraso();
      BigDecimal valor = p.getValorPrincipal() + dao.consultarMultaAtraso() + diasAtraso*taxaDiaraAtraso;
      return valor;
}

Mas o cliente XXX funciona um pouco diferente: a multa está sempre variando, e deve ser consultada em um webservice. A taxa tambem vem desse webservice, e é por mês de atraso e não por dia.
Aí você cria uma versão diferenciada para este cliente:

BigDecimal calcularValorPagamento(Pagamento p, Date dataPagamento) {
      int mesesAtraso = calcularDiferencaMeses(dataPagamento, p.getDataVencimento());
      WebServiceResponse response = webserviceClient.consultarTaxasEMultas();
      BigDecimal valor = p.getValorPrincipal() + response.getMulta() + meses*response.getTaxaMensal();
      return valor;
}

Essa é uma das classes que usam essa rotina:

class PagamentoFatura {
    void confirmarPagamento() {
        // ....
        BigDecimal valorAPagar = calcularValorPagamento(pagamento, new Date());
        if (valorAPagar != valorPago) {
             // ....
        }
        // ....
    }
}

Você substituiria por uma estratégia diferenciada para cada cliente:

// Interface para estratégia de cálculo
interface CalculadorPagamento {
    BigDecimal calcularValorPagamento(Pagamento p, Date dataPagamento);
}

// Implementação básica
class CalculadorPagamentoBase {
    BigDecimal calcularValorPagamento(Pagamento p, Date dataPagamento) {
      int diasAtraso = calcularDiferencaDias(dataPagamento, p.getDataVencimento());
      BigDecimal taxaDiariaAtraso = dao.consultarTaxaAtraso();
      BigDecimal valor = p.getValorPrincipal() + dao.consultarMultaAtraso() + diasAtraso*taxaDiaraAtraso;
      return valor;
    }
}

// Implementação com lógica específica do cliente 
class CalculadorPagamentoBufunfaBank {
    BigDecimal calcularValorPagamento(Pagamento p, Date dataPagamento) {
      int mesesAtraso = calcularDiferencaMeses(dataPagamento, p.getDataVencimento());
      WebServiceResponse response = webserviceClient.consultarTaxasEMultas();
      BigDecimal valor = p.getValorPrincipal() + response.getMulta() + meses*response.getTaxaMensal();
      return valor;
    }
}

// A classe que conhece qual estratégia usar para cada cliente.
// Devolve a instância correta dependendo da empresa que está utilizando.
class CalculadorPagamentoFactory {
   static CalculadorPagamento getCalculadorPagamento(Empresa empresa) {
       if (empresa.getId().equals(ID_BUFUNFA_BANK)) {
           return new CalculadorPagamentoBufunfaBank();
       } else {
           return new CalculadorPagamentoBase();
       }
   }
}

// Classe utilizadora (alterada para usar a estratégia plugável)
class PagamentoFatura {
    void confirmarPagamento() {
        // ....
        CalculadorPagamento calculador = CalculadorPagamentoFactory.getCalculadorPagamento(empresa);
        BigDecimal valorAPagar = calculador.calcularValorPagamento(pagamento, new Date());
        if (valorAPagar != valorPago) {
             // ....
        }
        // ....
    }
}

E dessa forma você tem todas as rotinas convivendo no mesmo projeto, mas é capaz de selecionar a execução apropriada em tempo de execução.
Como eu disse, é só um rascunho pra tentar transmitir a idéia.
Procure sobre alguns design patterns como Strategy, Abstract Factory, eles vão ajudar a pensar em como fazer.

2- Interface Gráfica e recursos
Você não mencionou se o sistema é desktop ou web… Se for desktop, a interface de usuário é código Java e portanto pode seguir a mesma estratégia usada no restante do código. Se for web, pode usar a substituição em tempo de deploy.

Exemplo:
Seu site tem as páginas, o CSS, as imagens.

A solução mais óbvia seria ter um projeto diferente para guardar as personalizações de cada cliente, sempre na estrutura padrão:

/web
  |
  |_____pages
  |
  |_____resources
            |_______css
            |_______images

Mas você pode guardar as customizações no mesmo projeto, em diretórios diferentes:

/web
  |
  |_____pages
  |
  |_____resources
            |_______css
            |_______images

/layout_bar_do_ze
  |
  |_____resources
            |_______css
            |_______images

/layout_banco_bufunfa
  |
  |_____resources
            |_______css
            |_______images

Nesse caso, o seu script de deploy seria responsável por substituir os arquivos customizados para cada cliente, sobrescrevendo a estrutura principal.
Essa mesma técnica vale para os recursos estáticos de uma aplicação desktop (por exemplo, imagens ou arquivos de configuração)


Conclusão:

Isso pode dar um trabalho enorme, eu não faço idéia da complexidade que pode estar o seu sistema, e nem se ele está razoavelmente modularizado e fácil de manter.
Mas apesar de tudo vale a pena. Substituir partes manualmente para cada cliente a cada release é um custo muito grande, e um risco maior ainda de algo dar errada. Fuja dessa opção como o diabo foge da cruz!

A forma certa é essa mesma que o gomesrod falou. Vc tem 1 produto e N costumizações. Pense nas costumizações como skins.

Para código diferente vc pode usar o Groovy ou outra linguagens de script e usar a Script API para chamar os scripts de dentro do seu código padrão. O código padrão deve descobri se ha um script para ser executado e se não, usa a logica normal do produto padrão.

Para layout vc pode usar o Freemarks ou outra linguagens do tipo. Assim seus skins não dependem de codigo. O codigo tem que esta todo em componentes que depois vc usa. É um conceito demelhante aos themes do wordpress.
Usar Theme é outra boa ideia. Vc controi no produto um conceito de tema. E depois vc deixa o thema ser abilitado. Se o cliente quiser modificações é só criar um tema para ele e mudar numa tela de admi, qual o tema a ser usado. O tema não é apenas o css etc, é o conjunto de páginas.

Se apoie em abstração. Não pense nas modificações como exceções, pense como regra , e inclua o mecanismo no produto principal. O ideal é vc ter um produto que tem uma pasta onde vc larga um jar. O produto lê esse jar e tem todas as modificações lá. os scripts, os temas , etc… e ai o produto se “auto-configura”. Assim vc mantém a versão do produto separado da versão do cliente, que seriam esses jars especiais.

Esta a solução arquitetural do problema.

A solução meia-boca é : faça uma sistema que controla as versões :slight_smile:

No passado trabalhei em projeto que já estava em andamento fazia anos, onde tinha uma tabela de configuração parametrizando as características de acordo com a personalização. A aplicação então jogava isso na sessão e dinamicamente durante o código normal decidia o que usar verificando os parâmetros ligados para o cliente. Sem sofisticação, não era bonito, mas funcionava.

Mas eu particularmente nao seguiria isso, prefiro tratar cada cliente com uma solução totalmente personalizada, mesmo que tenha sido iniciada a partir de uma ideia básica e genérica, pois depois ganha vida própria e decola para lugares diferentes de acordo com cada cliente, evoluindo naturalmente sem complicações técnicas, valorizando mais cada um pagando suas manutencoes conforme necessidade, mesmo que um dia outro cliente por coincidência queira algo similar, vai pagar pelo tempo necessário para adaptar no projeto dele.