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.