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:
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:
In this module, we declare new properties under which additional fields will be created in the tables, and add them to the form:
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:
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:
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:
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):
Then, in the same module, we declare the amount of the order, show it on the form and add a limit on its excess:
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:
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:
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:
- Each customer is connected only the functionality he needs . The structure of his database contains only those fields that he uses. The interface of the final solution does not contain unnecessary elements. The server and client do not perform unnecessary events and checks.
- Flexibility in changes to the basic functionality . Directly in the client’s project, you can make changes to absolutely any form of the product, add events, new objects and properties, actions, change the design and much more.
- The delivery of new improvements required by the customer is significantly accelerated . With each change request, you don’t need to think about how it will affect other customers. Due to this, many improvements can be made and put into operation as soon as possible (often within a few hours).
- A more convenient scheme for expanding the functionality of the product . First, any functionality can be included for a specific customer who is ready to try it, and then, in the event of a successful implementation, the modules are completely transferred to the product repository.
- Code base independence . Since many improvements are provided under customer service agreements, formally, the entire code developed under these agreements belongs to the customer. With this scheme, a complete separation of the product code that belongs to the vendor from the code owned by the client is ensured. Upon request, we transfer the repository to the client’s server, where he can use his own developers to modify the functionality he needs. In addition, if the supplier carries out licensing for individual product modules, the customer does not have the source code of modules for which there is no license. Thus, he does not have the technical ability to connect them independently in violation of the licensing conditions.
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.