A few weeks ago I had an opportunity to pair with Uncle Bob.
We were going to pair on a message queue convenience wrapper I had started earlier in the week. Embarrassingly, I wasn’t entirely sure if it worked. I looked at the code, the documentation of the library I was going to use, planned a potential API and started working towards it, with the expectation that at the end of mocking this thing out, a few higher level acceptance tests would do the trick.
We sat down and I walked him through the code. He politely nodded as I explained, but even as I explained it, I didn’t trust the code and felt like he saw right through my mistakes. I’m sure he did.
He asked me: “What is the simplest possible way we could send a message -- away from the code you’ve written.” And we figured that the simplest possible code to be working on was:
1 @queue = Queue.new('name.of.queue')
2 @queue.send_message('my message')
3 @queue.receive.should == 'my message'
What resulted was 69 lines of code in one file, with a test suite I trust. It’s simple, it’s clear and it doesn’t have functionality built in that’s not needed. It is nearly 45% less code than my original program, with nearly the same functionality, a simpler API and tests that are really testing.
My older program had something like this:
1 queue = Queue.new(exchange_options, queue_options)
2 queues = [queue]
Not bad from an outside perspective, but what does it do under the covers? It’s hard to tell, unlike the other example. The exchange and validation options took multiple keys that I did validations on. The multiple arguments are complex and opaque unless you are familiar with the message queue library I was using. Did I know I was going to have multiple queues here and that I’m going to instantiate them in the same place for the Queues object? What about testing a queue directly? Did I need to build up an entire working system just to test a queue? I just didn’t need this stuff, and it increased complexity dramatically.
The point I’d really like to stress is that fundamental, methodical approaches in programming are difficult to maintain. In my original program I made jumps. I could see an implementation I liked and went there. But in those jumps small but weird fragmentations in my design occured. Those jumps were almost right, but not exactly, and in doing so my code bloated, became more difficult to manage, and obscured the true use cases. I became more distant from working code, and while it felt well abstracted, ultimately it was poorly written.
My experience pairing with Uncle Bob reminded me that we always need to write for the simplest current use case, that we should be just as disciplined in knowing our use cases, and to always build on working code.
“What is the simplest program we can make for the functionality you require?”
That is a question that I have been asking myself a lot since our pairing. It forces me to trim use cases, because it’s common that I’ll write for use cases that just don’t matter. The question forces me to think about the most fundamental use case I can. And in doing so I have no problem writing small tests from working code I trust.
These methodical, fundamental approaches also yield a more impressive result: simple code. My original program was well-abstracted. The functions were small. The naming was clear and descriptive. But it wasn’t simple. It was bloated, complex.
Simple code is born out of discipline. You see ways of abstracting, of writing more -- but you don’t. Simple code is confusing at first because this code that you are reading really shouldn’t be so simple. There has to be something wrong. Simple code makes you feel like you really want to abstract something out. It’s begging to be transformed and changed into something slightly more opinionated. But is it really needed? Are there enough pressures to justify it? What use cases are you building for?
When you write only the code necessary to make your single use case satisfied, you end up with a simple solution. Striving to understand and cultivate use cases, and the easiest working solutions for them helps to ensure us that software will be as simple as possible.
Think about the code you write. Do you have a definite idea of the use cases you are writing for? Are you more concerned with well-abstracting your code, making your code clever, or making your code simple? Do you feel like they are the same thing? What about your process of writing code? Is it methodical and disciplined, or do you occasionally write more than is needed?
And if it helps you, go back to the question that I try to constantly ask myself: “What is the simplest program we can make for the functionality you require?”.
There are an infinite number of use cases you could solve for. Simple code is there, if you have the discipline to program against only what is needed, one at a time.