📜 ⬆️ ⬇️

Dry anti-pattern

For a long time I wondered what was wrong with some parts of the code. Time after time, in each of the projects there is a certain “especially vulnerable” component that “falls” all the time. The customer has the ability to periodically change the requirements, and the agile canons will force us to embody all the wishes by launching change request into our scrum mechanism. And as soon as the changes concern the component, after a couple of days, the QA find several new defects in it, rediscover the old ones, and even report that it is completely inoperable at one of the application points. So why is one of the components all the time on the lips, why is it so often pronounced the phrase a la "again # component # broke"? Why is this component given as an anti-example, in the context of “as long as one more is the same”? What makes this component so unstable to change?

When they find the cause that led to, or contributed to the development of such a defect in the application, this reason is designated as antipattern.
This time, the Strategy pattern was the stumbling block. The abuse of this pattern led to the creation of the most fragile parts of our projects. The pattern itself has a completely "peaceful" use, the problem is rather that it is shoved to where it fits, and not to where it is needed. "If you understand what I mean" (c).

Classification


The pattern exists in several “faces”. The essence of it does not change much, the danger of its use exists in any of them.
The first, classic view is a long-lived object, which receives another object through the interface, the strategy itself, through a setter, with some state change.
The second kind, a degenerate version of the first - the strategy is taken once, for the entire lifetime of the object. Those. for one scenario, one strategy is used, for another, another.
The third type is the executable method, either static or in a short-lived object, which takes an interface strategy as an input parameter. In the "gang of four" this type is called as "Template method".
The fourth type - interface, aka UI-ny. Sometimes referred to as a “pattern” pattern, sometimes as a “container”. On the example of web development, it is some kind of markup that contains a placeholder (or even more than one), where a variable part of the markup, which has several different implementations, will be rendered during execution. Parallel to the markup, in the JavaScript code also live parallel view models or controllers, depending on the architecture adopted in the application, organized by the second type.

The common features of these all cases are:
1) some unchangeable part of aka having one implementation, further, I will call it a container
2) the variable part, it is a strategy
3) the place of use, creating / calling the container, determining which strategy the container should use, I will call it a script below.
')

Disease progression


At first, when the component using this pattern was only implemented in the project, it did not seem so bad. It was used when it was necessary to create two identical pages (again, using web development as an example), which only slightly differ in content in the middle. On the contrary, the developer was glad how beautiful and elegant it was to implement the principle of DRY, i.e. completely avoid duplicate code. These are the epithets I heard about the component when it was just created. The one that became the popabol of the entire project a few months later.
And since I began to theorize, I’ll go a little further - it is attempts to realize the principle of DRY, through the strategy strategy, in fact, like through inheritance, lead to darkness. When, for the sake of DRY, without even noticing it, the developer sacrifices the SRP principle, the first and main postulate of SOLID. By the way, DRY is not part of SOLID, and in case of conflict, it is necessary to sacrifice them, because it does not favor the creation of a stable code, in contrast to, in fact, SOLID. As it turned out - rather the opposite. Re-use of the code should be a pleasant bonus of certain design decisions, and not the purpose of making them.
And the temptation to reuse occurs when the customer comes with a new story to create a third page. After all, it is so similar to the first two. It also helps the customer’s desire to realize everything “cheaper”, because reusing a previously created container is faster than implementing the page completely. The story got to another developer who quickly found out that the container’s functionality is not enough, and full-fledged refactoring does not fit into the estimates. Another of the mistakes here is that the developer continues to follow the plan set by the estimates, and this happens “in silence”, because there is no responsibility, it lies with the entire team that made such a decision and such an assessment.
And now new functionality is added to the container, new methods and fields are added to the strategy interface. If-s appear in the container, and in the old implementations of the strategy there appear "stubs" in order not to break the already existing pages. At the time of the second story, the component was already doomed, and the farther, the worse. It is harder and harder for developers to understand how it works, including those who “did something in it quite recently”. It's harder to make changes. Increasingly, one has to consult with the “previous ones” to ask how it works, why some changes have been made. It is increasingly likely that even the slightest change will make a new defect. Actually, we are already starting to talk about the fact that there is an increasing probability of introducing two or more defects, since one defect appears already with near-unit probability. And here comes the moment when new customer requirements cannot be realized. There are two ways out: either completely rewrite, or make an explicit hack. And in Angulyar there is just a suitable hack tool - you can do emit events from bottom to top, then broadcast from top to bottom, when you have finished dirty work at the top. At the same time, technical debt does not increase anymore, it has long been equal to the cost of selling this component from scratch.

Dry alternative


Inheritance is often censured, and the corporation of good, in its Go language, decided to do without it at all, and it seems to me that the negative to inheritance is partly based on the experience of implementing the DRY principle through it. "Strategic" DRY also leads to sad results. Direct aggregation remains. For illustration, I will take a simple example and show how it can be presented in the form of a strategy, that is, a template method without it.
Suppose we have two very similar scenarios, represented by the following pseudocode:
They repeat 10 lines X at the beginning and 15 lines Y at the end. In the middle, one script has lines A, another - lines B
{  X1 ... X10  1 ... 5  Y1 ... Y15 } 

 {  X1 ... X10  B1 ... B3  Y1 ... Y15 } 


The option of getting rid of duplication through strategy


 {  X1 ... X10 ()  Y1 ... Y15 } 

 {  1 ... 5 } 

 {  B1 ... B3 } 

 { (new ) } 

 { (new B) } 


Option through direct aggregation


 {  X1 ... X10 } 

 Y{  Y1 ... Y15 } 

 {  1 ... 5 } 

 {  B1 ... B3 } 

 { () A() Y() } 

 { () B() Y() } 

Here it is assumed that all methods are in different classes.
As I said, at the time of implementation, the first option does not look so bad. Its disadvantage is not that it is initially bad, but that it is not resistant to change. And, nevertheless, it is worse read, although with a simple example this may not be obvious. When you need to implement the third scenario, which is similar to the first two, but not 100%, there is a desire to reuse the code contained in the container. But it will not work out partially, you can only take it entirely, so you have to make changes to the container, which immediately carries the risk of breaking other scenarios. The same happens when a new requirement implies changes in scenario A, but this should not affect scenario B. In the case of aggregation, method X can be easily replaced by method X 'in one scenario, without affecting the others at all. It is easy to assume that the methods X and X 'may also almost completely coincide, and they can also be subdivided. With the “strategy” approach, if cascaded in the same “strategy” manner, then the evil placed in the project is raised to the second degree.

When can


Many examples of the use of the strategy pattern are visible and often used. All of them are united by one simple rule - there is no business logic in the container. Totally. There may be algorithmic content, such as a hash table, search or sort. The strategy also contains business logic. The rule that one element is equal to another or more-less is business logic. All linq operators are also the embodiment of the pattern, for example, the .Where () operator is also a template method, and the lambda it takes is a strategy.
In addition to algorithmic content, it can be content associated with the outside world, asynchronous requests, for example, or, in the example from the “gang of four”, a subscription to a mouse click event. What they call callbacks is essentially the same strategy, I hope they will forgive all my hyper-generalizations. Also, if we are talking about UI, then it can be tabs, or a pop-up window.
In short, it can be anything, completely abstracted from business logic.
If you are using the strategy strategy in development, and the business logic has fallen into the container - you know, you have already crossed the line, and you are standing ankle-deep in ... mmm, swamp.

Smells


Sometimes it is not easy to understand where the line is between business logic and general programming tasks. And at first, when the component is just created, it is not easy to determine that it will bring hemorrhoids in the future. And if business requirements never change, this component may never emerge. But if there are changes, the following code smells will inevitably appear:
1. The number of transferred methods. The parameter under discussion is not harmful in itself, but it may hint. Two or three is still normal, but if the strategy contains about a dozen methods, then probably something is wrong.
2. Flags. If, in addition to methods, there are fields / properties in the strategy, you should pay attention to what they are called. Fields such as Name, Header, ContentText are valid. But if you see such fields as SkipSomeCheck, IsSomethingAllowed, this means that the strategy already smacks.
3. Conditional calls. Associated with flags. If there is a similar code in the container, it means that you have already left for a swamp to the waist.
 if(!strategy.SkipSomeCheck) { strategy.CheckSomething(). } 

4. Inadequate code. An example from JavaScript -
 if(strategy.doSomething) 

From the name you can see that doSomething is a method, but is checked as a flag. That is, the developers were too lazy to create a flag denoting the type, but they used the presence / absence of a method as a flag, and it wasn’t even called inside an if block. If you meet this, you should know - the component is already in the technical debt.

Conclusion


Once again I want to express my opinion that the root cause of all that I described is not in the pattern as such, but in the fact that it was used for the DRY principle, and this principle was put above the sole responsibility principle, aka SRP. And, by the way, more than once I have come across the fact that the principle of sole responsibility is somehow not adequately interpreted. Something like “my divine class controls the satellite, managing the satellite is his only responsibility.” On this note I want to finish my opus and wish less often in response to "why so", to hear the phrase "historically it happened."

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


All Articles