For any project with a long history, the moment comes once when the code begins to live its own life - there simply is no one left who is well versed in logic and connections. Adding new features is sometimes like a shot at random: it can hit the target, or maybe the audience.
And then he comes, the refactoring of the payment process. But we decided to make the process even more interesting by adding IDEF-0 ideas to refactoring.
The Yandex.Money payment process has evolved since 2002, and its frontend over the years has been overgrown with the work of many generations of developers. He has grown to the point that even a change in the user's balance check algorithm before sending a transfer turned into a journey through a clearing with traps, a journey that the user could not see but was fascinating under the hood. In the article we will touch the server part of the frontend.
In addition to difficulties with support, it was difficult to introduce new developers, and this is a big minus for the company, where engineers regularly migrate between projects. Therefore, it was decided to conduct deep code refactoring. Given the amount of work, this meant writing the process again.
If you start from scratch, then do it thoroughly, using recognized methodologies - the theory of finite automata and IDEF-0 . The principles of describing business processes according to this standard are familiar from the university bench to both engineers and managers - in this they had to find a common language. At the same time, the technician’s blue dream will come true about the automatic construction of process diagrams that management loves so much. For example, such a scheme is displayed on one of the displays with statistics that are hung in abundance at the Yandex.Money office.
When translating all the old code into new rails, a set of Node.js modules appeared, which describe all the basic methods-processes. Moreover, they are described not just by a set of procedures, but in accordance with the ideas of IDEF-0: there are functional blocks, input and output data, process connections.
In general, IDEF-0 describes a lot of things that can be simplified during development, so we didn’t do the tracing of the standard and just borrowed the idea and all the relevant principles.
In IDEF-0, a function block is simply a separate function of the system, which is graphically depicted as a rectangle. In the Yandex.Money payment process, the functional blocks contain parts of the business logic of a process.
Each of the four sides of the functional block has its own role:
The upper side is responsible for management;
Left - input data (for whom the operation, how much to translate, etc.);
The right side displays the result;
In the Yandex.Money payment frontend, only two sides of the function block are used - input and output: a set of data is sent to the input to execute business logic, at the output the system waits for the result of the execution of this logic.
Here is how it looks in code:
/** * * @param {Object} $flow , , * @param {Object} inputData * @param {Object} inputData.userName */ const checkUserName($flow, inputData) { if (inputData.userName) { // const outputData = { userName: inputData.userName isUserNameValid: true }; $flow.transition('doSomethingElse', outputData); return; } $flow.transition('checkFailed', inputData); }
The function takes two parameters as arguments:
$ flow - service object, an instance of the current process;
When developing functional blocks, it is important to keep in mind the principle of common responsibility , otherwise there will not be enough flexibility when adding new business logic.
The interface arc is simply the arrow of a function block, which, as expected, indicates a data transfer or an effect on a function block.
In the new payment process, the role of the interface arc is performed by the Transition function of the $ flow object, which is an instance of the process responsible for providing the API.
The well-known principle of splitting large and complex into many simple and understandable parts. In code, this means simplification and unification of functions.
In IDEF-0 decomposition looks like this:
Decomposition was applied everywhere in the payment process, but consider the example of the process of checking user properties.
The verification of user properties consists of 5 functional blocks and two exits from the process (marked in blue), which can be decomposed. For example, checking the phone number does not apply only to the user and may be useful in other processes. If you select this action in a separate process, the code will become simpler and clearer:
After decomposing the verification of user properties, part of the functional blocks are moved to a new process that checks the phone number. Using BitBucket, the difference is more clearly visible - three functional blocks are responsible for checking the user's phone:
prepareToCheckPhone — data preparation ;
requestBackendForCheckPhone - request to backend;
Before the transfer to the outside, all these blocks overloaded the logic of checking user properties, and now the process has become much simpler and clearer even to a very young developer.
// check-phone.js module.exports = new ProcessFlow({ initialStage: 'prepareInputData', finalStages: [ 'phoneValid', 'phoneInvalid' ], stages: { /** * * @param {Object} $flow , * @param {Object} inputData */ prepareInputData($flow, inputData) { /** * , , * , . * , * , */ $flow.transition('checkPhone', { phone: inputData }); }, /** * * @param {Object} $flow , * @param {Object} inputData */ checkPhone($flow, inputData) { const someBackend = require('some-backend-module'); someBackend.checkPhone(inputData.phone) .then((result) => { $flow.transition('processCheckResult', result); }) .catch((err) => { $flow.transition('phoneInvalid', { err: err }); }); }, /** * * @param {Object} $flow , * @param {Object} inputData */ processCheckResult($flow, inputData) { if (inputData.isPhoneValid) { $flow.transition('phoneValid'); return; } $flow.transition('phoneInvalid'); } } }); // check-user.js const checkPhoneProcess = require('./check-phone'); module.exports = new ProcessFlow({ // , initialStage: 'checkUserName', // finalStages: [ 'userCheckedSuccessful', 'userCheckFailed' ], stages: { /** * * @param {Object} $flow , * @param {Object} inputData */ checkUserName($flow, inputData) { if (inputData.userName) { $flow.transition('checkUserBalance', inputData); return; } $flow.transition('userCheckFailed', { reason: 'invalid-user-name' }); }, /** * * @param {Object} $flow , * @param {Object} inputData */ checkUserBalance($flow, inputData) { if (inputData.balance > 0) { $flow.transition('checkUserPhone', inputData); return; } $flow.transition('userCheckFailed', { reason: 'invalid-user-balance' }); }, /** * * @param {Object} $flow , * @param {Object} inputData */ checkUserPhone($flow, inputData) { const phone = inputData.operatorCode + inputData.number; checkPhoneProcess.start(phone, { // phoneValid() { $flow.transition('userCheckedSuccessful'); }, phoneInvalid() { $flow.transition('userCheckFailed', { reason: 'invalid-user-phone' }); } }); } } });
Each Yandex.Money payment process is an instance of the ProcessFlow class, which provides a process management API. It has a start method that calls the function block described in initialStage . As arguments, the start method takes input data and process output handlers.
Processes usually contain complex business logic, so the code has to limit their complexity in accordance with the recommendations of IDEF-0:
No more than 6 functional blocks at each level. This limitation encourages the developer to use hierarchy when describing complex logic;
The lower limit of 3 blocks ensures that the creation of the process is justified;
In the already familiar illustration of the “as it was” process, 7 functional blocks are visible, which increases the temptation to write everything flat without bothering with the hierarchy.
In the next section, I’ll show you what the redesigned process looks like after simplifying the logic.
In large companies, business processes sometimes become obsolete faster than analysts have time to draw them. Alas, we are no exception in this regard, so I had to learn how to draw faster.
Thanks to IDEF-0 and strict rules for describing the processes in the code, we can use static code analysis to construct a diagram of the connections of both the functional blocks and the processes between them. For example, the Esprima product will do . As a result of analyzing the code, this tool forms an object with all functional blocks and transitions, and the visualization takes place in the browser using the GoJS library:
The diagram shows the check-user and check-phone processes with the dependencies indicated. If you expand them, you get the following:
The initial functional blocks are clearly visible on the diagram, the process outputs are marked in color. For example, from this scheme it is obvious that the result of userCheckFailed can be obtained not only at the stage of checking the phone number, but also at the time of checking the name. Previously, it was ridiculously not obvious.
The result of the refactoring of the payment process was a whole platform for describing data preparation processes. The main advantage of the time spent on refactoring is the correct way of thinking of the developers, who now adhere to strict rules when forming the logic of new processes. This means that in the future there will be less refactoring.
In addition, any newcomer can quickly grasp the essence of the process. This saves a lot of time at the briefings and allows you to introduce new chips without the fear that everything will fall apart.
There is also a side effect - business analysts no longer have to draw static charts, so the consumption of coffee and tea with cocoa has increased dramatically.
Source: https://habr.com/ru/post/321824/
All Articles