Thinking and TDD

The cycle of TDD, once again in brief, is to write a small test, ensure it fails, get it to pass, review and clean up the design (including that of the tests), and ensure the tests all still pass. You repeat the cycle throughout the day, keeping it short to maximize the feedback it gives you. Though repetitive, it’s not mindless—at each point you have many things to think about. Thinking and TDD, contains a list of questions to answer at each small step.

To keep things moving, I’ll assume you’re following the steps of the cycle and remind you of them only occasionally. You may want to tack to your monitor a handy reminder card of the cycle.

For our next test, we tackle rule #2 (“replace consonants with digits after the first letter”). A look at the replacement table reveals that the letter b corresponds to the digit 1.

c2/12/SoundexTest.cpp
 
TEST_F(SoundexEncoding, ReplacesConsonantsWithAppropriateDigits) {
*
ASSERT_THAT(soundex.encode(​"Ab"​), Eq(​"A100"​));
 
}

The test fails as expected.

 
Value of: soundex.encode("Ab")
 
Expected: is equal to 0x80b8a5f pointing to "A100"
 
Actual: "Ab000" (of type std::string)

As with most tests we will write, there might be infinite ways to code a solution, out of which maybe a handful are reasonable. Technically, our only job is to make the test pass, after which our job is to clean up the solution.

However, the implementation that we seek is one that generalizes our solution—but does not over-generalize it to support additional concerns—and doesn’t introduce code that duplicates concepts already coded.

You could provide the following solution, which would pass all tests:

 
std::​string​ encode(​const​ std::​string​& word) ​const​ {
 
if​ (word == ​"Ab"​) ​return​ ​"A100"​;
 
return​ zeroPad(word);
 
}

That code, though, does not move toward a more generalized solution for the concern of replacing consonants with appropriate digits. It also introduces a duplicate construct: the special case for "Ab" resolves to zero-padding the text "A1", yet we already have generalized code that handles zero-padding any word.

You might view this argument as weak, and it probably is. You could easily argue any of the infinite alternate approaches for an equally infinite amount of time. But TDD is not a hard science; instead, think of it as a craftperson’s tool for incrementally growing a codebase. It’s a tool that accommodates continual experimentation, discovery, and refinement.

We’d rather move forward at a regular pace than argue. Here’s our solution that sets the stage for a generalized approach:

c2/13/Soundex.h
 
std::​string​ encode(​const​ std::​string​& word) ​const​ {
*
auto​ encoded = word.substr(0, 1);
*
*
if​ (word.length() > 1)
*
encoded += ​"1"​;
*
return​ zeroPad(encoded);
 
}

We run the tests, but our new test does not pass.

 
Expected: is equal to 0x80b8ac4 pointing to "A100"
 
Actual: "A1000" (of type std::string)

The padding logic is insufficient. We must change it to account for the length of the encoded string.

c2/14/Soundex.h
 
std::​string​ zeroPad(​const​ std::​string​& word) ​const​ {
*
auto​ zerosNeeded = 4 - word.length();
*
return​ word + std::​string​(zerosNeeded, ​'0'​);
 
}

Our tests pass. That’s great, but our code is starting to look tedious. Sure, we know how encode works, because we built it. But someone else will have to spend just a little more time to carefully read the code in order to understand its intent. We can do better than that. We refactor to a more declarative solution.

c2/15/Soundex.h
 
class​ Soundex
 
{
 
public​:
 
std::​string​ encode(​const​ std::​string​& word) ​const​ {
*
return​ zeroPad(head(word) + encodedDigits(word));
 
}
 
 
private​:
*
std::​string​ head(​const​ std::​string​& word) ​const​ {
*
return​ word.substr(0, 1);
*
}
 
*
std::​string​ encodedDigits(​const​ std::​string​& word) ​const​ {
*
if​ (word.length() > 1) ​return​ ​"1"​;
*
return​ ​""​;
*
}
 
 
std::​string​ zeroPad(​const​ std::​string​& word) ​const​ {
 
// ...
 
}
 
};

We’re fleshing out the algorithm for Soundex encoding, bit by bit. At the same time, our refactoring helps ensure that the core of the algorithm remains crystal clear, uncluttered by implementation details.

Structuring code in this declarative manner makes code considerably easier to understand. Separating interface (what) from implementation (how) is an important aspect of design and provides a springboard for larger design choices. You want to consider similar restructurings every time you hit the refactoring step in TDD.

Some of you may be concerned about a few things in our implementation details. First, shouldn’t we use a stringstream instead of concatenating strings? Second, why not use an individual char where possible? For example, why not replace return word.substr(0, 1); with return word.front();? Third, wouldn’t it perform better to use return std::string(); instead of return "";?

These code alternatives might all perform better. But they all represent premature optimization. More important now is a good design, with consistent interfaces and expressive code. Once we finish implementing correct behavior with a solid design, we might or might not consider optimizing performance (but not without first measuring; see TDD and Performance for a discussion about handling performance concerns).

Premature performance optimizations aside, the code does need a bit of work. We eliminate the code smell of using a magic literal to represent the maximum length of a Soundex code by replacing it with an appropriately named constant.

c2/16/Soundex.h
 
static​ ​const​ size_t MaxCodeLength{4};
 
// ...
 
std::​string​ zeroPad(​const​ std::​string​& word) ​const​ {
*
auto​ zerosNeeded = MaxCodeLength - word.length();
 
return​ word + std::​string​(zerosNeeded, ​'0'​);

What about the hard-coded string "1" in encodedDigits? Our code needs to translate the letter b to the digit 1, so we can’t eliminate it using a variable. We could introduce another constant, or we could return the literal from a function whose name explains its meaning.

We could even leave the hardcoded string "1" in place to be driven out by the next test. But can we guarantee we’ll write that test before integrating code? What if we’re distracted? Coming back to the code, we’ll waste a bit more time deciphering what we wrote. Keeping with an incremental delivery mentality, we choose to fix the problem now.

c2/17/Soundex.h
 
std::​string​ encodedDigits(​const​ std::​string​& word) ​const​ {
*
if​ (word.length() > 1) ​return​ encodedDigit();
 
return​ ​""​;
 
}
 
*
std::​string​ encodedDigit() ​const​ {
*
return​ ​"1"​;
*
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.209.131