KISS - design principle, containing all other design principles

    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 code that runs as fast as possible, some that this is code that involves as many design patterns as possible ... But the right answer lies on the surface. Code is information in its purest form. And the main criteria for the value of information are 1) reliability 2) accessibility 3) understandability. Why credibility and accessibility 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 code comprehensibility important? In an understandable code, it is easier to look for errors, it is easier to change, modify and maintain it. So, understandability is the main value that a programmer should strive for. However, there is one problem. The fact is that comprehensibility is a purely subjective thing. We need some more objective criterion of comprehensibility. And this criterion is simplicity. Indeed, a simple application is more understandable than complex. However, simplicity is difficult to achieve. Here is what Peter Goodwin writes in his book “The Craft of a Programmer”: a simple application is more understandable than complex. However, simplicity is difficult to achieve. Here is what Peter Goodwin writes in his book “The Craft of a Programmer”: a simple application is more understandable than complex. However, simplicity is difficult to achieve. Here is what Peter Goodwin writes in his book “The Craft of a 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 decision is obtained by analyzing a huge amount of information. When the code is well designed, it seems that it could not have been otherwise, but it is possible that its simplicity was achieved as a result of intense mental work (and a large amount of refactoring). Making a simple thing is difficult. If the code structure seems obvious, you should not think that it was given without difficulty.

    So, the design principle of KISS (keep it simple and straightforward) proclaims that code simplicity is paramount because simple code is the most comprehensible.
    Almost all design principles are aimed at achieving code intelligibility. By violating any design principle, you reduce the comprehensibility of the code. An incomprehensible code automatically causes a person to feel that the code is complex, since it is difficult to understand and modify. If any of these principles is violated, the KISS principle is also violated. Therefore, we can say 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 violate KISS, introducing unnecessary complications into the code. If you do NOT use a design pattern where there is a problem corresponding to the pattern, then again you break KISS, making the code more complicated than it could be.

    In my opinion, the KISS principle can only be useful for novice designerswho 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 comprehensibility of the code, their proper use cannot lead to a decrease in the comprehensibility of the code. However, if you misunderstand the design principle (for example, interpret “do not produce unnecessary entities” as “produce as few entities as possible”), or if you observe one principle and violate a dozen others, then KISS can become a reliable airbag for you. In other cases, KISS is of little use. he is too general and abstract. The remaining principles are more specific and contain more explicit ways to achieve the comprehensibility and simplicity of the code.

    Due to the fact that different people’s ideas about such a concept as “simplicity” may vary, the following misconceptions regarding KISS-a have become widespread :
    Misconception 1. If we assume that simple code is the kind of code that is easiest to write, you can interpret that the KISS principle encourages you to write the first thing that comes into your head without thinking about the design at all.
    Misconception 2. If we assume that simple code is such code that requires as little knowledge as possible to write, it can be interpreted that the KISS principle encourages not to use design patterns.

    By simplicity, in this case, we should understandnot complicated, devoid of artificiality, the most natural, not difficult, easily accessible for understanding .

    C # Example



    Task: When crossing the figures, it is necessary to shade the area of ​​their intersection.
    Since it is quite difficult to develop a universal hatching algorithm for different combinations of
    shapes (rectangle-rectangle, rectangle-polygon,
    polygon-polygon, ellipse-polygon, ellipse-ellipse)
    , it will most likely not be very effective, we will implement
    our own algorithm for each option.

    What does the first thing that comes to mind look like:

    Expand code
        public 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(); //также код мог бы вернуть Rectangle или Circle, в зависимости от их размеров. Но для простоты будем считать что метод всегда возвращает RoundedRectangle
            }
            private IShape FindIntersection(Circle circle, RoundedRectangle rounedeRectangle)
            {
                return new Circle();
            }
            private IShape FindIntersection(RoundedRectangle roundedRectanglerectangle, Rectangle rectangle)
            {
                return new Rectangle();
            }  
        }
    



    However, this code contradicts two points from the definition of simplicity: the most natural and easy to understand. The code is not natural, because there is some artificial class IntersectionFinder. The code is not easily accessible for understanding, because a person unfamiliar with the code will need to look at all the places where IShape is used to understand whether the intersection calculation function is implemented and how to use it. In projects with 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 with the name FindIntersection increases in the form of an arithmetic progression from the number of figures, as a result, the IntersectionFinder class “inflates” very quickly and with a large number of figures, finding the desired function in it becomes a time-consuming task. Therefore, we will 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();//также код мог бы вернуть Rectangle или Circle, в зависимости от их размеров. Но для простоты будем считать что метод всегда возвращает RoundedRectangle
            }
            private IShape FindIntersection(RoundedRectangle rounedeRectangle)
            {
                return new Circle();
            }
        }
        public class Rectangle : IShape
        {
            public IShape FindIntersection(IShape shape)
            {
                if (shape is RoundedRectangle)
                    return FindIntersection(shape as RoundedRectangle);
                return shape.FindIntersection(this);
            }
            private IShape FindIntersection(RoundedRectangle roundedRectangle)
            {
                return new Rectangle();
            }
        }
        public class RoundedRectangle : IShape
        {
            public IShape FindIntersection(IShape shape)
            {
                return shape.FindIntersection(this);
            }
        }
    



    Well, now a programmer who is unfamiliar with the code does not have to look for a way around the project to make the intersection of two figures. The unnecessary entity “Intersection Calculator” has disappeared. The code has become more natural and easy to understand, which means it is simpler. Now when creating a new type of figure, you do not need to make changes to previously created classes, which means adding new types of figures has also been simplified. It is easier to find a specific intersection search algorithm, since now you do not need to search for it in a giant class among many methods with the same name.

    But now we notice that the method of deciding which particular function of calculating the intersection needs to be called is not without artificiality. A more natural approach would be this: to call a function called FindIntersection, whose argument type matches 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();//также код мог бы вернуть Rectangle или Circle, в зависимости от их размеров. Но для простоты будем считать что метод всегда возвращает RoundedRectangle
            }
            [UsedImplicitly]
            private Shape FindIntersection(RoundedRectangle rounedeRectangle)
            {
                return new Circle();
            }
        }
        public class Rectangle : Shape
        {
            [UsedImplicitly]
            private Shape FindIntersection(RoundedRectangle roundedRectangle)
            {
                return new Rectangle();
            }
        }
        public class RoundedRectangle : Shape
        {
        }
        public static class MethodFinder
        {
            public static MethodInfo Find(Type classType, string functionName, Type parameterType)
            {
                return
                    classType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
                    .FirstOrDefault(
                        x => x.Name == functionName
                        && x.GetParameters().Count() == 1 
                        && x.GetParameters().First().ParameterType == parameterType);
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                Shape shape = new Rectangle();
                var shapeIntersection = shape.FindIntersection(new Circle());
                Console.WriteLine(shapeIntersection.GetType());
            }
        }
    



    As you can see, public IShape FindIntersection (IShape shape) methods have disappeared from each concrete class of the figure, the total number of lines of code has been reduced. Now adding 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, because it has a clear interface and does not implement concepts from the subject area (which means that the reasons for its changes will be rare), so the complexity of the code practically did not increase when it was added.
    Here, the thought may arise that reflection is a slow thing, and for acceleration, for example, you can cache delegates dynamically formed using ExpressionTree, but KISS encourages you to write as simple code as possible, so you should refrain from this thought until the FindIntersection method is fast (Shape shape) really will 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 that, through reflection, recognizes all the descendants of the Shape class and verifies that the programmer has not forgotten to implement intersection search algorithms for all pairs of figures.

    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();
                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));
            }
        }
    



    Comparing the first and third examples with a glance, it may not seem obvious that the third example is simpler. However, let's imagine that the types of figures are not 3, but 30. Then the number of figure comparison functions is 465 (the sum of the arithmetic progression (1 + 30) * 30 \ 2). In the first case, the mechanism for selecting the desired function will be hidden behind 465 ifs (or, alternatively, behind a container with 465 pointers to methods, which is not much better), and among this pile of ifs, a programmer unfamiliar with the code a certain 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 in that a significant part of the programmers may think that the third example is a bad decision, since it uses reflection to access private variables (which is a kind of taboo among programmers), because they heard from reputable sources that using reflection for such purposes is bad, but they cannot explain why it is bad. This psychological phenomenon is called fixed values.

    Learn about the phenomenon of fixed values
    The description of the phenomenon is taken from the book of Chad Fowler the Programmer-fanatic and is demonstrated by the example of catching monkeys.
    Residents of South India, whom the monkeys have been harassing for many years, have come up with an original way to catch them. They dug a deep narrow hole in the ground, then with a thin object of the same length they widened the bottom of the hole. After that, rice was poured into the wider part at the bottom of the hole. Monkeys love to eat. In fact, it is mainly because of this that they are so bothersome. They will hop on cars or take risks running through a large crowd of people to snatch food out of your hands. South Indian people are too well aware of this. (Believe me, it’s very unpleasant when you are standing in the middle of the park and suddenly at a tremendous speed a macaque starts to run at you to grab something.) So, the monkeys came up, found rice and put their hands in the hole. Their hands were below. They eagerly grabbed as much rice as possible, gradually folding palms into fists. The fists occupied the volume of the wide part of the hole, and the upper part was so narrow that the monkey could not squeeze the fists through it. And was trapped. Of course, they could just give up food and stay at large. But monkeys attach great importance to food. In fact, food is so important for them that they cannot force themselves to refuse it. They will squeeze the rice until they are pulled out of the ground or die, trying to pull it out. Usually the second came earlier. The fixation of values ​​is when you believe in the significance of something so strongly that you can no longer objectively doubt it. Monkeys rank rice so highly that when they have to choose between rice and death captivity, they cannot understand that it is better to lose rice now. History makes monkeys very stupid, but most of us have our equivalent of rice. If you were asked if it is good to help starving children of the Third World countries with food, you would most likely have answered yes without hesitation. If someone 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, a fixed value is the belief that using reflection to access private methods is bad. 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, a fixed value is the belief that using reflection to access private methods is bad. 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, a fixed value is the belief that using reflection to access private methods is bad.


    Find out why using reflection to access private methods in this case is not a sacrilege
    In fact, invoking private methods outside the data type within which the method is declared is encapsulation violation. However, no matter how surprising it may sound, in this example, encapsulation is not broken. Conceptually, the parent class and the descendant class are the same data type. Parent code encapsulated from the outside world can be called in the heir (protected), while the parent can call the encapsulated methods of the heir (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 its internal structure too. Many developers are aware of this feature and prefer to use composition instead of inheritance (if the situation allows this).


    This example demonstrates how, using KISS and trying to make the code simpler, you can come to a better solution to the problem, even if your incorrect understanding of certain principles or taboos tells you to use a “crutch” instead of a declarative code that fully reflects the intention of the developer.

    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 aircraft building, cases are known where too hard workers nailed extra plates of armor on the plane to make the plane more tenacious in battle, as a result of which the mass of the aircraft became larger than the calculated one and the plane simply could not take off. In addition, the skills of many workers were low. Under such conditions, aircraft designs that a drunk unskilled worker could not assemble incorrectly, even if he wanted to, were of particular value. One of the echoes of the design decisions of that time was the inability to mix up and plug the wrong plug into the socket inside the computer. However, if the result of the work of an aircraft engineer is a drawing, by 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 drunken unskilled 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 decryption “Keep it simple stupid”, suitable in the aircraft industry, no longer reflects the essence of the principle for the programmer. Many lazy programmers interpret “make it idiocy simple” as “don’t bother designing yourself” (compare, for example, the description of the KISS principle in this article with this so that a drunken unskilled 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 decryption “Keep it simple stupid”, suitable in the aircraft industry, no longer reflects the essence of the principle for the programmer. Many lazy programmers interpret “make it idiocy simple” as “don’t bother designing yourself” (compare, for example, the description of the KISS principle in this article with this so that a drunken unskilled 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 decryption “Keep it simple stupid”, suitable in the aircraft industry, no longer reflects the essence of the principle for the programmer. Many lazy programmers interpret “make it idiocy simple” as “don’t bother designing yourself” (compare, for example, the description of the KISS principle in this article with this already not so well reflects the essence of the principle for the programmer. Many lazy programmers interpret “make it idiocy simple” as “don’t bother designing yourself” (compare, for example, the description of the KISS principle in this article with this already not so well reflects the essence of the principle for the programmer. Many lazy programmers interpret “make it idiocy simple” as “don’t bother designing yourself” (compare, for example, the description of the KISS principle in this article with thisdescription ). Fortunately, KISS also has some other decryption , 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 the example, I would like to thank Hokum , who provided the initial idea for the example, which I changed a bit.

    Also popular now: