⬆️ ⬇️

Enhanced Four Rules for Software Design

Hello, Habr! I present to you the article "Four Better Rules for Software Design" by David Bryant Copeland. David Bryant Copeland is a software architect and CTO for Stitch Fix. He maintains a blog and is the author of several books .



Martin Fowler recently tweeted with a link to his blog post about four simple design rules from Kent Beck, which I think can be further improved (and which can sometimes send the programmer in the wrong way):



Kent's Rules from Extreme Programming Explained :





According to my experience, these rules do not quite meet the needs of software design. My four rules for a well-designed system might be:





For me, these rules stem from what we do with our software.



So what do we do with our software?



We cannot talk about software design without first talking about what we intend to do with it.



The software is written to solve the problem. The program runs and has a behavior. This behavior is studied to ensure correct operation or to detect errors. Software also often changes to give it new or changed behavior.



Therefore, any approach to software design should be focused on predicting, studying, and understanding its behavior in order to make changing this behavior as simple as possible.



We check the correctness of behavior by testing, and therefore I agree with Kent that the first and most important thing is that well-designed software must pass the tests. I will even go further and insist that the software should have tests (i.e. be well covered by tests).



After the behavior has been verified, the following three points on both lists relate to understanding our software (and therefore its behavior). His list begins with code duplication, which is really in place. However, in my personal experience, focusing too much on reducing code duplication is expensive. To eliminate it, it is necessary to create abstractions that hide it, and it is these abstractions that make the software difficult to understand and change.



Eliminating code duplication requires abstractions, and abstractions lead to complexity



Don't Repeat Yourself or DRY is used to justify controversial design decisions. Have you ever seen similar code?



ZERO = BigDecimal.new(0) 


In addition, you probably saw something like this:



 public void call(Map payload, boolean async, int errorStrategy) { // ... } 


If you see methods or functions with flags, boolean, etc., then this usually means that someone used the DRY principle when refactoring, but the code was not exactly the same in both places, so the resulting code should have be flexible enough to accommodate both behaviors.



Such generalized abstractions are difficult to test and understand, since they should handle many more cases than the original (possibly duplicated) code. In other words, abstractions support much more behaviors than is necessary for the normal functioning of the system. Thus, eliminating code duplication can create new behavior that the system does not require.



Therefore, it is really important to combine some types of behavior, but it can be difficult to understand what kind of behavior is really duplicated. Often pieces of code look similar, but this happens only by accident.



Consider how much easier it is to eliminate duplication of code than to return it again (for example, after creating a poorly thought out abstraction). Therefore, we need to think about leaving duplicate code, unless we are absolutely sure that we have a better way to get rid of it.



Creating abstractions should make us think. If in the process of eliminating duplicate code you create a very flexible generalized abstraction, then you may have gone the wrong way.



This leads us to the next point - intent versus behavior.



Programmer’s intention is meaningless - behavior means everything



We often praise programming languages, constructs, or pieces of code for “revealing the programmer’s intentions.” But what's the point of knowing intentions if you cannot predict behavior? And if you know behavior, how much does intention mean? It turns out that you need to know how the software should behave, but this is not the same as the "programmer's intentions."



Let's look at this example, which very well reflects the intentions of the programmer, but does not behave as intended:



 function LastModified(props) { return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); } 


Obviously, the programmer planned that this React component would display a date with the message "Last modified on". Does this work as intended? Not really. What if this.prop.date doesn't matter? Everything just breaks down. We don’t know if it was so planned, or someone just forgot about it, and it doesn’t even matter. What matters is the behavior.



And this is exactly what we should know if we want to change this part of the code. Imagine we need to change the line to "Last modification". Although we can do this, it is not clear what should happen if date is missing. It would be better if we instead write the component in such a way as to make its behavior more understandable.



 function LastModified(props) { if (!props.date) { throw "LastModified requires a date to be passed"; } return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); } 


Or even like this:



 function LastModified(props) { if (props.date) { return ( <div> Last modified on { props.date.toLocaleDateString() } </div> ); } else { return <div>Never modified</div>; } } 


In both cases, the behavior is more understandable, and the intentions of the programmer do not matter. Suppose we choose the second alternative (which handles the missing date value). When we are asked to change the message, we can see the behavior and check whether the message "Never modified" is correct or whether it also needs to be changed.



Thus, the more unambiguous the behavior , the more chances we have to successfully change it. And this means that we may need to write more code or make it more accurate, or even write duplicate code sometimes.



This also means that we will need more classes, functions, methods, etc. Of course, we would like to keep their number minimal, but we should not use this number as our metric. Creating a large number of classes or methods creates conceptual overhead , and more concepts appear in the software than units of modularity. Therefore, we need to reduce the number of concepts, which, in turn, can lead to a decrease in the number of classes.



Conceptual costs contribute to confusion and complexity



To understand what the code will actually do, you need to know not only the subject area, but also all the concepts used in this code (for example, when searching for the standard deviation, you must know the assignment, addition, multiplication, for loops and array lengths). This explains why as the number of concepts in a design increases, its complexity for understanding increases.



I used to write about conceptual expenses , and a good side effect of reducing the number of concepts in a system is that more people can understand this system. This in turn increases the number of people who can make changes to this system. Definitely, a software design that can safely be changed by many people is better than one that can only be changed by a small handful. (Therefore, I believe that hardcore functional programming will never become popular, as it requires a deep understanding of many very abstract concepts.)



Reducing conceptual costs will naturally reduce the number of abstractions and make behavior easier to understand. I do not say “never introduce a new concept”, I say that it has its own price, and if this price outweighs the benefit, the introduction of a new concept should be carefully considered.



When we write code or design software, we should stop thinking about the elegance , beauty, or other subjective measure of our code. Instead, we should always remember what we are going to do with the software.



You do not hang the code on the wall - you change it



A code is not a work of art that you can print and hang in a museum. The code is executing. It is studied and debugged. And, most importantly, it is changing . And often. Any design that is difficult to work with should be called into question and reviewed. Any design that reduces the number of people who can work with it should also be called into question.



The code should work, so it should be tested. The code has bugs and will require the addition of new features, so we need to understand its behavior. The code lives longer than the ability of a particular programmer to support it, so we should strive for code that is understandable to a wide range of people.



When you write your code or design your system, do you simplify the explanation of system behavior? Is it becoming easier to understand how she will behave? Are you focused on solving the problem right in front of you or on a more abstract one?



Always try to keep behavior simple for demonstration, prediction and understanding, and keep the number of concepts to an absolute minimum.



')

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



All Articles