📜 ⬆️ ⬇️

Reflections on design principles

Why invent all these design patterns, principles and techniques? Wouldn't it be easier to do without all this, but just to teach the developers good design? Or why not formalize this process and introduce clear quantitative metrics that say that one solution is definitely better than another?

“Right Design” is the holy grail of young developers and young managers. And those and others dream of finding the answer to the main question of life, the universe and all such software development - how to achieve high-quality design, in a short time and with a minimum of effort.

Over time, both the young developer and the young manager will come to understand that this is impossible. It is impossible to find a perfect abstract design, since the words “perfect” and “abstract” contradict each other. Design is a constant search for a compromise between conflicting requirements: performance and readability, simplicity and extensibility, testability and integrity of the solution.

')
Even if we take into account that the developer always solves the right task, rather than struggling with windmills, it is impossible to say “abstractly” what design characteristics are key here and now.

There are formal criteria that describe the quality of a code or design: the cyclomatic complexity of methods, the depth of the inheritance hierarchy, the number of incoming and outgoing links of a class or module, the number of lines of a method, at worst. These quantitative indicators are useful, but getting them into the specified boundaries is a necessary but not sufficient condition for a good design. If classes are broken ineptly, and important domain abstractions are not identified, then no matter what quantitative characteristics a design has, it will never be good.

In addition to formal criteria, there are universal notions of good design — weak coupling (low coupling) and strong coupling (high cohesion). These properties are useful, but too informal.

Between formal and informal criteria are the principles of design - a set of rules on which experienced designers rely. Their goal is to describe in simple terms what is “good and what is bad” in terms of design: “The ideal class should have only one reason for change ( SRP ), have a minimal interface ( ISP ), correctly implement inheritance ( LSP ) and prevent cascading code changes as requirements change ( OCP ). "

Bertrand Meyer in his book “Agile !: The Good, The Hype, and The Ugly” gives a sufficiently clear definition of what the design principle is [Meyer2014]:

“A principle is a methodological rule that expresses a general view of software development. A good principle is both abstract and refutable. Abstractness distinguishes principle from practice , and refutation distinguishes principle from platitude (platitude). Abstractness means that a principle should describe a universal rule, not a concrete practice. Refutation means that a reasonable person should have the opportunity to disagree with the principle. If no one in their right mind would dispute the proposed principle, this rule would be useful, but not interesting. For a rule to be a principle - regardless of your opinion - you must assume the presence of people who hold the opposite point of view. ”

Levels of design principles




A developer during his professional career goes through several stages of ownership of such a tool as design principles.

At the first stage, the young developer has not yet grown to abstract principles; instead, he is looking for a set of concrete practices that will enable him to get a quality solution. "I want to hear a set of steps, a strict adherence to which will lead me to the desired result!" At this stage, the developer is good at copying someone else's solution, and is faced with serious difficulties if the description is too abstract or does not absolutely fit his case.

At the second stage, the developer begins to see what underlies the concrete practice, and why design principles are needed. “Yeah, this class violates the SRP because it goes to the base and contains business logic! It turns out that he has two clear reasons for a change! ”

Despite the increased level, it is at this stage that the most frequent use of principles or patterns for inappropriate purposes occurs. It is easy to prove that a particular class violates this principle, but at this stage of development it is not always obvious when this violation is justified and when it is not.

At the third stage , the developer (or rather, the Architect) develops an instinct and there appears a fairly clear understanding of what problem the specific design principle is intended to solve. Since, by its definition, the principle is not unambiguous, an experienced developer has an understanding when the violation is justified, when it is possible to live with it, and when the time has come to take on the correction of the design.

At this stage, the principles do not control the design of the application, but rather begin to play a communicative role: “Look, your class does too much, which means it violates SRP! And this class has two types of clients that violates ISP! ”.

NOTE
In martial arts, it is customary to distinguish three stages of mastery: shu, ha, and ri ( Shu, Ha, Ri ). At the first stage there is a student who only repeats the movements behind the master. At the second stage, the student begins to get rid of the rules and begins to decide himself when to follow them, and when not. At the third stage, the rules disappear, the student becomes a master and can create these rules himself. The same model, but somewhat under a different sauce, appeared in American culture called the Dreyfus Model .

The role of design principles


The application of any design principle has its price. Crushing a class into smaller components so that it responds to SRP can lead to a blurring of logic across several classes (low cohesion), and sometimes to a drop in performance.

Violation of the principle of open-closed may be justified by backward compatibility issues. We can ignore the Liskov substitution principle, since inheritance does not always determine the ratio of subtypes. The interface may be thick due to backward compatibility or ease of use. And inversion of dependencies can easily undermine the encapsulation and lead to the full OOP of the brain.

To effectively use the principles you need to know what problem they actually solve and whether it is relevant for you now. Typically, the relevance of the principles increases with increasing complexity, or with an increase in the cost of making changes. The key business logic of an application is one example of what should be isolated from the rest of the world. Any publicly available classes should have a minimum of additional connections with the outside world and should easily allow you to do simple things. Classes that are located at the interface of modules should be thought out better than others.

The key to good design and efficient use of design principles is an iterative approach to design. In the early stages, we understand little in the subject area, so all we can do is to break the modules into roles: the infrastructure is separate, the logic is separate. At the same time, class autonomy and minimization of side effects makes the design simpler, since the number of class / module links remains strictly limited.

As the application develops, design becomes cluttered with unnecessary patches, and our understanding of the subject area improves. I try to avoid major refactorings, and make changes to the design in the course of its development. Before making changes, I try to answer the question: why am I doing this? If it’s just that the current solution violates a certain design principle, then before making changes you need to understand if there is a problem.

The main driving force behind my changes is ultimately increased complexity. As soon as I fail to keep the decision in my head, it is difficult to navigate around it or it is not clear where to make changes, then it is time to think about how to improve it.

“The class has become too complicated, it breaks the SRP , it's time to break it into two. Information about the inheritance hierarchy has spread all over the module, probably, it is worth hiding it behind the factory. The class cannot be tested due to the abundance of hidden dependencies, it is time to select the class dependencies . ”

At the same time, I always check if principles or patterns didn’t lead me into the world of overdesign: did my design become easier after making changes? Do I solve a problem that actually does not exist? Did I get into the network of premature generalization? Sometimes you have to perform several iterations before you can find a reasonable solution to a certain problem.

Anti-design principles



The popularity of design principles can easily play a cruel joke with you and your team. An excessive love of principles and patterns can manifest itself in the form of over-engineering and excessive complexity. But knowing this, we can prepare and predict how the excessive love of principles will manifest itself in the application code.

Anti- SRP - The principle of vague responsibility . Classes are divided into many small classes, as a result of which the logic is spread over several classes / modules.

Anti -OCP - The principle of factory-factories . The design is too general and extensible, there are too many levels of abstraction.

Anti -LCP - The principle of incomprehensible inheritance. The principle is manifested either in an excessive amount of inheritance, or in its complete absence, depending on the experience and views of the local chief architect.

Anti -ISP - The principle of thousands of interfaces. Interfaces of classes are broken into too many components, which makes them inconvenient for all customers to use.

Anti-DIP - The principle of inversion of consciousness or DI-brain. Interfaces are allocated for each class and are transmitted in batches through constructors. Understanding where the logic is located is almost impossible.

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


All Articles