Design for test vs. design for API

How many design considerations are there in an almost trivial method? Let's look at two of them. Consider this code:

def publish!

If you've been studying OO design and the SOLID principles, using TDD as a practice to guide you towards those ideas, there's a missing piece here. The reference to Time is a dependency that should be injected. In Ruby, it's really easy for us to fix that:

def publish!(
  self.update_attributes(created_at: time)

I suspect a lot of TDDers would instinctively write the above first, skipping the first version by force of habit. But, let's stop and think about what the drives us to want the second version.

The strength of the second version is that it is designed for test. If we need to test how this model behaves when it is published at night, or on a leap day, or the day before Arbor Day, injecting the time object makes that easier.

There are some other test-focused design direction this method could go. We could create our own object whose role is to hand out timestamps, which would allow us to reasonably stub out the time reference, instead of injecting it. I'll bet there are other approaches lurking out there as well.

I want to look at another set of design considerations. I could design this code for testability, which often leads me to code that follows the SOLID principles which often leads me to decoupled code that is easier to change later. To many people, that's a good thing.

However, there's another lens I can look through: API design. How does this method hold up as a piece of behavior that developers will leverage?

Strictly speaking, the TDD'd version is a more complicated API. Even adding one optional parameter to a method carries "mass". Consider documenting the parameter-less version:

Publishes the current post. The created_at timestamp is set to the current time. Returns the created_at timestamp.

For numbers sake, it's 40 words. More importantly, it reads linearly. Now let's look at the dependency-injected version:

Publishes the current post. By default, created_at is set to the current time. Optionally, callers may pass in a Time object, or any object that returns a Time object when sent the now message. The created_at column is set according to that Time value. Returns the value of the created_at timestamp.

This one is 54 words. That's not too many more, numerically, but notice that the explanation is no longer linear. There's a default, easy case where I don't care about the timestamp. Then there's a clever case where I do care about the timestamp. In real API documentation, I'd need to specify when and why I'd want to use that clever case and what it looks like.

There's some further potential trouble lurking in this API. What if a caller passes in the wrong kind of Time object? What if sending the now message raises an exception? Those are important parts of the API too, both from a behavior specification perspective and when considering the user experience of using this API in code and possibly troubleshooting it when things go wrong.

My point is, that optional argument is starting to look rather weighty. Adding the code is pretty trivial. The possible interactions with the optional argument and its support cost is where it gets expensive. Like many things, it's a trade-off.

I won't claim to know which of these is better. Honestly, I think it comes down to a subjective view on what's important: test design, or API design. This is where I can't make a bold-sounding prognosis. I believe that design, even of code, is about deciding what to leave out. Everyone has to decide what to leave out for themselves.

Adam Keys @therealadam