Como modificamos o produto para um cliente específico

imagem

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:

Módulo de pedidos
MODULE Order;

CLASS Book '' ;
name '' = DATA ISTRING [ 100 ] (Book) IN id;

CLASS Order '' ;
date '' = DATA DATE (Order) IN id;
number '' = DATA STRING [ 10 ] (Order) IN id;

CLASS OrderDetail ' ' ;
order '' = DATA Order (OrderDetail) NONULL DELETE ;

book '' = DATA Book (OrderDetail) NONULL ;
nameBook '' (OrderDetail d) = name(book(d));

quantity '' = DATA INTEGER (OrderDetail);
price '' = DATA NUMERIC [ 14 , 2 ] (OrderDetail);
sum '' (OrderDetail d) = quantity(d) * price(d);

FORM order ''
OBJECTS o = Order PANEL
PROPERTIES (o) date, number

OBJECTS d = OrderDetail
PROPERTIES (d) nameBook, quantity, price, NEW , DELETE
FILTERS order(d) = o

EDIT Order OBJECT o
;

FORM orders ''
OBJECTS o = Order
PROPERTIES (o) READONLY date, number
PROPERTIES (o) NEWSESSION NEW , EDIT , DELETE
;

NAVIGATOR {
NEW orders;
}

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:
REQUIRE Order;

Neste módulo, declaramos novas propriedades sob as quais campos adicionais serão criados nas tabelas e os adicionamos ao formulário:
discount ', %' = DATA NUMERIC [ 5 , 2 ] (OrderDetail);
discountPrice ' ' = DATA NUMERIC [ 14 , 2 ] (OrderDetail);

EXTEND FORM order
PROPERTIES (d) AFTER price(d) discount, discountPrice READONLY
;

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:
WHEN LOCAL CHANGED (price(OrderDetail d)) OR CHANGED (discount(d)) DO
discountPrice(d) <- price(d) * ( 100 (-) discount(d)) / 100 ;

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:
sum '' = ABSTRACT CASE NUMERIC [ 16 , 2 ] (OrderDetail);
sum (OrderDetail d) += WHEN price(d) THEN quantity(d) * price(d);

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 :
sum(OrderDetail d) += WHEN discount(d) THEN quantity(d) * discountPrice(d);

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):
orderLimit ' ' = DATA NUMERIC [ 16 , 2 ] ();
EXTEND FORM options
PROPERTIES () orderLimit
;

Em seguida, no mesmo módulo, declaramos o valor do pedido, mostramos no formulário e adicionamos um limite ao seu excesso:
sum '' (Order o) = GROUP SUM sum(OrderDetail d) IF order(d) = o;
EXTEND FORM order
PROPERTIES (o) sum
;
CONSTRAINT sum(Order o) > orderLimit() MESSAGE ' ' ;

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:
DESIGN order {
OBJECTS {
NEW pane {
fill = 1 ;
type = SPLITH ;
MOVE BOX (o);
MOVE BOX (d) {
PROPERTY (price(d)) { pattern = '#,##0.00' ; }
PROPERTY (discountPrice(d)) { pattern = '#,##0.00' ; }
}
}
}
}
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:

Encomendar
MODULE Order;

CLASS Book '' ;
name '' = DATA ISTRING [ 100 ] (Book) IN id;

CLASS Order '' ;
date '' = DATA DATE (Order) IN id;
number '' = DATA STRING [ 10 ] (Order) IN id;

CLASS OrderDetail ' ' ;
order '' = DATA Order (OrderDetail) NONULL DELETE ;

book '' = DATA Book (OrderDetail) NONULL ;
nameBook '' (OrderDetail d) = name(book(d));

quantity '' = DATA INTEGER (OrderDetail);
price '' = DATA NUMERIC [ 14 , 2 ] (OrderDetail);
sum '' = ABSTRACT CASE NUMERIC [ 16 , 2 ] (OrderDetail);
sum (OrderDetail d) += WHEN price(d) THEN quantity(d) * price(d);

FORM order ''
OBJECTS o = Order PANEL
PROPERTIES (o) date, number

OBJECTS d = OrderDetail
PROPERTIES (d) nameBook, quantity, price, NEW , DELETE
FILTERS order(d) = o

EDIT Order OBJECT o
;

FORM orders ''
OBJECTS o = Order
PROPERTIES (o) READONLY date, number
PROPERTIES (o) NEWSESSION NEW , EDIT , DELETE
;

NAVIGATOR {
NEW orders;
}

Orderx
MODULE OrderX;

REQUIRE Order;

discount ', %' = DATA NUMERIC [ 5 , 2 ] (OrderDetail);
discountPrice ' ' = DATA NUMERIC [ 14 , 2 ] (OrderDetail);

EXTEND FORM order
PROPERTIES (d) AFTER price(d) discount, discountPrice READONLY
;

WHEN LOCAL CHANGED (price(OrderDetail d)) OR CHANGED (discount(d)) DO
discountPrice(d) <- price(d) * ( 100 (-) discount(d)) / 100 ;

sum(OrderDetail d) += WHEN discount(d) THEN quantity(d) * discountPrice(d);

orderLimit ' ' = DATA NUMERIC [ 16 , 2 ] ();
EXTEND FORM options
PROPERTIES () orderLimit
;

sum '' (Order o) = GROUP SUM sum(OrderDetail d) IF order(d) = o;
EXTEND FORM order
PROPERTIES (o) sum
;
CONSTRAINT sum(Order o) > orderLimit() MESSAGE ' ' ;

DESIGN order {
OBJECTS {
NEW pane {
fill = 1 ;
type = SPLITH ;
MOVE BOX (o);
MOVE BOX (d) {
PROPERTY (price(d)) { pattern = '#,##0.00' ; }
PROPERTY (discountPrice(d)) { pattern = '#,##0.00' ; }
}
}
}
}

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.

Source: https://habr.com/ru/post/pt461983/


All Articles