Chapter 4. Separation of Concerns
“Separation of concerns” ... is what I mean by “focusing one’s attention upon some aspect”: it does not mean ignoring the other aspects, it is just doing justice to the fact that from this aspect’s point of view, the other is irrelevant. It is being one- and multiple-track minded simultaneously.
Edsger Dijkstra, “On the Role of Scientific Thought”
Our source code has grown. Depending on the language, it’s 50–75 lines in one source file. That’s more than a screenful on many display monitors, and certainly more than a printed page in this book.
Before we get to the next feature, we’ll spend some time refactoring our code. That’s the subject of this and the next three chapters.
Test and Production Code
Thus far, we’ve written two different types of code.
-
Code that solves our Money problem. This includes
Money
andPortfolio
and all the behavior therein. We call this production code. -
Code that verifies that the problem is correctly solved. This includes all the tests and the code needed to support these tests. We call this test code.
There are similarities between the two types of code: they are in the same language, we write them in quick succession (through the by now familiar red-green-refactor cycle), and we commit both to our code repository. However, there are a few key differences between the two types of code.
Unidirectional Dependency
Test code has to depend on production code—at least on those parts of production code that it tests. However, there should be no dependencies in the other direction.
Currently, all our code for each language is in one file, as shown in Figure 4-1. So it’s not easy to ensure that there are no accidental dependencies from production code to test code. There is an implicit dependency from the test code to the production code. This has a couple of implications:
-
When writing code, we have to be careful to not accidentally use any test code in our production code.
-
When reading code, we have to recognize the patterns of usage and also notice the missing patterns, i.e., the fact that production code cannot call any test code.
Important
Test code depends on production code; however, there should be no dependency in the other direction.
If production code is dependent on test code, what are the possible bad results? In particularly bad cases, it can mislead us to a path where the code path that is tested is “pristine” whereas the paths that are not tested are fraught with bugs. Figure 4-2 shows a portion of the pseudocode for the engine control unit in a car. The code works differently if the engine is being tested for emissions compliance than when the engine is being used “in the real world.”
If you’re skeptical that such a blatant case of “be on your best behavior for tests” could ever happen in reality, you’re encouraged to read about the Volkswagen emissions scandal, from which the pseudocode in Figure 4-2 is drawn.1
Having a unidirectional dependency—where production code does not depend on test code in any way and is therefore not susceptible to behaving differently when under test—is vital to ensuring defects of this nature (whether accidental or malicious) do not creep in.
Dependency Injection
Dependency injection is a practice to separate the creation of an object from its usage. It increases the cohesion of code and reduces its coupling.2 Dependency injection requires different code units (classes and methods) to be independent from each other. Separating test and production code is an important prerequisite to facilitating dependency injection.
We’ll have more to say about dependency injection in Chapter 11, where we’ll use it to improve the design of our code.
Packaging and Deployment
When application code is packaged for deployment, the test code is almost always packaged separately from production code. This allows deploying production and test code independently. Often, only production code is deployed in certain “higher” environments such as the production environment. This is shown in Figure 4-3.
We’ll describe deployment in more detail in Chapter 13, when we build a continuous integration pipeline for our code.
Modularization
The first thing we’ll do is to separate the test code from the production code. This will require us to solve the problem of including, importing, or requiring the production code in the test code. It is vital that this should always be a one-way dependency, as shown in Figure 4-4.
In practice, this means that the code should be modularized along these lines:
-
The test and production code should be in separate source files. This allows us to read, edit, and focus on test or production code independently.
-
The code should use namespaces to clearly identify which entities belong together. A namespace may be called a “module” or a “package,” depending on the language.
-
Insofar as it’s possible, there should be an explicit code directive—
import
,require
, or similar, depending on the language—to indicate that one module depends on another. This ensures that we can specify the dependency shown in Figure 4-1 explicitly.
We’ll also look for opportunities to make code more self-describing. This would include renaming and reordering entities, methods, and variables to better reflect their intent.
Removing Redundancy
The second thing we’ll do is to remove redundancy from our tests.
We have had two multiplication tests for a while now: one for euros and one for dollars. They test the same functionality. In contrast, we have only one test for division. Should we keep both the multiplication tests?
There is seldom an ironclad “yes” or “no” answer to this. We could argue that the two tests protect us from inadvertently hardcoding the currency in the code that does the multiplication—although that argument would be weakened by the fact that we have one test for division and a similar hardcoded currency error could crop up there.
To make our decision making more objective, here is a checklist:
-
Would we have the same code coverage if we delete a test? Line coverage is a measure of the number of lines of code that are executed when running a test. In our case, there would be no loss of coverage if we deleted either one of the multiplication tests.
-
Does one of the tests verify a significant edge case? If, for example, we were multiplying a really large number in one of our tests and our goal was to ensure that there was no overflow/underflow on different CPUs and operating systems, we could make the case for keeping both tests. However, that is also not the case for our two multiplication tests.
-
Do the different tests provide unique value as living documentation? For example, if we were using currency symbols from beyond the alphanumeric character set ($, €, ₩), we could say that displaying these disparate currency symbols provides additional value as documentation. However, we are currently using letters drawn from the same 26 letters of the English alphabet (USD, EUR, KRW) for our currencies, so the variation between currencies provides minimal documentation value.
Tip
Line (or statement) coverage, branch coverage, and loop coverage are three different metrics that measure how much of a given body of code has been tested.
Where We Are
In this chapter, we reviewed the significance of separation of concerns and removing redundancy. These are the two goals that will garner our attention in the following three chapters.
Let’s update our feature list to reflect that:
5 USD × 2 = 10 USD |
10 EUR × 2 = 20 EUR |
4002 KRW / 4 = 1000.5 KRW |
5 USD + 10 USD = 15 USD |
Separate test code from production code |
Remove redundant tests |
5 USD + 10 EUR = 17 USD |
1 USD + 1100 KRW = 2200 KRW |
Our goals are clear. The steps to accomplish these—especially the first goal of separation of concerns—will vary significantly from language to language. Therefore, the implementation has been separated into the next three chapters:
Read these chapters in the order that makes most sense to you. Refer to “How to Read This Book” for guidance.
1 On the Volkswagen “dieselgate” scandal, Felix Domke has done a lot of work. He’s coauthored a whitepaper. He also delivered a keynote at the Chaos Computer Club conference.
2 Cohesion and coupling are described in more detail in Chapter 14.
Get Learning Test-Driven Development now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.