I will try to explain the essence of the KISS design principle simply and at the same time in great detail. KISS is a very general and abstract design principle that contains almost all other design principles. Design principles describe how to write "good" code. However, what does good code mean? Some people think that this is a code that runs as quickly as possible, some - that this is a code that involves as many design patterns as possible ... But the right answer lies on the surface. The code is pure information. And the main criteria for the value of information are 1) reliability 2) accessibility 3) understandability. Why credibility and availability are important is obvious. From the code there is no use, if it works with errors or if the server with the application "lies". Why is clear code important? In a clear code, it is easier to look for errors, it is easier to change, modify and maintain. So, clarity is the main value to which the programmer should strive. However, there is one discrepancy. The fact is that clarity is a purely subjective thing.
We need some more objective criterion of clarity. And this criterion is simplicity. Indeed, a simple application is more understandable than a complex one. However, simplicity is difficult to achieve. Here is what Peter Goodwin writes in his book The Craft of the Programmer:
If the project is simple, it is easy to understand ... Developing a simple project is not so easy. It takes time. For any complex program, the final solution is obtained by analyzing a huge amount of information. When the code is well designed, it seems that it could not be otherwise, but it is possible that its simplicity is achieved as a result of intense mental work (and a large amount of refactoring). Making a simple thing is difficult. If the structure of the code seems obvious, do not think that it was easy.
So, the KISS design principle (keep it simple and straightforward) proclaims that the simplicity of the code is above all, because simple code is the most understandable.Almost all design principles are aimed at achieving clarity of the code. Violating any design principle, you reduce the clarity of the code. The incomprehensible code automatically causes the person to feel that the code is complex, since it is difficult to understand and modify it. If any of these principles are violated, the KISS principle is also violated. Therefore, it can be said that KISS includes almost all other design principles.
Design patterns describe the most successful, simple and clear solutions to some problems. If you use the design pattern where there is no problem that this pattern solves, then you break KISS by introducing unnecessary complications into the code. If you do NOT use the design pattern where there is a problem matching the pattern, then again you are breaking KISS, making the code harder than it could be.
In my opinion, the KISS principle can be useful only for novice designers who do not know or do not understand the basic principles of design. KISS protects against misuse of design principles and patterns. Since the principles and patterns are designed to increase the clarity of the code, their correct use can not lead to a decrease in the clarity of the code. However, if you misunderstand the design principle (for example, interpret “do not multiply unnecessary entities” as “produce as few entities as possible”), or by observing one principle you violate a dozen others, then KISS can be a reliable airbag for you. In other cases, there is little confusion from KISS, since It is too general and abstract. The remaining principles are more specific and contain more obvious ways to achieve comprehensibility and simplicity of the code.
Due to the fact that the ideas of different people about such a concept as “simplicity” may differ, the following
misconceptions regarding KISS-a have become widespread:
Misconception 1. If we assume that simple code is such a code that is easiest to write, then we can interpret that the KISS principle calls for writing the first thing that comes to mind, without thinking about design at all.
Misconception 2. If we assume that simple code is such a code that requires as little knowledge as possible to write, then we can interpret that the KISS principle calls for not using design patterns.
')
By simplicity, in this case it should be understood as
not complicated, devoid of artificiality, the most natural, not difficult, easily accessible to understand .
C # example
Task: When crossing figures, it is necessary to shade the area of ​​their intersection.
So how to develop a universal hatching algorithm for different combinations
shapes (rectangle-rectangle, rectangle-polygon,
polygon-polygon, ellipse-polygon, ellipse-ellipse) pretty
difficult and most likely it will not be very effective, then we realize for everyone
option your algorithm.
What the first thing that comes to mind looks like:
Expand Codepublic interface IShape { } public class Circle : IShape { } public class Rectangle : IShape { } public class RoundedRectangle : IShape { } public class IntersectionFinder { public IShape FindIntersection(IShape shape, IShape shape2) { if (shape is Circle && shape2 is Rectangle) return FindIntersection(shape as Circle, shape2 as Rectangle); if (shape is Circle && shape2 is RoundedRectangle) return FindIntersection(shape as Circle, shape2 as RoundedRectangle); if (shape is RoundedRectangle && shape2 is Rectangle) return FindIntersection(shape as RoundedRectangle, shape2 as Rectangle); return FindIntersection(shape2, shape); } private IShape FindIntersection(Circle circle, Rectangle rectangle) { return new RoundedRectangle();
However, this code contradicts two points from the definition of simplicity: the most natural and easily accessible. The code is not natural, because there is some kind of artificial class IntersectionFinder. The code is not easily accessible, because a person unfamiliar with the code will need to look through all the places of use of IShape in order to understand whether the functionality for calculating the intersection of shapes is implemented and how it is used. In projects that have several tens (or even hundreds) of thousands of lines of code, this may not be a quick task. There is one more unpleasant moment that adds difficulties to working with the IntersectionFinder class: the number of functions named FindIntersection increases as an arithmetic progression from the number of figures, as a result of which the IntersectionFinder class “swells up” very quickly and with a large number of figures the search for the desired function in it becomes expensive on time occupation. Therefore, we transfer FindIntersection to IShape.
Expand Code public interface IShape { IShape FindIntersection(IShape shape); } public class Circle : IShape { public IShape FindIntersection(IShape shape) { if (shape is Rectangle) return FindIntersection(shape as Rectangle); if (shape is RoundedRectangle) return FindIntersection(shape as RoundedRectangle); return shape.FindIntersection(this); } private IShape FindIntersection(Rectangle rectangle) { return new RoundedRectangle();
Great, now a programmer unfamiliar with the code will not have to look for a way to make the intersection of two shapes across the entire project. The redundant Essence Evaluator entity has disappeared. The code has become more natural and easily accessible, which means it is simpler. Now, when creating a new type of shape, you do not need to make changes to the previously created classes, which means adding new types of shapes is also easier. It is easier to find a specific algorithm for finding an intersection, since now you do not need to search for it in a giant class among a multitude of methods with the same name.
But now we notice that the method of deciding which particular function to calculate the intersection needs to be called is not devoid of artificiality. A more natural approach would be: to call a function called FindIntersection, the type of the argument of which coincides with the type of the second figure.
Expand Code public class Shape { public Shape FindIntersection(Shape shape) { var method = MethodFinder.Find(this.GetType(), "FindIntersection", shape.GetType()); if (method != null) { return (Shape)method.Invoke(this, new[] { shape }); } return shape.FindIntersection(this); } } public class Circle : Shape { [UsedImplicitly] private Shape FindIntersection(Rectangle rectangle) { return new RoundedRectangle();
As you can see, the public IShape FindIntersection (IShape shape) methods have disappeared from each specific shape class, the total number of lines of code has decreased. Now add new types of shapes has become even easier. The FindIntersection (Shape shape) method is now in the base class and looks more simple and natural (
declarative ). A new MethodFinder class has been added, but the programmer does not need to know its internal structure, since it has a clear interface and does not implement concepts from the subject area (and therefore the reasons for its changes will be rare), so the complexity of the code practically did not increase when it was added.
There might be an idea that reflection is a slow thing, and to speed up, you can, for example, cache delegates that are dynamically generated by an ExpressionTree, however KISS calls for writing as simple as possible code, so you should refrain from this thought until the performance of the FindIntersection method (Shape shape) really does not become the bottleneck of the program, creating problems for the user. But what should not be postponed is the creation of a unit test, which, through reflection, recognizes all the heirs of the Shape class and checks that the programmer has not forgotten to implement intersection search algorithms for all pairs of shapes.
View test code [TestFixture] public class ShapeTest { [Test] public void AllIntersectsMustBeRealized() { var shapeTypes = typeof(Shape).Assembly.GetTypes().Where(x => x.IsSubclassOf(typeof (Shape))); var errorMessages = new List<string>(); foreach (var firstType in shapeTypes) foreach (var secondType in shapeTypes) { if (MethodFinder.Find(firstType, "FindIntersection", secondType) == null) { errorMessages.Add(string.Format(" : {0} {1}", firstType.Name, secondType.Name)); } } if (errorMessages.Any()) throw new Exception(string.Join("\r\n", errorMessages)); } }
By comparing the first and third examples, it may not seem obvious that the third example is simpler. However, let's imagine that the figure types are not 3, but 30. Then the number of shape comparison functions is 465 (the sum of an arithmetic progression (1 + 30) * 30 \ 2). In the first case, the mechanism for selecting the desired function will be hidden behind 465 if-s (or, alternatively, behind a container with 465 pointers to methods, which is not much better), and among this pile of if-s, the programmer unfamiliar with the code will have to see some system. Whereas in the 3rd case the approach is declarative and does not depend on the number of types of figures. This example is good because a significant part of programmers may think that the third example is a bad decision, because it uses reflection to access private variables (which is a kind of taboo among programmers), because they have heard from reputable sources that reflection for such purposes is bad, but they cannot explain why this is bad. This psychological phenomenon is called fixed values.
Learn about the phenomenon of fixed valuesThe description of the phenomenon is taken from the book by Chad Fowler
, a fanatic programmer and is demonstrated on the example of monkey fishing.
The inhabitants of South India, who were harassed by monkeys for many years, came up with an original way of catching them. They dug a deep narrow hole in the ground, then expanded the bottom of the hole with a thin object of the same length. After that, rice was poured into a wider part of the bottom of the hole. Monkeys love to eat. In fact, it is mainly because of this that they are so annoying. They will jump on cars or risk running through a large crowd of people to snatch food right out of your hands. Residents of South India know this too well. (Believe me, it’s very unpleasant when you are standing in the middle of a park and suddenly at great speed a monkey starts to run at you to grab something.) So, the monkeys came up, found rice and put their hands in a hole. Their hands were down. They eagerly seized as much rice as possible, gradually folding their palms into fists. The fists occupied the volume of the wide part of the burrow, and the upper part was so narrow that the monkey could not squeeze the fists through it. And she was trapped. Of course, they could just refuse to eat and stay free. But monkeys attach great importance to food. In fact, food is so important to them that they cannot bring themselves to give it up. They will squeeze the rice until they pull it out of the ground or die trying to pull it out. Usually the second came earlier. Fixing values ​​is when you believe in the value of something so much that you can no longer objectively question it. Monkeys value rice so highly that when they have to choose between rice and a death captive, they cannot understand that now it is better to lose rice. The story presents monkeys very silly, but most of us have our equivalent of rice. If you were asked if it was good to help the starving children of the third world countries with food, you most likely would have answered without thinking “yes”. If anyone tried to challenge your point of view, you would decide that he is crazy. This is an example of fixed value. You are convinced of something so strongly that you cannot imagine how you can not believe in it. And in this case, fixed value is the belief that using reflection to access private methods is bad.
Find out why the use of reflection to access private methods in this case is not blasphemousIn fact, calling private methods outside the data type within which the method is declared is a violation of encapsulation. However, no matter how surprising this may sound, in this example the encapsulation is not broken. Conceptually, the parent class and the derived class are the same data type. The parent's code, encapsulated from the outside world, can be called in the (protected) heir, while the parent can call the encapsulated (virtual) inherited methods (protected virtual). If you delve into the "insides" of the heir class, you will inevitably have to look at the internal structure of the parent class, and if the parent, in turn, also has a parent, then his internal structure also. Many developers are aware of this feature and prefer to use composition instead of inheritance (if the situation allows it).
This example clearly demonstrates how, using KISS and trying to make the code simpler, you can come to a better solution to the problem, even if your wrong understanding of certain principles or taboos tells you to use a “crutch” instead of a declarative code that fully reflects
the developer’s
intention .
A bit of history.
The KISS principle originated in the aircraft industry and is historically translated as “Keep it simple stupid” and stands for “make it simple to idiocy”. In the history of the aircraft industry there are cases when too zealous workers nailed extra armor plates on the plane to make the aircraft more resilient in battle, as a result of which the mass of the aircraft became more calculated and the plane simply could not take off. In addition, the skill of many workers was low. In such conditions, the design of the aircraft, which a drunk unskilled worker could not assemble correctly, even if he wanted, had a special value. One of the echoes of the design decisions of the time was the impossibility of confusing and plugging the wrong plug into the socket inside the computer. However, if the result of the labor of the aircraft engineer is the drawing according to which the product will be created, then in the case of the programmer, the product is the drawing itself (figuratively speaking). In the case of a programmer, he must write the code so that a drunk unqualified programmer can make changes to it in accordance with the changed business requirements (that is, change the drawing, and not assemble the plane). Due to differences in the specifics of the aircraft industry and programming, the decoding “Keep it simple stupid”, suitable in the aircraft industry, does not so well reflect the essence of the principle for a programmer. Many lazy programmers decipher “make it simple to idiocy” as “don't bother with design” (compare, for example, the description of the KISS principle in this article with this
description ). Fortunately, KISS has
some other decoding , one of which, in my opinion, best reflects the essence of KISS in programming - “keep it simple and straightforward”. Straightforward translates as simple, honest, straightforward, frank. “Keep it simple and straightforward,” thus, can be freely translated as “Make it simple and declarative,” and design is required to achieve declarativeness.
For example, thank
Hokum , who gave the initial idea for an example, which I changed a little.