Marcus Bread and YAGNI

    imageRecently, two Heroes appeared on our newsfeed, baker programmers - Boris and Marcus. Boris is a good person and a perfectionist, and Marcus is a very modest and gray programmer who does not want to stand out. Both strive for the best and want to be of service. But it seems that Marcus did not try very hard.
    This is a new branch - continued. Today, the storyline only touches Marcus. He is the main character.
    So, the story is under the cut.

    Original post: How two programmers baked bread

    Introduction

    I felt that in the original post a lot of attention was paid to Boris and very little to Marcus. Perhaps because of his modesty. The post was fun, I and many, judging by the comments, liked it. And I am very glad that they shot at the astronauts of architecture. And Marcus was in a winning position. But that was only the first salvo, it was time to set up the scope. There was a trick in the original post - what Boris did was fully revealed, and what Marcus did was left in the background behind the external interface. It was implicitly assumed that there was a terrible spaghetti code.

    Today I will try to rehabilitate the position of Marcus. The post will be devoted to the principle of YAGNI. This is just an example of using YAGNI and is based on personal experience. Unfortunately, there are not many books that show examples of how to apply this principle. And most programmers, in addition to reading books, give birth to these skills through experience and work. I believe that these are the code skills, and not just the theory. And such an experience would be nice to share. I will be glad if I also learn something new from you. This post is devoted only to practice and the task will be considered in C #. Which, unfortunately, can reduce the potential audience. But I see no other way out, because the theory itself cannot be accepted by opponents until they see real possibilities. UML class diagrams I do not like. But for Marcus, apparently, they were not needed.

    I ask you also not to pay very much attention to gaps in the style and code of C #, because I would just like to show the essence of the approach, as I understand it. And do not spray your attention to the little things. Also, I will not show the TDD approach, I will not show how to write unit tests, otherwise even for such a simple task the post would be very voluminous. Although, TDD, of course, would have made its own adjustments to the code. But we are only interested in whether Markus would have gotten so bad if he had used YAGNI, as it seemed from the original post.

    And of course, I will write trivial things. With many, judging by the comments, I’m like-minded and they would write such a post no worse. And some are even much better (using a functional approach).

    Let's get started. Let's go through the entire chain of requirements. I am Marcus. True, I am a little different Marcus and do not behave exactly like the previous one.

    Requirement 1
    - Guys, we need bread to be made.

    Analysis

    To make bread, you need a method. What is bread? This is a certain entity. This is not an object, otherwise it can be confused with other objects. It is not an int or any other built-in or created type. So bread, this is a new separate class. Does he have a condition or behavior? Personally, I know that it consists of dough (flour, wheat or rye ...), that it can be bought in a store and that it can be eaten. But this is my personal knowledge. The customer has not said a word about any behavior or condition. And since I’m a lazy person, even though I know a little more, I won’t press a button again, without directly indicating what the customer wants.

    We proceed only from the requirements: bread does not have a state and behavior, and we also need a method for obtaining bread. The C # language itself, unfortunately or fortunately, requires a little more fuss, namely: to define a method in any class. But since the customer didn’t say a word about it, we don’t bother with the name, we don’t bother with the instances, so far I decided to make a static method. If anything, we will always have time to redo it. I choose a name that matches the most average understanding of the requirements. So the first code is:

    class Bread
    {
    }
    class BreadMaker
    {
        public static Bread MakeBread()
        {
            return new Bread();
        }
    }
    


    Requirement 2
    - We need bread not only to be made, but baked in the oven

    Analysis.

    Sometimes customers try to indicate how to implement something, instead of saying what they want. The customer’s wishes for implementation do not affect me either physically or psychologically. In this case, this is not a requirement. The customer has no way to check where I got the bread. And until he even expressed such a desire - to check. Therefore - how I receive bread and give it to him is not his customer business. But I can politely agree with the demand and be glad that they pay further money for idleness.

    I’m not doing anything yet. But just in case, I remember the stove. The customer did not indicate the oven, nor their differences, nor their different effects on bread. The latter is also important. Even if the customer indicated several types of ovens, then there is no place to rush anyway - the bread is the same.

    But still, we will make pleasant things for the bosses and slightly correct the code so that it matches the meaning. Namely: we already know that bread will be baked in the oven, and not bought in a store. Just rename the method of getting bread:

    class BreadMaker
    {
        public static Bread BakeBread()
        {
            return new Bread();
        }
    }
    


    Requirement 3
    - We need a gas furnace to not be able to furnace without gas

    Analysis

    Oh! New information has arrived. It turns out that there is a gas furnace and its behavior is different from other furnaces. True, the topic of other furnaces is not disclosed again. Well, okay. Let them be different.

    Let's try to compare several implementations.

    Implementation 1.
    enum Oven
    {
        GasOven,
        OtherOven
    }
    class BreadMaker
    {
        public static double GasLevel { get; set; }
        public static Bread BakeBread(Oven oven)
        {
            return oven == Oven.GasOven && GasLevel == 0 ? null : new Bread();
        }
    }
    


    The side effect is immediately evident. The gas level is set separately from the BakeBread () call. Once there is a gap, a wide field of opportunities for the appearance of bugs opens. The appearance of these bugs (bugs) can damage our field, then there will be no wheat and, therefore, bread.

    With such separate parameter settings, the user of our code (and we can be this victim user) may well forget to set the gas level before starting the gas furnace. And then the gas level may remain from the previous setting of the oven, when we baked bread earlier. Which will lead to unpredictable behavior if we really forget.

    We also see that the property is static. Which is also very bad - we have only one gas level. But getting rid of static methods and properties will not solve the problem described above, so we do not consider this option.

    Implementation 2.

    enum Oven
    {
        GasOven,
        OtherOven
    }
    class BreadMaker
    {
        public static Bread BakeBread(Oven oven, double gasLevel)
        {
            return oven == Oven.GasOven && gasLevel == 0 ? null : new Bread();
        }
    }
    


    Pretty simple. And a little better than the previous implementation. But consistent parameters are not always passed to the BakeBread () method. For non-gas furnaces, gasLevel makes no sense. And although the method will work, the task of gasLevel for non-gas furnaces will confuse users of our code. And the correctness of the parameters is not checked at the compilation stage.

    Implementation 3. In order to agree on the parameters, the furnace will have to be made to seem like classes.
    Moreover, the usual bakes bread always, and gas is not always. Those. two classes, virtual methods, overload. But you need to think about how to do access modifiers for them so that the ovens do not create on their own, but use my BakeBread () method, otherwise, side effects will appear.

    And then it dawns on me (Marcus)! At this stage, just do this:

    class BreadMaker
    {
        public static Bread BakeBreadByGasOven(double gasLevel)
        {
            return gasLevel == 0 ? null : new Bread();
        }
        public static Bread BakeBreadByOtherOven()
        {
            return new Bread();
        }
    }
    


    Indeed, the customer has not yet said a word how we will use the stove. Such code at this stage is quite satisfactory.

    Requirement 4
    - We need so that the ovens can also bake pies (separately - with meat, separately - with cabbage), and cakes.

    Analysis

    Yes, no question! And if you set the temperature level in the oven, so we can bake ice cream in it. Joke. I, Marcus, try to be serious - not a word about temperature. Who knows you, customers))

    So, pies and cakes. Moreover, pies of two kinds. But this, we know from life that the meat pie and the cabbage pie have more in common than the cake. But in the context of the task, the customer did not talk about this. He did not say that we would somehow group the pies separately, the cakes separately. Therefore, so far, based on the requirements - the cake behaves almost like a pie with cherries - they are all equal. Do they have any behavior? Not. Is there a condition? Not. So, to distinguish them from each other, it is quite enough for us to make an enumeration. And to run ahead, guessing the wishes of the customer that will arise tomorrow, we basically do not want to. So - the enumeration is the most correct. Probably. Not sure. But do not. It will always be possible to rewrite, if that.

    In parallel, we change the names, now we are baking not bread, but bakery products.

    public enum BakeryProductType
    {
        Bread,
        MeatPasty,
        CabbagePasty,
        Cake
    }
    public class BakeryProduct
    {
        public BakeryProduct(BakeryProductType bakeryProductType)
        {
            this.BakeryProductType = bakeryProductType;
        }
        public BakeryProductType BakeryProductType { get; private set; }
    }
    class BakeryProductMaker
    {
        public static BakeryProduct BakeByGasOven(BakeryProductType bakeryProductType, double gasLevel)
        {
            return gasLevel == 0 ? null : new BakeryProduct(bakeryProductType);
        }
        public static BakeryProduct BakeByOtherOven(BakeryProductType breadType)
        {
            return new BakeryProduct(breadType);
        }
    }
    


    Requirement 5
    - We need bread, cakes and pies to be baked according to different recipes.

    Analysis

    A quick glance at the code, we notice that we have a wonderful listing of BakeryProductType. It is called somehow clumsily, somehow programmatically, not close to the subject area. But it behaves like a recipe. In what happens! Bread and rolls are baked according to recipes, and not by type. And with us, probably, the recipe gets into the roll designer. Enough to rename. The only disorder is the “bun” type property. But I would have resigned myself. Looking mechanically at the code and imagining the subject area as some sets, I don’t see much difference between the recipe and the type. Those. the recipe is the direct reason for what happens later. Of course, in life, we know a little more about recipes - they not only describe what happens. They also contain an acquisition algorithm. But who cares? Did the customer talk about this? Not. Means in the context of the task this was not so. We will need an algorithm - we will bind later, we’ll come up with something.
    Therefore, I put up with the fact that the property will remain a type, and the enumeration will remain a recipe. Do not create a bunch of heirs or another enumeration because of the properties of our spoken language. In the context of the task, everything is accurate. Although not very beautiful. Compromise?

    public enum Recipe
    {
        Bread,
        MeatPasty,
        CabbagePasty,
        Cake
    }
    public class BakeryProduct
    {
        public BakeryProduct(Recipe recipe)
        {
            this.BakeryProductType = recipe;
        }
        public Recipe BakeryProductType { get; private set; }
    }
    class BakeryProductMaker
    {
        public static BakeryProduct BakeByGasOven(Recipe recipe, double gasLevel)
        {
            return gasLevel == 0 ? null : new BakeryProduct(recipe);
        }
        public static BakeryProduct BakeByOtherOven(Recipe recipe)
        {
            return new BakeryProduct(recipe);
        }
    }
    


    Requirement 6
    - We need bricks to be burned in the kiln.

    Analysis

    If you literally follow this requirement and all the written requirements, then the brick is no different from a cake or bread. The funny thing is that our brick is much more different from the jam pie, because we do not have it in the requirements, and so-so from the meat pie. Just like from bread. Therefore, this requirement according to YAGNI, has been greatly exaggerated, is realized only by expanding the enumeration of recipes with renaming all classes - rolls in the “oven product”, which is also a brick, etc. The whole point of how to create a class architecture is how it will be used. It is from what is considered common (i.e. the state and behavior of the base class) and what is private (state and behavior of the heirs). If there is neither one nor the other, then an enumeration is also possible. It's not scary not to guess.

    Did any of you see the horror in the code? Maybe this code is difficult to test? Yes, it seems, Boris’s code is much more difficult to test. Volume is more, more tests. More functionality than required? More tests.
    Of course, apparently, the original post implied that the requirements were more detailed and each phrase was clarified with detailed explanations. But the YAGNI genre requires not to think out.

    Let's play the requirements further.

    Requirement 7
    - How did you not inspect? Not every stove can burn bricks. To do this, you need a special oven.

    Analysis

    Well, okay. We remove the brick from the enumeration (recipes?) And return the names. Create a separate empty Brick class. And a new method:

    public static Brick MakeBrickByFurnace()
    {
        return new Brick();
    }
    


    By the way, the abundance of methods where everyone produces exactly some kind of object is better than some flexible way of creating objects, if flexibility is not required right now. If flexibility is not required, then the program should allow less, be more limited. We do not consider unit tests, where it is often convenient to replace objects of specific types with interfaces. All this code is easily translated into interfaces on occasion. Yes, and C # with its reflection is not very demanding in testing for some kind of decoupling.

    Further, the customer decided to play against Marcus.

    Requirement 8
    - Each recipe must contain products and their quantity (weight). Recipes in the demand are attached.

    Analysis

    The first blood that Boris was waiting for.

    Let's try to cope with the terrible spaghetti code, which should have been formed here for a long time and not give us any chance to refactor. Is it so?

    Products for the recipe are obviously listing. The recipe itself already contains not only the name of what it will create (or what is the same, the name of itself), but also a set of products with their quantity. But at the same time, we notice that a strict set of products is associated with a specific recipe and it does not change. (Once again, we recall that the post about YAGNI - no, “what if the reserve wants it to change!” No, suddenly, today it’s today, and tomorrow it’s tomorrow).

    Those. the customer did not say that the products and weight in the recipe may vary. Of course, he did not say that they should be fixed. But the fixed case is more limited and strict. And we always choose more limited cases. For us, it’s better not to be more flexible, but to be simpler and stricter.

    Yes, and a recipe with a strict selection of products - better matches personal experience. It follows that in this case it is not practical to use inheritance and write a class for each recipe. Then each class will store just constants.

    And a couple more thoughts. Because At the moment, the recipes in the code are just a given enumeration and it was set before compilation, then, if there are no other requirements, apparently this behavior should remain. From this, it follows that all recipes should be available to us and they are set directly in the code. It is impossible to create a new one without an enumeration extension. From here, it seems, you need to make the Recipe class, after renaming the enumeration with the same name in RecipeName. The world is so volatile. Now the listing just points to the recipe and allows you to choose it, but does not fully characterize it.

    To satisfy the conditions above, just like this:

    public enum RecipeName
    {
        Bread,
        MeatPasty,
        CabbagePasty,
        Cake
    }
    public enum RecipeProduct
    {
        Salt,
        Sugar,
        Egg,
        Flour
    }
    public class Recipe
    {
        private Recipe() { }
        public RecipeName Name { get; private set; }
        public IEnumerable> Products { get; private set; }
        private static Dictionary> predefinedRecipes;
        static Recipe()
        {
            predefinedRecipes = new Dictionary> 
                { 
                    {
                        RecipeName.Bread, new Dictionary
                                            { 
                                                {RecipeProduct.Salt, 0.2},
                                                {RecipeProduct.Sugar, 0.4},
                                                {RecipeProduct.Egg, 2.0},
                                                {RecipeProduct.Flour, 50.0}
                                            }
                    }
    		   ..................
                };
        }
        public static Recipe GetRecipe(RecipeName recipeName)
        {
            Recipe recipe = new Recipe();
            recipe.Name = recipeName;
            recipe.Products = predefinedRecipes[recipeName];
            return recipe;
        }
    }
    


    You don’t have to break anything. To create a product, the name of the recipe is enough for now. We don’t change anything there. It will be necessary, we will pass the recipe itself.

    In this code, we just made a class from a recipe listing and associated the name of the recipe with the products that make it up. It will be necessary to express the sequence of actions in the recipe, just as it can be “screwed”. I hope this is clear and there will be no spaghetti code. There will be a separate behavior for the classes - easily the Recipe class becomes the base class and the heirs appear. But we don’t think about it. We have YAGNI, we were not told to do this. But we are not afraid of this.

    Requirement 9 The
    vile customer, having learned about our unplanned approach, decided to catch it.
    “I want the recipe to change.” And the stoves were cooked according to any recipe drawn up by the cook.

    We enter into a polemic:
    - How to change?
    - We believe that the cook does not know the recipes and can experiment. Have you made the recipes fixed? And he wants to add a different amount of eggs, sugar, etc.
    - And what kind of bun will we get in this case? Should we get something? Bread, cakes or cake? Obviously, if the recipes are different, then the cook will bake something else.
    - I think that the cake is different in taste, more sweet, less sweet. Also bread. So, to some extent, the recipes may differ, but we will get some product from the list.
    - I.e. to find out what we get, we need to look for the closest recipe to that list of products in the cook’s recipe?
    - Yes.


    Analysis

    We have fixed recipes. Now recipes may not be fixed. But those that we have are benchmarks. To allow users of our code to create their own recipes, just make the constructor open. But it is also necessary to give the opportunity to ask products. I do not want to give the opportunity to assign a property to users or specify a specific type. Otherwise, he will be able to damage our standards. So the easiest way is to give the opportunity to transfer products to the designer. It will also eliminate the gap between creation and initialization and, therefore, reduce the likelihood of a bug.

    Now we have two constructors:

    private Recipe() { }
    public Recipe(IEnumerable> products) 
    {
        Dictionary copiedProducts = new Dictionary();
        foreach (KeyValuePair pair in products)
        {
            copiedProducts.Add(pair.Key, pair.Value);
        }
        this.Products = copiedProducts;
    }
    


    A second constructor creates a copy. This is because of the C # properties - pass default links. The class user will have a link, and if you do not make a copy, he will be able to change the ingredients of the recipe later. What is not included in our plans.

    Also, in this post, I try not to use lambdas and zheneriks less, remaining within the framework of standard OOP. So that a larger audience can understand what I'm doing. The code could have been written differently and simply. But my goal is to describe the YAGNI principle itself and some ways of evaluating the code, and not show the different possibilities of the sharpe. Of course, assessment methods depend on the language and its capabilities.

    The second constructor, which for users, does not set the value of the property - the name of the recipe. Because our products are passed to the designer and can’t change, then there and somehow calculate the proximity. More precisely, especially the "smart" can change, but we will not suffer from paranoia. We believe that the developers are adequate and are in the position of creation, not destruction.

    We need to write some kind of proximity method. The customer did not specify, so we will write the simplest one with the least squares method. Given that each ingredient has a different "weight". For now, we’ll write down some weights that you can adjust later.

    The code is approximately like this:

    private double GetDistance(Recipe recipe)
    {
        Dictionary weights = new Dictionary();
        weights[RecipeProduct.Salt] = 50;
        weights[RecipeProduct.Sugar] = 20;
        weights[RecipeProduct.Egg] = 5;
        weights[RecipeProduct.Flour] = 0.1;
        double sum = 0.0;
        foreach(KeyValuePair otherProductAmount in recipe.Products)
        {
            var productAmounts = this.Products.Where(p => p.Key == otherProductAmount.Key);
            if (productAmounts.Count() == 1)
            {
                sum += Math.Pow(productAmounts.First().Value - otherProductAmount.Value, 2) 
                                     * weights[otherProductAmount.Key];
            }
            else
            {
                return double.MaxValue;
            }
        }
        return sum;
    }
    private RecipeName GetRecipeName()
    {
        IEnumerable etalons = ((RecipeName[])Enum.GetValues(typeof(RecipeName)))
            .Select(recipeName => Recipe.GetReceipt(recipeName));
        IEnumerable> recipeNamesWithDistances = etalons
            .Select(e => new KeyValuePair(e.Name, GetDistance(e)));                
        double minDistance = recipeNamesWithDistances.Min(rd => rd.Value);
        if (minDistance == double.MaxValue)
        {
            throw new Exception("Подходящий рецепт не найден");
        }
        return recipeNamesWithDistances.First(rd => rd.Value == minDistance).Key;
    }            
    


    And in the constructor call, respectively, the naming is added:

    public Recipe(IEnumerable> products) 
    {
        Dictionary copiedProducts = new Dictionary();
        foreach (KeyValuePair pair in products)
        {
            copiedProducts.Add(pair.Key, pair.Value);
        }
        this.Products = copiedProducts;
        this.Name = GetRecipeName();
    }
    


    It would be more reliable to always calculate on the fly. But then one would have to come up with a separation of naming for the reference and for non-fixed recipes. This is enough for now.

    As you can see, it’s not at all scary to redo the code to meet such a current requirement. We didn’t break anything much. And just expanded. There is no more time for completion than if we had foreseen in advance. But to predict and not guess is really scary. Imagine this, like a simple code, according to this requirement, but made earlier. This is just a monster that requires extra testing. And now it is written reasonably.

    The code is not perfect, I could not help but rush into zheneriki and lambda. So the code gets smaller and cleaner. I hope this doesn’t hurt the understanding of unfamiliar readers with C # and lambdas. Of course, it can be further reduced. But I'm trying to be understood by more people.

    Here I have already tied to a specific algorithm, although this customer did not require it. YAGNI or not? Here we understand the situation. Perhaps the customer immediately needs a visible result. This often happens. Therefore, at least some algorithm is needed. But if later we need a different algorithm, it costs nothing to replace this one with another. Or even write a few and choose. Or even from users of the code to take a delegate who will do a comparison on proximity.

    Clear business, now it is necessary to transfer not the name of the recipe to the manufacturing methods, but the recipes themselves. Those. like this:

    public class BakeryProduct
    {
        public BakeryProduct(Recipe recipe)
        {
            this.BakeryProductType = recipe.Name;
        }
        public RecipeName BakeryProductType { get; private set; }
    }
    


    AND:

    public static BakeryProduct BakeByGasOven(Recipe recipe, double gasLevel)
    {
        return gasLevel == 0 ? null : new BakeryProduct(recipe);
    }
    public static BakeryProduct BakeByOtherOven(Recipe recipe)
    {
        return new BakeryProduct(recipe);
    }
    


    Here, refactoring is far from over-stressed.

    Requirement 10
    The insidious customer has somehow studied our code and is looking for the most painful place, for which our code is completely unprepared. After all, we should already have unreadable spaghetti 9 times. We wrote without a plan, and our code is terribly not flexible. Apparently Boris entered into a conspiracy with him and they are looking for weaknesses.

    - And now I need that everything that the furnaces produce can be sold in a store and this is called a product. The store must place a certain amount of goods, each product has a price and you must be able to calculate the price of all goods in the store. At the same time, the workers who make bricks and rolls are not qualified and do not know how to make them. They simply fill the raw materials in the stove (abstract))), and get the product, i.e. goods being taken to the store.

    Analysis

    Prior to this, sometimes unrelated things were made in our ovens. Brick, for example. He has no common ancestor with rolls. And rightly so, how could we know what they have in common? No, of course, in life we ​​know about bricks and about bread. But we could not believe that the customer would want to consider them later as a product. Then, we have three different methods that do not overlap very much. There is no one method that produces any thing. But should it? There was no ancestor. Should not.

    Then, the class hierarchy is rather evil. Like any extra line of code. The fact that we have at the moment before this requirement had separate methods that return specific classes is better, safer. Imagine that we would immediately make a furnace that does something through a single method. How did Boris. And so she would release Product. What is a product? This is the base class. Which has basic behavior and state. But we, for example, in the user code needed to make a pie. Here, the oven makes the product. Not a pie. Those. this pie is actually only when we get it out of the oven, it is called a product. And suddenly we needed to find out how much meat was in it. And this behavior is in the heir, namely in the meat pie. What should we do then? Bring a product link to a link to its real type.

    And here you just need to wait for trouble. When casting from the base type to the successor, the compiler can no longer verify the correctness of the cast. Those. the severity of typing is violated. It is necessary to use reflection in the user code, find out which real type or try to cast, and then create branches, throw executions if something is wrong, etc.

    Those. premature flexibility is not only not beneficial, it is harmful.

    But now we need it for real.
    So, we ourselves formulate succinctly the requirements that have developed at the moment: “Loafs are prepared according to the recipe, no bricks. Bricks can only be made in a special oven, and only bricks (not rolls) can be made in it. We need a single mechanism for loading raw materials and receiving goods. Buns and bricks are a commodity that has a price. ”

    The latter is implemented simply. At buns and bricks a common ancestor appeared - the goods.

    public abstract class Article
    {
        public double Price { get; private set; }
        public Article(double price)
        {
            this.Price = price;
        }
    }
    


    Inherit classes:

    public class BakeryProduct : Article
    {
        public BakeryProduct(Recipe recipe, double price): base(price)
        {
            this.BakeryProductType = recipe.Name;
        }
        public RecipeName BakeryProductType { get; private set; }
    }
    public class Brick: Article
    {
        public Brick(double price) : base(price) { }
    }
    


    And we refactor calls to manufacturing methods, passing the price that the user sets to the designers.

    Something like this:

    public static BakeryProduct BakeByOtherOven(Recipe recipe, double price)
    {
        return new BakeryProduct(recipe, price);
    }
    


    This is almost half the battle. There are little things left. But for the sake of shortening the post, I will simply describe what needs to be done. You need to create one method that would return the goods. For consistency of parameters it is necessary to make the class Raw materials and heirs, which will be - raw materials for rolls and raw materials for bricks. Raw materials for rolls, of course, also contain a recipe. Classes are necessary because just by a set of parameters (gas level, recipe, etc.) we can transmit strange parameters, which makes the program unreliable. In fact, the classes of raw materials are methods of coordinated packing of parameters.

    In a single method for receiving goods, we can in the simplest case use a switch and select the desired method, which is already there, to produce what is needed, depending on the raw materials. I would have done in this case. With a small number of elements in the enumeration, this does not really clog the code. With an increasing number of elements, you can think of other ways. For example, about an abstract factory. Using overloaded methods in it, you create raw materials and a stove at the same time, which can work with this raw material.

    As you can see, there is no difficulty in transforming the architecture on the go, as requirements arrive. There is no difficulty covering this with tests. The methods are not great. Moreover, such code is easier to cover with tests, because it is smaller. Code without predictions is always in a more or less flexible state in all directions. Moreover, it is quite tough. Boris’s code slipped a third of the way. And this can be transformed ad infinitum. For transformations to be possible, refactoring must always be done. The YAGNI principle only says that you need to implement only minimal functionality. But in no case does he say that if the code works, then do not touch it. For refactoring and unit tests, the YAGNI principle does not apply. Only then does such development technology work.

    Naturally, the code in the post is not perfect. The goal was only to show the principle. I am sure that with many particularities in how I did the requirements analysis, and YAGNI supporters will not agree. Everyone has their own personal experience. Each has its own methods and techniques. And it still depends on the programming language, because it is a means of expressing thoughts.

    Also popular now: