Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
roth_stephan_clean_c20_sustainable_software_development_patt.pdf
Скачиваний:
29
Добавлен:
27.03.2023
Размер:
7.26 Mб
Скачать

Chapter 8 Test-Driven Development

It’s Time to Clean Up Again

At this point we should take a critical look at our code once again. If we continue like this, the code will contain many code duplications, because the three while statements look very similar. We can, however, take advantage of these similarities by abstracting the code parts that are equal in all three while loops.

It’s refactoring time! The only code parts that are different in all three while loops are the Arabic number and its corresponding Roman numeral. The idea is to separate these variable parts from the stable rest of the loop.

In a first step, we introduce a struct that maps Arabic numbers to their Roman equivalents. In addition, we need an array (we will use std::array from the C++ Standard Library here) of that struct. Initially, we will only add one element to the array that allocates letter “C” to the number 100. See Listing 8-21.

Listing 8-21.  Introducing an Array that Holds Mappings Between Arabic Numbers and Their Roman Equivalents

struct ArabicToRomanMapping { unsigned int arabicNumber; std::string romanNumeral;

};

const std::array arabicToRomanMappings { ArabicToRomanMapping { 100, "C" }

};

After these preparations, we modify the first while loop in the conversion function to verify if the basic idea will work. See Listing 8-22.

Listing 8-22.  Replacing the Literals with Entries from the New Array

std::string convertArabicNumberToRomanNumeral(unsigned int arabicNumber) { std::string romanNumeral;

while (arabicNumber >= arabicToRomanMappings[0].arabicNumber) { romanNumeral += arabicToRomanMappings[0].romanNumeral; arabicNumber -= arabicToRomanMappings[0].arabicNumber;

}

359

Chapter 8 Test-Driven Development

while (arabicNumber >= 10) { romanNumeral += "X"; arabicNumber -= 10;

}

while (arabicNumber >= 1) { romanNumeral += "I"; arabicNumber--;

}

return romanNumeral;

}

All tests pass. So, we can continue to fill the array with the mappings “10-is-X” and “1-is-I”. See Listing 8-23.

Listing 8-23.  Again a Pattern Emerges: The Obvious Code Redundancy Can Be Eliminated by a Loop

const std::array arabicToRomanMappings {

ArabicToRomanMapping { 100,

"C" },

ArabicToRomanMapping

{

10,

"X"

},

ArabicToRomanMapping

{

1,

"I"

}

};

std::string convertArabicNumberToRomanNumeral(unsigned int arabicNumber) { std::string romanNumeral;

while (arabicNumber >= arabicToRomanMappings[0].arabicNumber) { romanNumeral += arabicToRomanMappings[0].romanNumeral; arabicNumber -= arabicToRomanMappings[0].arabicNumber;

}

while (arabicNumber >= arabicToRomanMappings[1].arabicNumber) { romanNumeral += arabicToRomanMappings[1].romanNumeral; arabicNumber -= arabicToRomanMappings[1].arabicNumber;

}

while (arabicNumber >= arabicToRomanMappings[2].arabicNumber) { romanNumeral += arabicToRomanMappings[2].romanNumeral; arabicNumber -= arabicToRomanMappings[2].arabicNumber;

}

return romanNumeral;

}

360

Chapter 8 Test-Driven Development

And again, all tests are passed. Excellent! But there is still a lot of duplicated code, so we have to continue our refactoring. The good news is that we can now see that the

only difference in all three while loops is just the array index. This means that we can get along with just one while loop if we iterate through the array. See Listing 8-24.

Listing 8-24.  Through the Range Based for Loop, the DRY Principle Is No Longer Violated

std::string convertArabicNumberToRomanNumeral(unsigned int arabicNumber) { std::string romanNumeral;

for (const auto& mapping : arabicToRomanMappings) { while (arabicNumber >= mapping.arabicNumber) {

romanNumeral += mapping.romanNumeral; arabicNumber -= mapping.arabicNumber;

}

}

return romanNumeral;

}

All tests pass. Wow, that’s great! Just take a look at this compact and well-readable piece of code. More mappings of Arabic numbers to their Roman equivalents can now be supported by adding them to the array. We will try this for 1,000, which must be converted into an M. Here is our next test:

checkIf(1000).isConvertedToRomanNumeral("M");

The test failed as expected. By adding another element for “1000-is-M” to the array, the new test, and of course all previously tests, should pass.

const std::array arabicToRomanMappings {

ArabicToRomanMapping { 1000,

"M" },

ArabicToRomanMapping {

100,

"C" },

ArabicToRomanMapping

{

10,

"X"

},

ArabicToRomanMapping

{

1,

"I"

}

};

A successful test run after this small change confirms our assumption: it works! That was quite easy. We can add more tests now, for example, for 2,000 and 3,000. And even 3,333 should work immediately:

361

Chapter 8 Test-Driven Development

checkIf(2000).isConvertedToRomanNumeral("MM"); checkIf(3000).isConvertedToRomanNumeral("MMM"); checkIf(3333).isConvertedToRomanNumeral("MMMCCCXXXIII");

Good. Our code works even with these cases. However, there are some Roman numerals that have not yet been implemented. For example, the 5 that has to be converted to “V”.

checkIf(5).isConvertedToRomanNumeral("V");

As expected, this test fails. The interesting question is the following: what should we do now so that the test gets passed? Maybe you think about a special treatment of this case. But is this really a special case, or can we treat this conversion in the same way as the previous and already implemented conversions?

Probably the simplest thing that could possibly work is to just add a new element at the correct index to our array? Well, maybe it’s worth it to try it out…

const std::array arabicToRomanMappings {

ArabicToRomanMapping { 1000,

"M" },

ArabicToRomanMapping {

100,

"C" },

ArabicToRomanMapping {

10,

"X" },

ArabicToRomanMapping

{

5,

"V"

},

ArabicToRomanMapping

{

1,

"I"

}

};

Our assumption was true: All tests are passed! Even Arabic numbers like 6 and 37 should be converted correctly to their Roman equivalent. We verify that by adding assertions for these cases:

checkIf(6).isConvertedToRomanNumeral("VI"); //...

checkIf(37).isConvertedToRomanNumeral("XXXVII");

Approaching the Finish Line

And it comes as no surprise that we can use basically the same approach for “50-is-L” and “500-is-D”.

362

Chapter 8 Test-Driven Development

Next, we need to deal with the implementation of the so-called subtraction notation; for example, the Arabic number 4 has to be converted to the Roman numeral “IV”. How could we implement these special cases elegantly?

Tip  if you ask yourself how to find all the important test cases for this code kata, I just want to remind you about the topics of equivalence partitioning and boundary value analysis discussed in Chapter 2.

Well, after a short consideration it becomes obvious that these cases are nothing really special! Ultimately, it is of course not forbidden to add a mapping rule to our array where the string contains two characters instead of one. For instance, we can just add a new “4-is-IV” entry to the arabicToRomanMappings array. Maybe you will say, "Isn’t that a hack?” No, I don’t think so. It is pragmatic and easy, without making things unnecessarily complicated.

Therefore, we first add a new test that will fail:

checkIf(4).isConvertedToRomanNumeral("IV");

For the new test to be passed, we add the corresponding mapping rule for 4 (see the penultimate entry in the array):

const std::array arabicToRomanMappings {

ArabicToRomanMapping { 1000,

"M"

},

ArabicToRomanMapping {

500,

"D"

},

ArabicToRomanMapping {

100,

"C"

},

ArabicToRomanMapping {

50,

"L"

},

ArabicToRomanMapping {

10,

"X"

},

ArabicToRomanMapping {

5,

"V"

},

ArabicToRomanMapping {

4,

"IV"

},

ArabicToRomanMapping {

1,

"I"

}

};

After we’ve executed all tests and verified that they passed, we can be certain that our solution also works for 4! Hence, we can repeat that pattern for “9-is-IX,” “40-is-XL,” “90-is-XC,” and so on. The schema is always the same, so I do not show the resulting source code here (the final result with the complete code is shown in Listing 8-25), but I think it’s not hard to comprehend.

363