Wednesday, August 25, 2021

TDD: Thinking in Top Down

We did so by writing high level tests that were checking special patterns (gliders …). That was a nightmare ! It felt like trying to reverse engineer the rules of the game from real use cases. It did not bring us anywhere.  -- Philippe Bourgau

I don't have this problem when I work top down.

The reason that I don't have this problem is that the "rules of the game" are sitting right there in the requirements, so I don't have to reverse engineer them from a corpus of examples.

What I do have to do is show the work.  That happens during the refactoring phase, where I explicitly demonstrate how we arrive at the answer from the parameters of the question.

For a problem like the glider, the design might evolve along these lines: why is this cell alive?  Because its live-neighbor-count in the previous generation was three.  Where did that three come from?  Well, it's a sum, we count 1 for each neighbor that is alive, and zero for each that is dead, for each of the eight neighbors.  Where do the neighbors come from?  We identify them by making eight separate calculations of using the coordinates of the target cell.  And so on.

Sometimes, I imagine this as adding a comment to the hard coded answer, explaining why that answer is correct, and then introducing the same calculation in the code so that the comment is no longer necessary.

Paraphrasing Ward Cunningham, our goal is to produce code aligned with what we understand to be the proper way to think about the problem.  Because we understand the rules of the game, we can align to them during the refactor phase without waiting for more examples to test.

Top down doesn't mean that you must jump to lion taming in one go.  Top down refactorings tend to run deep, so it often makes sense to start with a examples that are narrow.  It's not unreasonable to prefer more tests of lighter weight to a single "and the kitchen sink too" example.


Thursday, August 5, 2021

TDD: Duplication

We had a long discussion on slack today about duplication, and refactoring before introducing the second test.  Didn't come away with the sense that ideas were being communicated clearly, but I suppose that's one of the hazards of talking about it, instead of showing/pairing on it.

Or the idea was just too alien -- always a possibility.

In the process, I found myself digging out the Fibonacci problem again, because I remembered that Kent Beck's demonstration of the Fibonacci problem "back in the day" had supported the point I wanted to make.  After looking in all of the "obvious" places, I thought to check the book.  Sure enough, it appears in Appendix II of Test Driven Development by Example.

(Rough timeline: Kent created a Yahoo group in February 2002; the Fibonacci exercise came up in March, and as of July the current draft had a chapter on that topic.)

Kent's refactoring in the text looks like:

This is his first step in removing duplication; replacing the easy-to-type literal that he used to pass the test with an expression that brings us one step closer to what we really mean.

Of course, there's nothing "driving" the programmer to make this change at this point; Kent is just taking advantage of the fact that he has tests to begin cleaning things up.  As it happens, he can clean things up to the point that his implementation completely solves the problem.

Today, there were (I think) two different flavors of objection to this approach.  

One of them focused on the test artifacts you end up with - if you implement the solution "too quickly", then your tests are deficient when viewed as documentation.  Better, goes the argument, to document each behavior as clearly as possible with its own test; and if those tests are part of your definition of done, then you might as well introduce the behaviors and the tests in rhythm.

It's an interesting thought - I don't agree with it today (tests are code, code is a liability would be my counter argument) - but its certainly worth consideration, and I wouldn't be surprised to discover that there are circumstances where that's the right trade off to make.

The other object came back to tests "driving" the design.  In effect, the suggestion seems to be that you aren't allowed to introduce a correct implementation until it is the simplest that passes all the tests.  I imagine an analogy to curve fitting - until you have two tests, you can't implement a line, until you have three tests, you can't implement a parabola, and so on.

That, it seems to me, leads you straight to the Owl.  Or worse, leaves us in the situation that Jim Coplien warned us of years ago - that we carry around a naive model that gives us the illusion of progress.

Sunday, April 25, 2021

TDD: When the API changes

If I discover my API is bad after writing 60 tests, I have to change a lot! -- shared by James Grenning

My experience with the TDD literature is that it tends to concentrate of situations that are fixed.  Here's a problem, implement a solution in bite sized steps, done.

And to be fair, in a lot of cases -- where the implications of "bite sized steps" are alien, that's enough.  There's a lot going on, and eliminating distractions via simplification is, I believe, a good focusing technique.  Red-Green-Refactor is the "new" idea, and we want to keep that centered until the student can exercise the loop without concentration.

But -- if we intend that TDD be executed successfully outside of lessons -- we're going to need to re-introduce complicating factors so that students can experience what they are going to face in the wild.

Wednesday, April 21, 2021

TDD: Show Your Work Designs

I was reading Brian Marick's write up of his experience with Hillel Wayne's budget modeling experiment.  

Toward the end of the exercise, Marick writes:

I decided to write an affordability function that would call the same functions as can_afford? but return rich results instead of funneling all the possibilities into true or false....  Then I wrote can_afford? in terms of affordability.

I've seen this pattern a number of times recently.  It came up during a transport tycoon exercise,  where I started exploring the idea of generating a complete report of a simulation, and then extracting from that report the answer to the simple question.

Before that, it appeared when I started using kitchen sink logging in my AWS Lambda functions; the event handler produces a record of all of the information it has collected along the way, one field of which is "the answer".  The log entry for the request get a detailed document of all of the (not secret) information, and we can later carve that information into smaller pieces.

What the pattern reminds me of is STEM exams in high school; writing out the derivation of the answer long form, with the final calculation circled at the bottom.  The motivation is, I think, much the same; the view of the intermediate calculations is an important diagnostic tool when the final answer indicates a fault.

GeePaw Hill has recently been discussing the Made application and the Making application.  The Made application provides narrow, targeted affordances, designed to delight the end user who pays the bills.  But the purpose of the Making application is delightful (cost effective) making, where the human in the loop has different concerns.

Injecting the Making into the Made gives us, perhaps, the best of both worlds.

I also see similarity between this idea and Ward Cunningham's early description of technical debt; the underlying report is going to be more closely aligned with how we think about the domain than the simple distillation of the answer, and with the long form design in place, we have code that is aligned with the business, and should be easy to change when the business expects the code to be easy to change.

 

Wednesday, March 10, 2021

TDD: Unreachable states

 This week, I tried a master mind coding exercise, two different ways.

Mastermind was a code breaking game from my childhood; each incorrect guess returns a hint, describing how close your guess was to the goal.

At the Boston Software Crafters meetup, our breakout room first attacked the problem of identifying all 5000+ codes (ten letters, but no repeats).  Once we got that sorted, we then started working on implementing the filters we would need to eliminate candidates.  And progress, though steady from that point, was slow - we had to think a lot about what the next guess might be.

Working the problem on my own the next day, I made two changes to my approach - I deliberately introduced an (untested) adapter between the game client and my more easily tested design, and then with that more easily tested design I started working with unreachable candidate lists.

By unreachable, what I mean is that there is no sequence of guesses that would eliminate all of the other possibilities and leave just the two samples that I had selected.

Although the samples were not reachable, they were easy to reason about.  I could concentrate my attention on how the new logic should interact with these two data points, ignoring all of the other considerations as "out of scope".

In the end, my test suite included five assertions, never more than two lines of code per assertion.  And yet, when I hooked it up to the "real" data, the system worked, right out of the gate.

@ScottWlaschin argues that it can be useful to choose designs that make illegal states unrepresentable; I don't disagree - but I think that some care is required in choosing an appropriate definition of "illegal". Some of the states that you won't encounter in a healthy system are still useful when trying to explore the properties of that system.

Tuesday, March 2, 2021

Dependency Inversion Review

Earlier this week, I decided to dig out a copy of Robert Martin's 1996 article on the Dependency Inversion Principle.

I don't find his example particularly satisfactory, in particular the way that he works the example confuses, I believe, a number of different concerns.  So I thought to try the exercise of a "purer" approach, as I would do it today.

To begin, let's consider the original starting implementation of Copy()

Now,my first priority is that I not break any existing clients. So my intention is to refactor this code without changing the behavior or the signature.

That means I'm going to work my way up to an "extract function" refactoring, where the new function has the re-usable design that we are looking for.  

To begin, we need to think about replacing our dependencies on the I/O functions with abstractions.  Martin dives quickly into "objects" to address this in his examples, but that seems an imprecise hammer to use, given that functions already permit a perfectly satisfactory abstraction - the function pointer.

With an couple of variables to capture the functions we are invoking in our default implementation, it's not trivial to extract our improved method.

And done.

There's no particular magic to the fact that I use function pointers here.  In the kingdom of nouns,  these would be abstraction class instances or interfaces.  In a language like python, it would be a "callable".

Note that we have changed the code by adding a third leaf dependency to the copy function.  Copy() is otherwise a lot simpler, but mostly because we've stashed the complexity under another card.  If CopyV2 is part of the published interface, then we have introduced a new capability that allows consumers to provide substitutes for ReadKayboard and WritePrinter; CopyV2 is likely easier to test than its predecessor.

On the other hand, we're introducing the liability of more code now, in the hopes of accruing some benefit later.  And this isn't a particularly difficult refactoring to introduce "later".

Is the ease with which this refactoring can be introduced representative of the code that we encounter in the wild?  I believe so - but spaghetti code is certainly a thing, and this example doesn't obviously demonstrate that handling entanglement is trivial.