- •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 3 Be Principled
add unnecessary, homemade complexity to this intrinsic complexity. Therefore, it is advisable not to use every fancy feature of your language or cool design patterns just because you can. On the other hand, do not overplay simplicity. If 10 decisions are necessary in a switch-case statement and there is no better, alternative solution, that’s just how it is.
Keep your code as simple as you can! Of course, if there are high prioritized quality requirements about flexibility and extensibility, you have to add complexity to fulfill these requirements. For instance, you can use the well-known strategy pattern (see Chapter 9 about design patterns) to introduce a flexible variation point into your code when requirements demand it. But be careful and add only the amount of complexity that makes such things easier.
“Focusing on simplicity is probably one of the most difficult things for a programmer to do. And it is a life long learning experience.”
—Adrian Bolboaca (@adibolb), April 3, 2014, on Twitter
YAGNI
“Always implement things when you actually need them, never when you just foresee that you need them.”
—Ron Jeffries, You’re NOT gonna need it! [Jeffries98]
This principle is tightly coupled to the previously discussed KISS principle. YAGNI is an acronym for “you aren’t gonna need it!” or is sometimes translated to “you ain’t gonna need it!”. YAGNI is the declaration of war against speculative generalization and over- engineering. It states that you should not write code that is not necessary at the moment, but might be in the future.
Probably every developer knows these kinds of tempting impulses in their daily work: “Maybe we could use it later…”, or “We’re going to need…” No, we aren’t gonna need it! We should under all circumstances avoid producing anything today for an uncertain and speculative future. In most cases, this code is simply not needed. But if we have implemented that unnecessary thing, we’ve wasted our precious time and the code gets more complicated than it should be! And of course, we also violate the previously discussed KISS principle. Even worse, these code pieces could be buggy and could cause serious problems!
43
Chapter 3 Be Principled
My advice is this: Trust in the power of refactoring and build things only when you know that they are actually necessary, not before.
DRY
“Copy and paste is a design error.”
—David L. Parnas
Although this principle is one of the most important, I’m quite sure that it is often violated, unintentionally or intentionally. DRY is an acronym for “don’t repeat yourself!” and states that we should avoid duplication, because duplication is evil. Sometimes this principle is also referred to as “once and only once” (OAOO).
The reason that duplication is very dangerous is obvious: when one piece is changed, its copies must be changed accordingly. And don’t have high hopes. It is a safe bet that change will occur. I think it’s unnecessary to mention that any copied piece will be forgotten sooner or later and we can say hello to bugs.
Okay, that’s it—nothing more to say? Wait, there is still something and we need to go deeper. In fact, I believe that the DRY principle is often misunderstood and also construed too pedantically by many developers! Thus, we should refresh our understanding of this principle.
It’s About Knowledge!
“Don’t Repeat Yourself (or DRY) is probably one of the most misunderstood parts of the book.”
—Dave Thomas, Orthogonality and the DRY Principle, 2003
In their brilliant book, The Pragmatic Programmer [Hunt99], Dave Thomas and Andy Hunt state that applying the DRY principle means that we have to ensure that “every piece of knowledge must have a single, unambiguous, authoritative representation within a system.” It is noticeable that Dave and Andy did not explicitly mention the code, but they talk about the knowledge.
First of all, a system’s knowledge is far broader than just its code. For instance, the DRY principle is also valid for business processes, requirements, database schemes,
44
Chapter 3 Be Principled
documentation, project plans, test plans, or the system’s configuration data. DRY affects everything! Perhaps you can imagine that strict compliance with this principle is not as easy as it might seem at first sight.
Building Abstractions Is Sometimes Hard
Moreover, an exaggerated application of the DRY principle at all costs in a code base can lead to some fiddly problems. The reason is that creating an adequate common abstraction from duplicated code pieces can quickly become a tricky task, sometimes deteriorating the readability and comprehensibility of the code.
The annoyance becomes really big if there are requirement changes or functional enhancements that affect only one locus of usage of a multiple used abstraction, as the following example demonstrates.
Let’s look at the following two (simplified) classes (Listings 3-1 and 3-2) from software for an online mail order business.
Listing 3-1. The Class for the Shopping Cart
#include "Product.h" #include <algorithm>
#include <vector>
class ShoppingCart { public:
void addProduct(const Product& product) { goods.push_back(product);
}
void removeProduct(const Product& product) { std::erase(goods, product);
}
private: std::vector<Product> goods;
};
45
Chapter 3 Be Principled
Listing 3-2. The Class Used to Ship the Ordered Products
#include "Product.h" #include <algorithm>
#include <vector>
class Shipment { public:
void addProduct(const Product& product) { goods.push_back(product);
}
void removeProduct(const Product& product) { std::erase(goods, product);
}
private: std::vector<Product> goods;
};
I’m pretty sure you would agree that these two classes are duplicated code and that they therefore violate the DRY principle. The only difference is the class name; all other lines of code are identical.
THE ERASE-REMOVE IDIOM (UNTIL C++20)
Before C++20, if developers wanted to eliminate elements from a container, such as a std::vector, they often applied the so-called Erase-Remove idiom on that container.
In this idiom, two steps were successively applied to the container. First, the algorithm std::remove was used to move those elements that did not match the removal criteria, to the front of the container. The name of this function is misleading, as no elements are actually removed by std::remove, but are shifted to the front of the container.
After that, std::remove returns an iterator pointing to the first element of the tail elements in the container. This iterator, as well as the container’s end iterator, have then been passed to the std::vector::erase member function of the container to physically remove the tail elements. Applied to an arbitrary vector named vec, it looked like this:
46
Chapter 3 Be Principled
// Removing all elements that match 'value' from a vector before C++20: vec.erase(std::remove(begin(vec), end(vec), value), end(vec));
Since C++20, the Remove-Erase idiom is no longer necessary for this purpose. Instead, the two template functions std::erase and std::erase_if, both defined in header
<vector>, can do the job. These functions not only physically delete the elements that match the deletion criteria, but can also be used easier because it is not necessary anymore to pass two iterators. Instead, the entire container can be passed, like this:
// Removing all elements that match 'value' from a vector since C++20: std::erase(vec, value);
A suitable solution to get rid of the duplicated code seems to be to refactor the code and create a common abstraction, for instance by using inheritance, as shown in Listing 3-3.
Listing 3-3. The Base Class ProductContainer, from which ShoppingCart and Shipment Is Derived
#include "Product.h" #include <algorithm>
#include <vector>
class ProductContainer { public:
void addProduct(const Product& product) { products.push_back(product);
}
void removeProduct(const Product& product) { std::erase(goods, product);
}
private:
std::vector<Product> products;
};
class ShoppingCart : public ProductContainer { }; class Shipment : public ProductContainer { };
47