When Doing the “Right” Thing is Wrong
I was asked some questions this week which made me really think about the topic of doing the “right” thing as far as writing code is concerned. Should we always follow what we consider to be best practices when writing code? What about when getting the code done quickly is paramount? Or when you know you are likely to throw away the code you are writing?
Think about it this way. If I told you that I needed you to write a program that would google words from a dictionary, and for each page returned in the results, it would navigate to that page and identify every location in which that word occurred. (For example: It googles ‘heart’. Clicks the first link. Returns that ‘heart’ appears as word number 5, 14, and 26 for that page.) For each answer you gave through this program, I would pay you a certain amount of money. The amount of money I would pay you would be:
- .15 cents for each match you produce in the first hour after I say “go”.
- .03 cents per match in the 2nd through 4th hours.
- .01 cent per match in any hours 5 through 10, and nothing for matches beyond that time.
- Wrong answers only cost you a .01 cent penalty.
How would you write that code? What things would be important to you? What principles would you hold on to, and which ones would you toss out the window? It is hard to imagine in that scenario caring about code coverage percentage on your unit tests. Do you even write unit tests? You’re probably only going to do the best practices that will help you write the code as fast as possible and as correctly as possible.
Doing the right thing has suddenly become wrong. Considering myself a software craftsman, this disturbs me… Deeply.
Best practices have differing ROIs in different situations
There are two main groups I would group best practices into:
- Things we do to help us solve the problem.
- Things we do to help us maintain the code.
Some of these things overlap and some of these things can be situational based on:
- What kind of problem we are trying to solve
- What the reward is for finishing fast
- What the penalty is for being wrong
In general, the practice or application of best practices is dependant on the life of the code. For short-lived code, we are more focused on things which will help us solve the problem initially and quickly. For long-lived code, we are more concerned with practices which will help us to maintain the code, and will reduce the cost of changes to the system over time.
Now, you may disagree with parts of my chart here, but I think we can all agree this is at least pretty close to correct.
- There aren’t many things we do that only help us solve the problem.
- Some of the things we do can help us solve the problem and benefit us by helping the maintainability of the code we write.
- Most of the best practices we follow are for the purpose of maintaining the code. They don’t actually help us solve the problem at all.
Consider in particular “Test Doubles.” Most developers understand this concept as mock objects. It stretches a little further and includes things like stubs and dummy objects, but for this discussion we’ll lump them together. A mock object specifically is an object created for the purpose of verifying the calls the code under test should be making during its execution. Just because you can use mock objects, does not mean you always should.
They can be useful in two distinct ways. One way is to provide isolated testing for a unit of code, by making sure that its behavior is correct without depending on other parts of the system. Another use is to enable you to build your code without having to have other parts of the system, which may not be ready yet.
One of those usages is good for maintainability of the system. It decouples your unit tests to make sure they are only testing a unit and break only when that unit fails in a specific way. The other usage is for helping you to build your code more quickly without having to rely on some external interface which may be slow or not exist yet. Depending on the life of your code, and how complex your interactions are with other parts of the system, you may have a high ROI using mock objects in one of these two ways, both ways, or not at all.
Other items in the chart have similar trade-offs and conditional uses.
Time to market changes things
In the example at the beginning of this post, getting the code up and running quickly was the primary factor in determining the profitability of the code. Errors had some penalty, but not anywhere near the penalty of getting the code up and running late.
Sometimes in the real world, in real software development, you will find this to be the case. When it is the case, you have to be prepared to make some sacrifices. You may have to decide that you are going to write version 1 quickly, and version 2 will be a complete rewrite which will be maintainable. (If there is a version 2, which may depend on how fast you get version 1 out the door.)
Often in an environment which cannot incur the additional time and/or process overhead of building high code coverage unit tests and automated functional or integration tests, you may have to choose writing the test cases that are going to give you the best bang for your buck.
You may have to determine stable parts of the system which have functionality that are less likely to change, and to build good code covering unit tests around those parts since they will have a long life. You may also have to determine a subset of basic functionality that you can automate to give you a large bang for your buck. This could be considered a smoke test.
This isn’t a license to kill. You can’t cheat the system.
This doesn’t mean that you can disregard best practices. There are far fewer situations where focusing on speed vs. maintainability is the correct decision. If you are working on releasing a product which has an estimated R&D time of 1 year, reducing quality at the beginning of the project will cost your more time in the end. If you are working on a product that needs to be released in 2 weeks, or the profit or the profit margin quickly drops, focusing on solving the problem instead of maintainability is probably the right answer. Just know this… If that product ends up living for another 2 years, you will pay the price if you don’t start following the best practices for maintainability.
Don’t link to this blog post and say “See John says we don’t have to write unit tests.” If you’re looking for an excuse for taking short cuts and not following best practices, don’t look here. I am adamantly against that idea.
But, if you think that following every best practice and having 100% code and branch coverage on all your code is always the right thing to do, hopefully I’ve shown you why it isn’t always. Your default behavior should be to write code for the purpose of reading and maintaining, with the knowledge that 80% of the code we write is done in a way to optimize reading and changing of it. Sometimes though, you have to be able to write code in a way optimized for speed of writing.