Revealing Design Flaws

Good design

Bad design is truly bad. It is the root cause of software being hard to change and hard to work with. We can never quite be sure whether our changes are going to work because we can never quite be sure what a bad design is really doing. Changing that kind of code is scary and often gets put off. Whole sections of code can be left to rot with only a /* Here be dragons! */ comment to show for it. The first major benefit of TDD is that it forces us to think about the design of a component. We do that before we think about how we implement it. By doing things in this order, we are far less likely to drift into a bad design by mistake.

Outside-in vs. inside-out

The way we consider the design first is to think about the public interfaces of a component.

We think about how that component will be used and how it will be called.

We don’t yet consider how we will make any implementations actually work. This is outside-in thinking. We consider the usage of the code from outside callers before we consider any inside implementation. This is quite a different approach to take for many of us.

Typically, when we need code to do something, we start by writing the implementation.

After that, we will ripple out whatever is needed in method signatures, without a thought about the call site. This is inside-out thinking. It works, of course, but it often leads to complex calling code. It locks us into implementation details that just aren’t important. Outside-in thinking means we get to dream up the perfect component for its users. Then, we will begin the implementation to work with our desired code at the call site. Ultimately, this is far more important than the implementation. This is, of course, abstraction being used in practice. We can ask questions like the following:

  • Is it easy to set up?

  • Is it easy to ask it to do something?

  • Is the outcome easy to work with?

  • Is it difficult to use it the wrong way?

  • Have we made any incorrect assumptions about it?

We can see that by asking the right sort of questions, we’re going to get the right sort of results.

Writing tests

By writing tests first, we cover all these questions.

  • We decide upfront how we are going to set up our component, perhaps deciding on a clear constructor signature for an object.

  • We decide how we are going to make the calling code look and what the call site will be.

  • We decide how we will consume any results returned or what the effect will be on collaborating components.

This is the heart of software design. TDD does not do this for us, nor does it force us to do a good job. We could still come up with terrible answers for all those questions and simply write a test to lock those poor answers into place. We’ve seen that happen on numerous occasions in real code as well. TDD provides that early opportunity to reflect on our decisions.

We are literally writing the first example of a working, executable call site for our code before we even think about how it will work. We are totally focused on how this new component is going to fit into the bigger picture.

test itself provides immediate feedback on how well our decisions have worked out. It gives three tell-tale signals that we could and should improve. We’ll save the details for a later chapter but the test code itself clearly shows when our component is either hard to set up, hard to call, or its outputs are hard to work with.

Analyzing the benefits of writing tests before production code

There are three times we can choose to write tests:

  • before the code.

  • after the code.

  • or never.

Obviously, never writing any tests sends us back to the dark ages of development. We’re winging it. We write code assuming it will work, then leave it all to a manual test stage later. If we’re lucky, we will discover functional errors at this stage, before our customers do.

Get hands-on with 1200+ tech skills courses.