Revealing Design Flaws
Learn how to uncover design flaws early through test-driven development practices.
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 really 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 go to waste 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 have yet to 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 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’ll 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 up front how we’re going to set up our component, perhaps deciding on a clear constructor signature for an object.
We decide how we’re going to make the calling code look and what the call site will be.
We decide how we’ll 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’re literally writing the first example of a working, executable call site for our code before we even think about how it will work. We’re totally focused on how this new component is going to fit into the bigger picture.
The test itself provides immediate feedback on how well our decisions have worked out. It gives three clear indicators 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 making it up as we go along. We write code assuming it’ll work, then leave it all to a manual test stage later. If we’re lucky, we’ll discover functional errors at this stage, before our customers do.
Get hands-on with 1400+ tech skills courses.