A rule of thumb and a silver bullet

Making good design decisions (in the software engineering sense) is tricky.  In theory, you can boil everything down to first principles and make a decision based in pure logic.  But in practice, this tends not to happen very much outside of textbooks.  Real projects are just too complex and messy.  And many design principles exist to make your life easier in the future: making the code more understandable, easier to extend, easier to fix, easier to modify.  Even a logical argument has to begin with some hypothetical future situations, so it’s not purely logical.

Although good design can be justified by logic after the fact, getting good at it in practice requires you to develop the right intuition and gut instincts: what you might call good taste.  Most people acquire it by making bad design decisions and being forced to live with the consequences, learning from their mistakes.  It’s an experience thing.

So how can you accelerate your learning?  How do you make better decisions when your instincts aren’t fully developed?  How do you make sure you develop good intuitions?

People meditating

Here’s another related problem.  How many times have you come across an older developer who makes an awful design decision, but who stubbornly defends it and plays the infuriating “I have more experience” card to dismiss you?  It’s unfortunate, but people can still have poor intuition despite years of experience.  Maybe they don’t learn from their mistakes very well, or maybe it’s in the nature of their experience: they’ve learned so much about a particular set of development circumstances (say, 8-bit games made by 2 people in a few months using assembler) that their gut instincts just aren’t right for the current situation (say, cutting edge games written by 100 people over several years in C++).

These debates are near-impossible because you each favour your own gut instincts, and it’s hard to explain or justify them.  You may not even recognise your own gut instincts as such.  You just think the other person’s wrong and you can’t explain why.  You can try appealing to logic, but you typically have to invoke hypothetical future situations or past mistakes to do so, and the two of you are likely to come up with different scenarios to support your respective arguments.

An answer

I recently discovered a rather wonderful one-word answer to both these problems: “testability”.  I’ve known for years that unit testing is a Good Thing, but I only just realised that “testability” appears to be a neat little rule of thumb to approximate local design quality!  Specifically, making code testable tends to promote the following design principles:

  • Loose coupling – because unit tests only work when they can isolate a small piece of code to test.
  • Minimising dependence on state (especially super-evil global state!) – because setting up state for test code is tedious and error-prone.
  • Simplicity and ease of use – because if your API is awkward to use, writing test code for it quickly brings home that point in painful fashion.
  • Separating interface from implementation – because this is key to “mocking”.
  • Code reuse – because the test harness provides a rather different “user” for your code.  The more “users” you successfully add, the more reusable your code becomes.
  • Extensibility – because writing tests is easier if your code provides hooks to inspect and tweak what it’s doing, and these hooks tend to lend themselves to extensibility too.

No doubt there are more: check out this excellent article by Miško Hevery of Google.

So if you simply want to improve and develop your intuitions (and your design!), you can ask yourself “what are the implications for testability?”  The really nice thing is that it doesn’t take long to write a bit of test code and see for yourself: you can check your answer.

Equally, suppose you commit as a team to unit testing of your codebase. Now, when you’re faced with a developer who’s designed an awful API and is stubbornly defending it, you can beat them over the head with the testability stick (figuratively speaking).  Testability cuts through a lot of the judgement and intuition and makes the discussion concrete: a couple of small test harness code fragments quickly show which approach is better.  “Testability” feels more binary where “good design” feels fuzzy.  This doesn’t help you with larger architectural issues, but getting your code in good shape locally is well worth it.

Of course, unit testing has many other benefits – but this was a pleasantly surprising side effect for me🙂

This entry was posted in Software development. Bookmark the permalink.

5 Responses to A rule of thumb and a silver bullet

  1. Rob Chant says:

    Certainly an interesting perspective, especially as I’m someone who really resists unit testing! (I get away with it as my only project is a hobby really).

  2. lukehalliwell says:

    I certainly don’t mean to suggest being religious about doing unit testing. If you’re doing a hobby project, sure – there’s no point. Even on a bigger project, I’m only really talking about doing it on “library” code – stuff that’s reused a lot, that’s critical to everyone to be reliable, and that deals more with algorithms and data than with user input and display. It’s well known that unit testing UI code doesn’t work as well, and it’s the same in games – gameplay, graphics, user input code – and anything else close to the application level – typically aren’t going to be that suitable for automatic testing.

  3. Mark Simpson says:

    I’m a test engineer (I’m totally biased). I agree whole heartedly.

    Being exposed to testability concerns on a day to day basis has proved to be hugely important for my own personal development as a programmer.

    My reasoning is simple: I’ve looked at a lot of code over the last year. Some of it was convoluted, hard-wired, dependent on static state etc. The end result is that it was extremely painful to test to any degree of usefulness.

    I naturally looked for remedies to make the software more testable, and in doing so:

    * I realised why static classes, global state and singletons are nearly always counter-productive

    * I discovered dependency injection to create ‘seams’ in my classes, and to enforce explicit ordering (no more “yeah, initialise this one, then this static class, then set this property, then initialise this static system, then.. etc.)

    * I now strongly favour composition over inheritance

    * I started favouring small, discrete classes that have fewer responsibilities

    I think you can see where this is going. It just so happened that all of the things that make code more testable also tend to make better software. For that reason alone, I am extremely glad that I started in this job!

    I haven’t yet discovered anything to do with testability that has made my design worse.

  4. Pingback: Mark’s Testblog » Blog Archive » Testable code happens to be better designed code - …for these are testing times, indeed.

  5. Pingback: Mark’s Testblog » Blog Archive » No longer a software engineer in test - …for these are testing times, indeed.

Comments are closed.