How many design considerations are there in an almost trivial method? Let’s look at two of them. Consider this code:
def publish! self.update_attributes(created_at: Time.now) end
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!(time=Time.now) self.update_attributes(created_at: time) end
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_attimestamp is set to the current time. Returns the
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_atis set to the current time. Optionally, callers may pass in a
Timeobject, or any object that returns a
Timeobject when sent the
created_atcolumn is set according to that
Timevalue. Returns the value of the
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.