Hello again! Sorry it’s been a while since I last wrote. This may, or may not, be the start of more regular posts. Let’s see…
Yesterday I finally got a tricky bit of code working. It’s a spike, helping me to figure out how a complicated distributed algorithm will all hang together.
And when I say “working”, I mean that it worked on my laptop :-). I tried deploying it to my production server and it didn’t work there. But that’s because part of it makes a post to http://localhost:44002
, which doesn’t exist in production. Not a surprise.
So my job for the next few cycles will be to expand on this code, removing the assumption about where it runs. Notice that I said “expand” rather than “fix”. The code isn’t broken. It definitely works, but only under certain circumstances. It is tested, but it includes an assumption. So I consider the job of the next few cycles to fall into a bit of a grey area between feature writing and refactoring, and here’s what I plan to do…
My domain model — and my architectural assumptions — tell me that I should be getting that URL from user profile data held elsewhere in my network. And in fact there should be a list of such URLs, instead of just one as in the current spike. That is, I’ll need to replace that hardcoded string “http://localhost:44002“
with an API call that should return an array of profiles, each of which includes an attribute whose value is the appropriate URL. That API call could also fail. So I’m planning to replace a string value with a call to an endpoint that will return some sort of Promise of either a list of profiles or one of several kinds of error. Oh, and that endpoint doesn’t yet exist.
I’ll outline my plan below, but before continuing, maybe take a moment to think about how you would approach this while keeping the code working…
I could start by writing the new endpoint. After all, I “know” what its signature should be. But then it will sit around, not being called, while I reshape the current code to cope with errors, arrays, profiles, and promises. The client-side code knows about none of those things currently, and they each represent a little risk to the application’s current fragile working state.
So instead I plan to do the opposite. I’m going to apply the following refactorings, possibly in this order, possibly not:
Wrap the hardcoded URL in a function call. This gives it a name, which will help me to remember why it exists. And it creates a “seam”, hiding some of the details of what happens next.
Refactor the client to have an array containing the single hardcoded URL. This will force the downstream code to deal with multiple URLs, but the code will be easy to keep working.
Wrap the hardcoded URL in a user profile object, so that my client-side code knows how to unpack such a thing, and so that I know what it needs to look like.
Wrap the hardcoded data in a Promise and test for error conditions. I don’t really know yet what that looks like, and I expect I’ll have to revisit this once the API call is being made for real.
Each of these refactorings should result in code that still works, and can be committed to main without changing existing behaviour. Now, finally, I can write a simple API endpoint that just returns exactly the same hardcoded data (an array containing a single made-up profile whose URL is still the hardcoded localhost value), and call it from the client. At this point I’m making the network call, and the code dealing with Promises and errors is being exercised. If all goes to plan, this is still releasable with no change to current functionality.
Something very important has now happened, though: I’ve pushed the hardcoded value back towards where it will eventually originate — without changing any existing, tested functionality. The hardcoding is now inside the server API endpoint, and no longer in the client. The client has been generalised, and should in future just do what the server tells it to do.
This is the essence of outside-in development, and the foundation of the Mikado method too. Each cycle pushes an assumption towards an edge of the application until it disappears. Each cycle can be committed to main and deployed. And all the while, the entire application keeps working.
Does your team work this way? If not, and you would like to learn how to apply outside-in development in your work, message me and let’s chat about whether some of my training or coaching could work for you.
There is a part of pushing assumptions away that I found most challenging, which is how both the consumer and the producer interact to define a contract. For example, the consumer has opinions on the fact that a URL must be provided in the response, but the fact that the producer is located elsewhere (and reachable through HTTP) implies that a Promise needs to be used while adding all sort of possible error conditions.
I did something like this for a protocol I am using. The protocol has three ... well I will call them modes. The device I'm talking to supports three of them : MODE2, MODE3 and MODE5. Each of these modes uses five registers, A, B, C, D and E. I ended up with ( but did not start with ) a map for each the models which looks up the register addresses for A,B,C,D and E. The construction of this map ended up by assuming that the offsets of the addresses for A,B,C,D and E are the same for each mode. In other words only the base address of the registers changes from mode to mode, not the offsets from the base address. This assumption was hard coded into my code as I generalised from MODE2 to MODE3, and the assumption was valid as I made that transition.
The problem came with MODE5. The offsets were not the same as for MODE2 and MODE3. In fact, for MODE5, the registers are in a different order - it's not A,B,C,D,E, it's actually A,D,C,B,E. At this point, I have a problem. I know that if I write a test for MODE5, it will fail, because I have "incorrectly" generalised from the similarity between MODE2 and MODE3. But ... my company is designing this device. Frankly I just think this is a bug. I believe that my generalisation is correct and that this bug should be fixed by the people who are designing the device.
Should I :
(i) degeneralise ( ie complicate ) my code and write a working test for MODE5 ?
(ii) write a failing test for MODE5 ?
(iii) not write any test for MODE5 ?
[ Apologies for not using TDD as you can see from my question - but I think the question could be rephrased in TDD terms ]