Então, vendemos ao cliente um produto de software B2B.
Na apresentação, ele gostou de tudo, mas durante a implementação verificou-se que algo ainda não se encaixava. É claro que você pode dizer que precisa seguir as "melhores práticas" e se transformar em um produto, e não vice-versa. Isso pode funcionar se você tiver uma marca forte (por exemplo, três letras grandes e puder enviar as três letras pequenas). Caso contrário, eles explicarão rapidamente que o cliente conseguiu tudo graças aos seus processos comerciais exclusivos e vamos mudar melhor seu produto ou ele não funcionará. Há uma opção para recusar e se referir ao fato de que as licenças já foram compradas e não há para onde ir do submarino. Mas em mercados relativamente estreitos, essa estratégia não funcionará por muito tempo.
Nós temos que modificar.
As abordagens
Existem várias abordagens básicas para a adaptação do produto.
Monólito
Quaisquer alterações são feitas diretamente no código-fonte do produto, mas estão incluídas em determinadas opções. Em tais produtos, como regra, existem formas monstruosas com configurações que, para não se confundirem, recebem seus números ou códigos. A desvantagem dessa abordagem é que o código-fonte se transforma em um grande espaguete, no qual existem tantos casos de uso diferentes que se tornam muito longos e caros de manter. Cada opção subseqüente requer mais e mais recursos. O desempenho desse produto também deixa muito a desejar. E se o idioma em que o produto está escrito não suporta práticas modernas como herança e polimorfismo, tudo fica muito triste.
Copiar
O cliente recebe todo o código-fonte do produto com uma licença para modificá-lo. Freqüentemente, esses fornecedores dizem ao cliente que eles não irão adaptar o produto, pois será muito caro (é muito mais lucrativo para um fornecedor vender licenças do que entrar em contato com os serviços). Mas eles têm terceirizados familiares que contratam funcionários relativamente baratos e de alta qualidade em algum lugar de países terceiros que estão prontos para ajudá-los. Também há situações em que as melhorias serão realizadas diretamente pelos especialistas do cliente (se eles tiverem unidades de pessoal). Nesses casos, o código-fonte é tomado como ponto de partida e o código modificado não terá nenhuma conexão com o que era originalmente e viverá sua própria vida. Nesse caso, você pode remover com segurança pelo menos metade do produto original e substituí-lo por sua própria lógica.
Fusão
Esta é uma mistura das duas primeiras abordagens. Mas, nele, o desenvolvedor que corrige o código deve sempre se lembrar: “a fusão está chegando”. Quando uma nova versão do produto de origem é lançada, na maioria dos casos, é necessário mesclar manualmente as alterações no código de origem e no código modificado. O problema é que, em qualquer conflito, será necessário lembrar por que certas alterações foram feitas, e isso pode ser há muito tempo. E se a refatoração de código foi executada no produto original (por exemplo, os blocos de código foram simplesmente reorganizados), a mesclagem será muito demorada.
Modularidade
Logicamente, a abordagem mais correta. O código fonte do produto nesse caso não sofre alterações e são adicionados módulos adicionais que expandem a funcionalidade. No entanto, para implementar esse esquema, o produto deve ter uma arquitetura que permita sua expansão dessa maneira.
Descrição do produto
Mais adiante, com exemplos, mostrarei como expandimos os produtos desenvolvidos com base na plataforma aberta e gratuita da
lsFusion .
Um elemento chave do sistema é o módulo. Um módulo é um arquivo de texto com a extensão
lsf , que contém o
código lsFusion . Em cada módulo, a lógica do domínio (funções, classes, ações) e a lógica de apresentação (formulários, navegador) são declaradas. Módulos, como regra, estão localizados em diretórios, divididos por um princípio lógico. Um produto é uma coleção de módulos que implementam sua funcionalidade e armazenados em um repositório separado.
Os módulos são interdependentes. Um módulo depende de outro se ele usa sua lógica (por exemplo, refere-se a propriedades ou formulários).
Quando um novo cliente aparece, um repositório separado (Git ou Subversion) é lançado para ele, no qual os módulos com as modificações necessárias serão criados. Este repositório define o chamado módulo superior. Quando o servidor inicia, apenas esses módulos serão conectados, dos quais depende direta ou transitivamente através de outros módulos. Isso permite que o cliente use não todas as funcionalidades do produto, mas apenas a parte necessária.
Jenkins cria uma tarefa que combina os módulos do produto e do cliente em um único arquivo jar, que é instalado em um servidor de produção ou teste.
Considere vários casos principais de melhorias que surgem na prática:
Suponha que tenhamos um módulo
Pedido no produto, que descreve a lógica padrão dos pedidos:
O cliente
X deseja adicionar uma porcentagem de desconto e um preço de desconto para a linha do pedido.
Primeiro, um novo módulo
OrderX é criado no repositório do cliente. Em seu cabeçalho, uma dependência é colocada no módulo
Order original:
Neste módulo, declaramos novas propriedades sob as quais campos adicionais serão criados nas tabelas e os adicionamos ao formulário:
Tornamos o preço com desconto indisponível para gravação. Será calculado como um evento separado quando o preço inicial ou a porcentagem de desconto mudar:
Agora você precisa alterar o cálculo do valor na linha do pedido (ele deve levar em conta o preço de desconto recém-criado). Para fazer isso, geralmente criamos certos "pontos de entrada" onde outros módulos podem inserir seu comportamento. Em vez da declaração inicial da propriedade sum no módulo Order, usamos o seguinte:
Nesse caso, o valor da propriedade
sum será coletado em um CASE, onde WHEN pode ser espalhado por diferentes módulos. É garantido que, se o módulo A depender do módulo B, todos os WHEN do módulo B funcionarão depois do WHEN do módulo A. Para calcular corretamente o
valor descontado, a seguinte declaração será adicionada ao módulo
OrderX :
Como resultado, se um desconto for definido, o valor estará sujeito a ele, caso contrário, a expressão original.
Suponha que um cliente queira adicionar uma restrição para que o valor do pedido não exceda um determinado valor especificado. No mesmo módulo
OrderX , declaramos uma propriedade na qual o valor da restrição será armazenado e o adicionamos ao formulário de
opções padrão (você pode criar um formulário separado com as configurações, se desejar):
Em seguida, no mesmo módulo, declaramos o valor do pedido, mostramos no formulário e adicionamos um limite ao seu excesso:
E, finalmente, o cliente pediu para alterar levemente o design do formulário de edição de pedidos: colocar o cabeçalho do pedido à esquerda das linhas com um separador, além de sempre mostrar preços com precisão de dois caracteres. Para fazer isso, o código a seguir é adicionado ao seu módulo, que altera o design gerado padrão do formulário de pedido:
Como resultado, obtemos dois módulos de
Pedido (no produto), nos quais a lógica básica do pedido é implementada, e
OrderX (no cliente), no qual a lógica de desconto necessária é implementada:
Deve-se observar que o módulo
OrderX pode ser chamado de
OrderDiscount e transferido diretamente para o produto. Então, se necessário, será possível que cada cliente conecte facilmente a funcionalidade com descontos.
Isso está longe de todas as possibilidades que a plataforma oferece para expandir a funcionalidade em módulos individuais. Por exemplo, usando herança, você pode implementar modularmente a lógica do
registro .
Se houver alguma alteração no código-fonte do produto que contradiga o código no módulo dependente, um erro será gerado quando o servidor iniciar. Por exemplo, se o formulário de
pedido for
excluído no módulo
Pedido , na inicialização, ocorrerá um erro que o formulário de
pedido não foi encontrado no módulo
PedidoX . Além disso, o erro será destacado no
IDE . Além disso, o IDE tem uma função para procurar todos os erros no projeto, o que permite identificar todos os problemas que ocorreram devido à atualização da versão do produto.
Na prática, temos todos os repositórios (do produto e todos os clientes) conectados a um projeto; portanto, refatoramos o produto com calma, enquanto alteramos a lógica nos módulos do cliente em que ele é usado.
Conclusão
Essa arquitetura micromodular oferece os seguintes benefícios:
- Cada cliente está conectado apenas à funcionalidade de que precisa . A estrutura do banco de dados contém apenas os campos que ele usa. A interface da solução final não contém elementos desnecessários. O servidor e o cliente não realizam eventos e verificações desnecessários.
- Flexibilidade nas alterações na funcionalidade básica . Diretamente no projeto do cliente, você pode fazer alterações em absolutamente qualquer forma do produto, adicionar eventos, novos objetos e propriedades, ações, alterar o design e muito mais.
- A entrega de novas melhorias exigidas pelo cliente é significativamente acelerada . A cada solicitação de alteração, você não precisa pensar em como isso afetará outros clientes. Devido a isso, muitas melhorias podem ser feitas e colocadas em operação o mais rápido possível (geralmente dentro de algumas horas).
- Um esquema mais conveniente para expandir a funcionalidade do produto . Primeiro, qualquer funcionalidade pode ser incluída para um cliente específico que está pronto para experimentá-lo e, em seguida, no caso de uma implementação bem-sucedida, os módulos são completamente transferidos para o repositório do produto.
- Independência da base de código . Como muitas melhorias são fornecidas nos contratos de serviço ao cliente, formalmente, todo o código desenvolvido sob esses contratos pertence ao cliente. Com esse esquema, é garantida uma separação completa do código do produto que pertence ao fornecedor do código pertencente ao cliente. Mediante solicitação, transferimos o repositório para o servidor do cliente, onde ele pode modificar a funcionalidade necessária com seus próprios desenvolvedores. Além disso, se o fornecedor licenciar módulos de produtos individuais, o cliente não terá o código-fonte dos módulos para os quais não há licença. Portanto, ele não tem capacidade técnica para conectá-los independentemente, violando as condições de licenciamento.
O esquema de modularidade descrito acima com a ajuda de extensões na programação é geralmente chamado de
mix in . Por exemplo, o Microsoft Dynamics introduziu recentemente o conceito de extensão, que também permite expandir os módulos base. No entanto, é necessária muita programação de nível inferior, o que, por sua vez, requer qualificações mais altas dos desenvolvedores. Além disso, diferentemente do lsFusion, a expansão de eventos e restrições requer os "pontos de entrada" iniciais para o produto para tirar proveito disso.
No momento, de acordo com o esquema descrito acima, apoiamos e implementamos um
sistema ERP para varejo com mais de 30 clientes relativamente grandes, que consiste em mais de 1000 módulos. Entre os clientes, existem redes de FMCG, além de farmácias, lojas de roupas, cadeias de drogarias, atacadistas e outros. No produto, respectivamente, existem categorias separadas de módulos conectados, dependendo do setor e dos processos de negócios utilizados.