- •Table of Contents
- •About the Author
- •About the Technical Reviewer
- •Acknowledgments
- •Software Entropy
- •Clean Code
- •C++11: The Beginning of a New Era
- •Who This Book Is For
- •Conventions Used in This Book
- •Sidebars
- •Notes, Tips, and Warnings
- •Code Samples
- •Coding Style
- •C++ Core Guidelines
- •Companion Website and Source Code Repository
- •UML Diagrams
- •The Need for Testing
- •Unit Tests
- •What About QA?
- •Rules for Good Unit Tests
- •Test Code Quality
- •Unit Test Naming
- •Unit Test Independence
- •One Assertion per Test
- •Independent Initialization of Unit Test Environments
- •Exclude Getters and Setters
- •Exclude Third-Party Code
- •Exclude External Systems
- •What Do We Do with the Database?
- •Don’t Mix Test Code with Production Code
- •Tests Must Run Fast
- •How Do You Find a Test’s Input Data?
- •Equivalence Partitioning
- •Boundary Value Analysis
- •Test Doubles (Fake Objects)
- •What Is a Principle?
- •KISS
- •YAGNI
- •It’s About Knowledge!
- •Building Abstractions Is Sometimes Hard
- •Information Hiding
- •Strong Cohesion
- •Loose Coupling
- •Be Careful with Optimizations
- •Principle of Least Astonishment (PLA)
- •The Boy Scout Rule
- •Collective Code Ownership
- •Good Names
- •Names Should Be Self-Explanatory
- •Use Names from the Domain
- •Choose Names at an Appropriate Level of Abstraction
- •Avoid Redundancy When Choosing a Name
- •Avoid Cryptic Abbreviations
- •Avoid Hungarian Notation and Prefixes
- •Avoid Using the Same Name for Different Purposes
- •Comments
- •Let the Code Tell the Story
- •Do Not Comment Obvious Things
- •Don’t Disable Code with Comments
- •Don’t Write Block Comments
- •Don’t Use Comments to Substitute Version Control
- •The Rare Cases Where Comments Are Useful
- •Documentation Generation from Source Code
- •Functions
- •One Thing, No More!
- •Let Them Be Small
- •“But the Call Time Overhead!”
- •Function Naming
- •Use Intention-Revealing Names
- •Parameters and Return Values
- •Avoid Flag Parameters
- •Avoid Output Parameters
- •Don’t Pass or Return 0 (NULL, nullptr)
- •Strategies for Avoiding Regular Pointers
- •Choose simple object construction on the stack instead of on the heap
- •In a function’s argument list, use (const) references instead of pointers
- •If it is inevitable to deal with a pointer to a resource, use a smart one
- •If an API returns a raw pointer...
- •The Power of const Correctness
- •About Old C-Style in C++ Projects
- •Choose C++ Strings and Streams over Old C-Style char*
- •Use C++ Casts Instead of Old C-Style Casts
- •Avoid Macros
- •Managing Resources
- •Resource Acquisition Is Initialization (RAII)
- •Smart Pointers
- •Unique Ownership with std::unique_ptr<T>
- •Shared Ownership with std::shared_ptr<T>
- •No Ownership, but Secure Access with std::weak_ptr<T>
- •Atomic Smart Pointers
- •Avoid Explicit New and Delete
- •Managing Proprietary Resources
- •We Like to Move It
- •What Are Move Semantics?
- •The Matter with Those lvalues and rvalues
- •rvalue References
- •Don’t Enforce Move Everywhere
- •The Rule of Zero
- •The Compiler Is Your Colleague
- •Automatic Type Deduction
- •Computations During Compile Time
- •Variable Templates
- •Don’t Allow Undefined Behavior
- •Type-Rich Programming
- •Know Your Libraries
- •Take Advantage of <algorithm>
- •Easier Parallelization of Algorithms Since C++17
- •Sorting and Output of a Container
- •More Convenience with Ranges
- •Non-Owning Ranges with Views
- •Comparing Two Sequences
- •Take Advantage of Boost
- •More Libraries That You Should Know About
- •Proper Exception and Error Handling
- •Prevention Is Better Than Aftercare
- •No Exception Safety
- •Basic Exception Safety
- •Strong Exception Safety
- •The No-Throw Guarantee
- •An Exception Is an Exception, Literally!
- •If You Can’t Recover, Get Out Quickly
- •Define User-Specific Exception Types
- •Throw by Value, Catch by const Reference
- •Pay Attention to the Correct Order of Catch Clauses
- •Interface Design
- •Attributes
- •noreturn (since C++11)
- •deprecated (since C++14)
- •nodiscard (since C++17)
- •maybe_unused (since C++17)
- •Concepts: Requirements for Template Arguments
- •The Basics of Modularization
- •Criteria for Finding Modules
- •Focus on the Domain of Your Software
- •Abstraction
- •Choose a Hierarchical Decomposition
- •Single Responsibility Principle (SRP)
- •Single Level of Abstraction (SLA)
- •The Whole Enchilada
- •Object-Orientation
- •Object-Oriented Thinking
- •Principles for Good Class Design
- •Keep Classes Small
- •Open-Closed Principle (OCP)
- •A Short Comparison of Type Erasure Techniques
- •Liskov Substitution Principle (LSP)
- •The Square-Rectangle Dilemma
- •Favor Composition over Inheritance
- •Interface Segregation Principle (ISP)
- •Acyclic Dependency Principle
- •Dependency Inversion Principle (DIP)
- •Don’t Talk to Strangers (The Law of Demeter)
- •Avoid Anemic Classes
- •Tell, Don’t Ask!
- •Avoid Static Class Members
- •Modules
- •The Drawbacks of #include
- •Three Options for Using Modules
- •Include Translation
- •Header Importation
- •Module Importation
- •Separating Interface and Implementation
- •The Impact of Modules
- •What Is Functional Programming?
- •What Is a Function?
- •Pure vs Impure Functions
- •Functional Programming in Modern C++
- •Functional Programming with C++ Templates
- •Function-Like Objects (Functors)
- •Generator
- •Unary Function
- •Predicate
- •Binary Functors
- •Binders and Function Wrappers
- •Lambda Expressions
- •Generic Lambda Expressions (C++14)
- •Lambda Templates (C++20)
- •Higher-Order Functions
- •Map, Filter, and Reduce
- •Filter
- •Reduce (Fold)
- •Fold Expressions in C++17
- •Pipelining with Range Adaptors (C++20)
- •Clean Code in Functional Programming
- •The Drawbacks of Plain Old Unit Testing (POUT)
- •Test-Driven Development as a Game Changer
- •The Workflow of TDD
- •TDD by Example: The Roman Numerals Code Kata
- •Preparations
- •The First Test
- •The Second Test
- •The Third Test and the Tidying Afterward
- •More Sophisticated Tests with a Custom Assertion
- •It’s Time to Clean Up Again
- •Approaching the Finish Line
- •Done!
- •The Advantages of TDD
- •When We Should Not Use TDD
- •TDD Is Not a Replacement for Code Reviews
- •Design Principles vs Design Patterns
- •Some Patterns and When to Use Them
- •Dependency Injection (DI)
- •The Singleton Anti-Pattern
- •Dependency Injection to the Rescue
- •Adapter
- •Strategy
- •Command
- •Command Processor
- •Composite
- •Observer
- •Factories
- •Simple Factory
- •Facade
- •The Money Class
- •Special Case Object (Null Object)
- •What Is an Idiom?
- •Some Useful C++ Idioms
- •The Power of Immutability
- •Substitution Failure Is Not an Error (SFINAE)
- •The Copy-and-Swap Idiom
- •Pointer to Implementation (PIMPL)
- •Structural Modeling
- •Component
- •Interface
- •Association
- •Generalization
- •Dependency
- •Template and Template Binding
- •Behavioral Modeling
- •Activity Diagram
- •Action
- •Control Flow Edge
- •Other Activity Nodes
- •Sequence Diagram
- •Lifeline
- •Message
- •State Diagram
- •State
- •Transitions
- •External Transitions
- •Internal Transitions
- •Trigger
- •Stereotypes
- •Bibliography
- •Index
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