I’ve been lucky to attend a very interesting meetup with Adrian Bolboaca recently at Arolla on the edgy topic of Evolutionary Design using Inductive or Deductive approach. It is about TDD of course, as Adi is a master of TDD. The topic could not escape my mind since then, so I’d like to share about it here, even if it remains not fully baked. I also hope Adi will share his material on all this soon.
Evolutionary Design is the art of growing a system by observing its natural traits then normalizing and optimizing its growth. — Adrian Bolboaca
In Adi words, normalizing means refactoring or any other transformation on the code, like extracting a Bounded Context out of a bigger piece of code.
The opportunity for transformations is when we add new behaviours or tests. Therefore,
The important thing is which behaviour to add and when.
Going too fast would be “like putting too much chemicals on plants and they die.”
Inductive approach of Evolutionary Design
Inductive reasoning is the derivation of general principles from specific observations. It’s about finding broad generalizations from concrete examples.
Behaviour Slicing from simple to complex
Behaviour Slicing in an inductive approach is a way to define the tests progression that we will follow to drive our Test-Driven Development. The technique is shown below on the flip chart, illustrated on a simple addition function:
Behavior Slicing:
- Start from the outputs (Equivalence partitioning)
- Find the possible inputs
- Order from simple to complex @adibolb pic.twitter.com/Fx3byzfd5p— cyrille martraire (@cyriux) October 25, 2017
Typically, we would start with a focus on the happy path cases before we would move to the negative path cases.
We then progress in Baby Steps. This includes Baby Steps in design, which Adi defines as:
Baby Steps for design is introducing only one concept at a time
Putting this in practice, we find out that the rule “Introducing only one concept at a time” in the tic-tac toe kata suggests the missing case of “empty board, nobody wins” that we haven’t noticed so far.
And if you can’t find a case without introducing more than one concept, then simplify the problem (even artificially), e.g. Reduce the 3x3 board to a 1x1 board to introduce just Token but not Position yet.
The goal in this inductive approach is to evolve from primitives to [clusters of] design concepts. We progress from generalizing small behaviour into design concepts (…) which we then generalize into design concepts, (…) which we then fragment into modules and Bounded Contexts. As commonly known in TDD circles, the trigger for a transformation is typically the “Rule of 3", which Adi explains:
We use the “Rule of 3” as a prerequisite for refactoring to avoid generalizing from accidental coincidences. And the number 3 is only indicative, sometime you need to go to up to 7 occurrences to be sure.
In this progression, the behaviours matter, not the implementation. We could re-implement later and the tests should still pass.
As in classical TDD As If You Meant It approach, from Keith Braithwaite, we follow a strict progression for introducing new design concepts out of the pre-existing one, going from Scalars to primitives, Field or Parameter, to Settings class, to Data Structure, Logic to Pure Function, Class Function, to Component:
As a consequence,
following a pure inductive approach is a “very functional approach: You can only create a class from one or more pure functions you have already.
Note that this is different from designing your failing test from your dream API (“Assuming it was done and perfect for me as a user, how would it look like?”); in contrast, here we even let the functions and their signature emerge from the behaviour underneath.
This inductive approach overall is closer to the “Bottom up”, “classicist” or “Chicago school” styles of TDD.
Deductive approach of Evolutionary Design
The deductive approach involves “More design upfront”, and the focus is more on collaboration (interaction) behaviour. From Wikipedia, Deductive reasoning is the process of reasoning from premises to reach a logically certain conclusion. Here it is not about generalizing from examples, but about starting from what is given (e.g. the behaviour expected by the user) and then propagate the consequences to the design.
We start by creating a Scaffolding, which can be a Guiding Test (GOOS), a Bounded Context with business entities (DDD), or a Walking skeleton (Cockburn). Then we deduce the design concepts depending on the scaffolding.
We find an entry point: typically what the user sees. Then we find the next layer of collaboration. We iteratively understand if we reach the end, for example if our guiding test is green, or when we reach the storage with a walking skeleton, and then we refactor the design concepts.
A big difference with the inductive approach is that:
the tests are generic from the beginning, they start by using design concepts early on, and the underlying representation needs to be addressed minimally.
The essentials of a deductive approach are the links between the design concepts, through their API and collaborators, as they would show in a dependency graph.
One drawback of a deductive approach is that:
it’s harder to test, and the tests make refactoring more difficult.
Behaviour Slicing, deductive style
Behaviour Slicing deductive style is different than in inductive style. We would inventory all the entities we have in mind, e.g. for the Tic-tac toe we would list Board, Game, Turn, Game Result, Position, Player, Token.
The Inputs and Outputs chart would include cases like: When game starts | Validate that the game has 3 players, and a board, which is empty.
From this coarse grain upfront design we can then implement from the outside, starting with mocks first, as in a mockist style.
After almost 3 hours of talking and live-coding, Adi concluded by inviting us at considering Tests as pressure, and from that:
We observe growth patterns that simplify the resulting system.
Adi also listed some anti-patterns, like considering that you know the resulting design in advance, and not listening to the design smells.
My takeaway
A lot of food for thoughts indeed:
A lot of food for thoughts during @adibolb session tonight, digging deep into the essence of evolutionary design through TDD. Thanks Adi! pic.twitter.com/JGdXEpx59a
— Arolla (@ArollaFr) October 25, 2017
A lot of food for thoughts, digging deep into the essence of evolutionary design through TDD.
“Inductive vs Deductive” is not fully equivalent to the “Mockist vs Classicist” dichotomy (or debate). It is more essential than that.
Inductive starts from the minimal description of the behaviour, introducing concepts when proven necessary. This means we start with primitives and built-in basic language constructs before we extract higher level concepts. This fits well for everything algorithm-ish.
Deductive is Contract-Driven, and fits better the edges of a system:
- Starting from you dream API from a Developer Experience point of view.
- Starting from given JSON contracts
- Starting from the domain language, the Ubiquitous Language
Note that in all three examples, you can probably implement in a deductive approach in a full classicist style, without mocks.
Also, in this view, conforming to the Ubiquitous Language is a kind of contract, as it’s a given and we try very hard to only name domain objects after the domain language.
When contracts are given, we have to follow them, so we are forced using a deductive approach. This typically happens on the edges of a system, with Web-Services and integration to the infrastructures.
When there’s no given contract, we prefer to let things emerge out of necessity. But we may also decide to introduce intermediary contracts for our own design reasons, e.g. to split the work between teams, or to limit each module to a manageable (small) size.
However we may negotiate and evolve contracts, and we would for sure evolve the Ubiquitous Language in a mix of deductive (using the words from the problem statement) and inductive (driven by necessity from refactoring the behaviour). Most contracts evolve from the implementation feedback: “there’s a concept of CalculationPreferences missing in the contract”, or “we got that concept of End Date wrong in the contract, the date should not be a date but a number of days after this other date.”
Mixing inductive and deductive approaches
I tend to think that we usually we have a mix of both deductive constraints and inductive freedom.
Every system has edges where it interacts with external actors. For the system to be valuable, it’s good practice to start with the best contracts from the client (external actor getting value from our system) perspective: the ideal API, or the ideal REST interaction.
Note that for providers it can be the other way round: define our ideal contracts from the internal needs of the system.
When we decide to follow a Domain-Driven approach, we explicitely decide that we want the domain model to conform to the Ubiquitous Language of the business. It’s a decision to be Conformist, in the DDD Context Mapping sense. Therefore we are constrained by the language (don’t worry, it’s an enabling constraint) but we have a lot of freedom to induce lower-level implementation concepts, and even to suggest changes to the Ubiquitous Language.
All this also suggests we are getting increasingly closer to really understanding how design works. Could this help automate design one day?
Directeur Technique d'Arolla
Article très intéressant qui explique la nécessité de connaître les 2 approches et de ne pas chercher à les affronter.
Merci pour se partage.