How we modify the product for a specific client

image






So, we sold the client a B2B software product.



At the presentation, he liked everything, but during the implementation it turned out that something still did not fit. You can of course say that you need to follow “best practice”, and change yourself into a product, and not vice versa. This may work if you have a strong brand (for example, three large letters, and you can send all three small letters). Otherwise, they will quickly explain to you that the customer achieved everything thanks to his unique business processes, and let's better change your product or it won’t work. There is an option to refuse and refer to the fact that licenses have already been purchased, and there is nowhere to go from the submarine. But in relatively narrow markets, such a strategy will not work for a long time.



We have to modify.



The approaches



There are several basic approaches to product adaptation.



Monolith



Any changes are made directly to the source code of the product, but are included in certain options. In such products, as a rule, there are monstrous forms with settings, which, in order not to get confused, are assigned their numbers or codes. The disadvantage of this approach is that the source code turns into a big spaghetti, in which there are so many different use cases that it becomes very long and expensive to maintain. Each subsequent option requires more and more resources. The performance of such a product also leaves much to be desired. And if the language in which the product is written does not support modern practices like inheritance and polymorphism, then everything becomes very sad.



Copy



The client is given the entire source code of the product with a license to modify it. Often, such vendors tell the customer that they will not adapt the product themselves, since it will be too expensive (it’s much more profitable for a vendor to sell licenses than to contact services). But they have familiar outsourcers who hire relatively inexpensive and high-quality employees somewhere in third countries, ready to help them. There are also situations when improvements will be carried out directly by the customer’s specialists (if they have staffing units). In such cases, the source code is taken as a starting point, and the modified code will have no connection with what it was originally, and will live its own life. In this case, you can safely remove at least half of the original product and replace it with your own logic.



Merger



This is a mixture of the first two approaches. But in it, the developer who corrects the code should always remember: “merge is coming”. When a new version of the source product is released, it will in most cases have to manually merge the changes in the source and modified code. The problem is that in any conflict it will be necessary to remember why certain changes were made, and this could be a very long time ago. And if code refactoring was carried out in the original product (for example, code blocks were simply rearranged), then merging will be very time-consuming.



Modularity



Logically the most correct approach. The source code of the product in this case does not undergo changes, and additional modules are added that expand the functionality. However, in order to implement such a scheme, the product must have an architecture that allows it to be expanded in this way.



Description



Next, with examples, I will show how we expand products developed on the basis of the open and free lsFusion platform.



A key element of the system is the module. A module is a text file with the lsf extension, which contains lsFusion code . In each module, both domain logic (functions, classes, actions) and presentation logic (forms, navigator) are declared. Modules, as a rule, are located in directories, divided by a logical principle. A product is a collection of modules that implement its functionality and stored in a separate repository.



Modules are interdependent. One module depends on another if it uses its logic (for example, refers to properties or forms).



When a new client appears, a separate repository (Git or Subversion) is launched for it, in which modules with the necessary modifications will be created. This so-called top module is defined in this repository. When the server starts, only those modules will be connected, on which it depends directly or transitively through other modules. This allows the client to use not all the functionality of the product, but only the part he needs.

Jenkins creates a task that combines the product and client modules into a single jar file, which is then installed on a production or test server.



Consider several main cases of improvements that arise in practice:



Suppose we have an Order module in the product, which describes the standard order logic:



Order module
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;

}







Customer X wants to add a discount percentage and a discount price for the order line.

First, a new OrderX module is created in the customer repository. Its title puts a dependency on the original Order module:

REQUIRE Order;





In this module, we declare new properties under which additional fields will be created in the tables, and add them to the form:

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

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



EXTEND FORM order

PROPERTIES (d) AFTER price(d) discount, discountPrice READONLY

;







We make the discounted price unavailable for recording. It will be calculated as a separate event when either the initial price or the discount percentage changes:

WHEN LOCAL CHANGED (price(OrderDetail d)) OR CHANGED (discount(d)) DO

discountPrice(d) <- price(d) * ( 100 (-) discount(d)) / 100 ;







Now you need to change the calculation of the amount on the order line (it must take into account our newly created discount price). To do this, we usually create certain “entry points” where other modules can insert their behavior. Instead of the initial declaration of the sum property in the Order module, we use the following:

sum '' = ABSTRACT CASE NUMERIC [ 16 , 2 ] (OrderDetail);

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







In this case, the value of the sum property will be collected in one CASE, where WHEN can be scattered across different modules. It is guaranteed that if module A depends on module B, then all WHENs of module B will work later than the WHENs of module A. To correctly calculate the discounted amount , the following declaration is added to OrderX module:

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







As a result, if a discount is set, the amount will be subject to it, otherwise the original expression.



Suppose a client wants to add a restriction that the order amount should not exceed a certain specified amount. In the same OrderX module, we declare a property in which the constraint value will be stored, and add it to the standard options form (you can create a separate form with settings if you wish):

orderLimit ' ' = DATA NUMERIC [ 16 , 2 ] ();

EXTEND FORM options

PROPERTIES () orderLimit

;







Then, in the same module, we declare the amount of the order, show it on the form and add a limit on its excess:

sum '' (Order o) = GROUP SUM sum(OrderDetail d) IF order(d) = o;

EXTEND FORM order

PROPERTIES (o) sum

;

CONSTRAINT sum(Order o) > orderLimit() MESSAGE ' ' ;







And finally, the client asked me to slightly change the design of the order editing form: to ensure that the order header is to the left of the lines with a separator, and also that the prices always show up to two digits. To do this, the following code is added to its module, which changes the standard generated design of the order form:

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' ; }

}

}

}

}





As a result, we get two Order modules (in the product), in which the basic logic of the order is implemented, and OrderX (at the customer), in which the necessary discount logic is implemented:



Order
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' ; }

}

}

}

}







It should be noted that the OrderX module can be called OrderDiscount and transferred directly to the product. Then, if necessary, it will be possible for each customer to easily connect functionality with discounts.



This is far from all the opportunities that the platform provides for expanding the functionality in individual modules. For example, using inheritance, you can modularly implement register logic.



If there are any changes in the source code of the product that contradict the code in the dependent module, an error will be generated when the server starts. For example, if the order form is deleted in the Order module, then at startup there will be an error that the order form was not found in the OrderX module. Also, the error will be highlighted in the IDE . In addition, the IDE has a function to search for all errors in the project, which allows you to identify all problems that have occurred due to updating the product version.



In practice, we have all the repositories (of the product and all customers) connected to the same project, so we calmly refactor the product, while changing the logic in the customer modules where it is used.



Conclusion



Such micromodular architecture provides the following benefits:





The above described modularity scheme with the help of extensions in programming is most often called mix in . For example, the concept of extension has recently appeared in Microsoft Dynamics, which also allows you to expand the base modules. However, much lower-level programming is required there, which in turn requires higher qualifications of developers. In addition, unlike lsFusion, the expansion of events and restrictions requires the initial “entry points” to the product in order to take advantage of this.



Currently, according to the scheme described above, we support and implement an ERP system for retailing with more than 30 relatively large customers, which consists of more than 1000 modules. Among the customers there are FMCG networks, as well as pharmacies, clothing stores, drogerie chain stores, wholesalers and others. In the product, respectively, there are separate categories of modules that are connected depending on the industry and the business processes used.



All Articles