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:
- We created a test “script”
#runScenario
sending a #runScenario
message to #runScenario
external block (the term “closure” may be closer to someone). - They said that
oven
is a surrogate object (Mocketry automatically initializes the parameters of the script block accordingly). - They said that when a baker is asked to make bread (the first nested block), the stove should receive the message
#produceBread
(in the second nested block, passed as an argument to the message #satisfy:
. In fact, this is the first condition of the test. - In addition, we asked our fake stove in response to this message to return some object (
#bread
), which in the future should be the result of the original request for baking bread. The identity of these objects is the second condition of the test. And here we are not interested in the nature of this resultant object, but only its identity is important to the object that the furnace produced. Therefore, in this role we use, in fact, a simple string constant: #bread
.
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:
- 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.
- 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:
- A request to the baker to cook something is accompanied by a recipe.
- The baker transmits the same recipe to the stove (in reality, most likely, this is done in some other way, but we now know nothing about it - so we make it as simple as possible)
- What the baker gets from the stove gives out as the end result
- The connection between the recipe and the final product, unfortunately, remains “behind the scenes” - simply because nothing is clear about this (yet?).
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 ...