📜 ⬆️ ⬇️

Small talk about dough-driven baking

Something like a preface


The article “How two programmers baked bread” at first seemed to me just a joke - attempts to build some kind of “design” look so absurd, based on the “requirements” that the “manager” puts forward. But in every joke there is some truth ... In general, the question arose to myself: how will the approach that I try to adhere to in my practice work in this situation? What has grown when trying to give an answer, in fact, is presented below.

TDD + Smalltalk


Actually, the essence of the approach I used is in the title, but I think some clarifications are required here.
I am a supporter of “pure” TDD (TDD is a test-driven development , hence the feminine). “Clean” in this case means that at certain stages of software development, namely, immediately after receiving functional requirements and before the next release (that is, at the stages of requirements analysis, design and construction of program code), the developer acts in full compliance with this methodology, without deviating from it.

“Clean” TDD is provided by combining the “classic style” of creating tests (based on state analysis) and “TDD with surrogate objects (mock-s)” (based on analyzing the interaction of the developed subsystem with other objects). Mocks allow to design the system “top-down” (to carry out functional decomposition, creating an “empty frame” of the system), and the classic TDD then provides the elaboration of the bottom-up implementation. And, in my experience, this approach is very well (at least, better than all mainstream alternatives I know) combined with the use of the Smalltalk environment. Here I will not go into details, leaving them for the following articles, but simply offer to see how this approach will work on this somewhat strange, but from this and something interesting example.

Step 1. “Guys, we need bread to be made”


We create the first test, at the same time inventing the necessary terminology (if you wish, you can probably call it a metaphor).
')
So far the only thing we know: at the exit, the system gives out "bread". Since no functionality in the existing “staging” with bread is related, there is a desire to simply check the output object for belonging to the corresponding class ( Bread ).

And who makes the bread? A baker, probably ... Accordingly, in the test we fix the following functional requirement, which we managed to pull out of the existing “statement” of the problem: you can ask the bread maker to make bread, in response to which he must give us an object of the corresponding class. This requirement relates to the functionality of the baker, the class for the first test is called (so far) simply BakerTests , and in it we create a test:

 BakerTests >> testProducesBread | baker product | baker := Baker new. product := baker produceBread. product class should be: Bread 

The implementation of the test is as trivial as it is.

Summing up this iteration, we can note for ourselves that absolutely no requirements are made for bread, the assignment of the appropriate class is not clear and it certainly makes sense to try a manager about it: this is why we have to limit ourselves to such a trivial test, and it seems that We did not start the development from the highest level of abstraction, and this often leads to problems later on. Nevertheless, in the framework of our example, we will assume that the first iteration is complete.

Step 2. “We need bread not just to be made, but baked in the oven”


We fix the arriving crumbs of new knowledge about our system in the test. What did we find out? Only that the baker in the baking process interacts with the oven. To fix this, surrogate objects will be useful to us (after all, they are intended for such things). I use the mocketry framework. With him and I got this code:

 BakerTests >> testUsesOvenToProduceBread | product | [ :oven | baker oven: oven. [ product := baker produceBread ] should strictly satisfy: [ (oven produceBread) willReturn: #bread ]. product should be: #bread ] runScenario 

Here we did the following:

I also note that a slightly refined version of the test is presented here: we have already got rid of duplication associated with creating a baker in both tests, “pulling” it into an instance variable and initializing it in the #setUp method, which is automatically called before each test #setUp :

 BakerTests >> setUp super setUp. baker := Baker new. 

I note that in the process of writing the test I had to make the following decision: the baker knows in advance what kind of stove he uses - it becomes part of his condition. This decision is actually not very important, because if necessary, it will be easy enough to change: if the stove becomes known only at the time of the work, we add a parameter to produceBread ; and if it should be obtained from somewhere else, we introduce an object that at the right moment will give us the desired furnace.

To implement this test, slightly alter the #produceBread method in the baker:

 Baker >> produceBread ^ oven produceBread 

In the process of compiling this method, the system is interested in what an oven is. In response, we explain that we want to create an instance variable. After that, running the test, we see the debugger message and understand that the system’s discontent is due to the lack of the required setter. Create it directly from the debugger without interrupting the test:

 Baker >> oven: anOven oven := Oven new 

Right there, at compile time, create the Oven class.

Continuing to perform the test, we see that he successfully fulfills. But running all the tests in our system (and there are already two), we see that the first one broke. If the reason is not obvious in advance, we can easily find out from the diagnostic message or by analyzing the state of the system on the current stack in the debugger: the stove is not set. Well, we will provide a stove by default (here I ’m thinking that, like in Squeak and Pharo , Object already has a call to the #initialize method when creating an instance - in other Smalltalk environments it’s very — yes, actually, very — it’s just that by ourselves):

 Baker >> initialize super initialize. oven := Oven new. 

We start the test - the system reports that the #produceBread method in the Oven class is not implemented. We realize right there:

 Oven >> produceBread ^ Bread new 

Continue execution - the test turns green from correctness. All (both) dough is now green. Let's move on to refactoring ... And, it seems, there is nothing to refactor (which is understandable, because we almost do not write the code - thanks to PM for our happy programming).

The result obtained after this iteration, as well as after the previous one, looks somewhat doubtful: the baker himself, it turns out, does almost nothing. But what happened is the simplest solution in the given conditions. In short, all questions are for PM :)

Step 3. "We need to have different types of stoves"


Again: why do you need - history is silent. But since we have accepted the conditions of the game, we play: for each desired type of stove, you can create and implement it by test. However, it immediately turns out that we don’t have to implement anything with such a “staging”! Let us verify this by the example of a gas stove (the only one that we will need later):

 GasOvenTests >> testProducesBread | oven | oven := GasOven new. oven produceBread should be a kind of: Bread 

But, it is logical to trace the gas stove from Oven , where #produceBread already implemented, we immediately get the test in green. In general, this is a bad symptom: we seem to have written a meaningless test. The accusations against the manager become a common place, I miss them ... :) Perhaps in a real task, some functionality is connected with different types of furnaces, but in this case it is covered in such darkness that there is no sense in fantasizing.

Step 4. “We need a gas stove to not be able to burn without gas”


Again, more questions than answers will have to be thought out. The simplest, but, it seems, solution to this formulation looks like this for me:

 GasOvenTests >> testConsumesGasToProduceBread [ :gasProducer | oven gasProducer: gasProducer. [ oven produceBread ] should strictly satisfy: [ gasProducer consume ] ] runScenario GasOven >> produceBread gasProducer consume. ^ super produceBread >> gasProducer: gasProducer gasProducer := aGasProducer 

Does the stove hardly change the source of gas at its discretion? And how exactly the consumption takes place is not yet clear - therefore, we simply inform the source about the very fact of consumption.

The solution is essentially identical to the previous one, which is easily explained - the tasks are set in the same style, and accordingly we solve them with similar, once already worked methods (after all, there are no problems identified).

Like last time, one test broke (gas source is not set by default), we fix:

 GasOven >> initialize super initialize. gasProducer := GasProducer new. GasProducer >> consume 

- yes, this method (I hope, for now) is left empty, since no specific requirements have been set for it.

Step 5. “We need the ovens to bake more pies (separately - with meat, separately - with cabbage), and cakes”


Again the fog: what does it mean to bake pies? How are they different from bread? and from the cake? I saw two options:

  1. These products differ in some of their properties (or rather, behavior) - but we don’t know anything about it, so this option does not give us anything in this situation. We reject.
  2. Products differ in manufacturing method. This option is more productive in terms of knowledge of the system: to create a product, you must specify the method of its manufacture. We fix this in the test.

What do we call the method of manufacture? In my opinion, this is a recipe ...

 testUsesOvenToProduceByRecipe | product | [ :oven | baker oven: oven. [ product := baker cookWith: #recipe ] should strictly satisfy: [ (oven produce: #recipe) willReturn: #product ]. product should be: #product ] runScenario 

Here we fixed the following:

You can do a few more iterations, “throwing” tests on various types of recipes ... but for this I would like to know something about how this should work. You can, of course, dream up, but it is a pity to have time ... Therefore, we proceed to the next item.

Step 6. “We need bread, cakes and pies to be baked according to different recipes”


We, it seems, have already done this ... well, how could.

Step 7. “We need to bake bricks in the furnace”


If we assume that a brick can be baked by a baker by a recipe (and why not? We have not received any information that contradicts this), then we don’t need to do anything again ... well, except to add another test to the collection of tests that we have not (yet) done recipes.

In general, everything seems to be ...

Result


What have we got? Six classes ... and not very much (even frankly, just a little) of functionality ... But personally, I am inclined to "thank" our manager for this.

 Baker Bread Oven ElectricOven GasOven GasProducer 


It will be interesting to hear your opinion about the result and the process ...

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


All Articles