Sufficient Design

Posted April 26, 2010 by Joshua Kerievsky

 

Weights When I consider the quality of software design on the products we write and sell, I do so from the dual perspective of business owner and programmer.

As a business owner, I pay attention to our user's success and our revenue.

As a programmer, I pay attention to our software process and code quality.

Balancing these perspectives is a practice I call Sufficient Design.

Are All Features Created Equal?

I don't think so.

Not all of a product's features are of equal importance to users of that product.

Some features form the heart and soul of a product, others play a supporting role and some code is just pure plumbing.

Yet some programmers argue that the software design quality of every last piece of code ought to be as high as possible. "Produce clean code or you are not a software craftsman!"

Such advice is well-intentioned. Many of us have seen the near paralysis that comes with high technical debt.

Yet ultimately the craftsmanship advice fails to consider simple economics: If you take the time to craft code of little or moderate value to users, you're wasting time and money.

The fact is, some code simply doesn't need an excellent design, whether that code is a start-up experiment or a corporate application feature.

Lets consider a real world example.

A Slightly Flawed Design

In 2004, during one of our visits to my wife's home town of Des Moines, Iowa, I found myself in a bookstore, programming. I was test-driving a piece of code that would handle requests and responses from web users. As I worked, each new failing test helped me to evolve the design.

While I was not explicitly trying to produce a command pattern implementation, my code naturally ended up taking on the shape of that pattern. The main method on all of the command classes (e.g. PageAction) looked like this:

public String processWith(ActionParameters parameters)...

My tests passed by evaluating the String that each command (or collection of collaborating commands) returned. I did my best to refactor the code so that it was as simple as possible.

This code allowed our small team to produce an early version of an eLearning product.

Some Command ClassesOne day, after we had many command classes in the emerging product, we needed to write a command that would allow a user to download a coding exercise. Suddenly, we found that the String returned by the processWith(...) method didn't fit our needs. This time we needed to output binary data.

No worries. We made a CodeDownloadAction to do the work.

But since it implemented the same processWith(...) method as all of the other commands, it had to return null:

public class CodeDownloadAction extends Action...
public String processWith(ActionParameters parameters) {
    ...
    output(file, addArchiveSuffixTo(downloadFileName));
    return null;
}

Returning null felt like really bad design. We looked into fixing it, which would have meant making processWith(...) return void and passing the string/binary data around via arguments to each command instance. Not incredibly complicated, but given the number of commands we had and our desire to work in small, safe refactoring steps, we didn't find an easy way to make this change.

 
So what did you do? Leave that mess in the code?
 

Indeed we did. We said we'd fix it some other time.

About a year later, with many more command classes now in the code, I took some time with a pair programmer to go after the return null smell. My pair and I spent a half day attempting to clean up the smell in a piecemeal fashion (again, by taking small, safe refactoring steps), yet we did not succeed. Rather than waste more time, we reverted the code and moved on to other important things.

Now, several years later, the same smell in this piece of plumbing code is alive and well. The thing is, we rarely create new binary commands, so this design flaw isn't getting in our way.

Instead, we've kept our focus on producing new features and thoroughly improving the design of our most important code.

What The Heck Are You Saying, Josh?

I'm saying that we need high design quality for stuff that is critical to our products and less design quality for stuff that isn't critical.

And I'll also say this: we never compromise on doing TDD (Test-Driven Development). We always do it, only we adjust our "Design Dial" to be more aggressive or less aggressive about refactoring, based upon the ebb and flow of our product development.

 
So you're now advocating having tons of Technical Debt in our code bases? Don't you know the problems that this creates?
 

I'm advocating no such thing.

After years of building eLearning software, we learned that people will pay real money for certain features of our product. That means we spend a good deal of time crafting those parts of the code, while stuff like our little return null problem continue to get overlooked because our users couldn't care less about it and because it isn't getting in our way.

The art of software design is now defined by how well an individual or team can adjust the Design Dial on a feature basis to produce software that people love and which makes money.

There is no one-size-fits-all approach to software quality.

If you're a feature junkie, pushing one feature after another into your product with little thought for software design, your technical debt will paralyze you starting around release 4.0.

If you're a quality junkie, you will over-engineer every last thing you do.

Sufficient Design is where Lean meets Craft.

Make design quality commensurate with feature importance.