- •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 9 Design Patterns and Idioms
Special Case Object (Null Object)
In the section “Don’t Pass or Return 0 (NULL, nullptr)” in Chapter 4, you learned that returning a nullptr from a function or method is bad and should be avoided. There we also discussed various strategies to avoid regular (raw) pointers in a modern C++ program. In the section “An Exception Is an Exception, Literally!” in Chapter 5, you learned that exceptions should only be used for truly exceptional cases and not for the purpose of controlling the normal program flow.
The open and interesting question is now this: How do we treat special cases that are not real exceptions (e.g., a failed memory allocation), without using a non-semantic nullptr or other weird values?
Let’s pick up our code example again, which we have seen several times before: the query of a Customer by name. See Listing 9-40.
Listing 9-40. A Lookup Method for Customers by Name
Customer CustomerService::findCustomerByName(const std::string& name) {
//Code that searches the customer by name...
//...but what shall we do, if a customer with the given name does not exist?!
}
Well, one possibility would be to return lists instead of a single instance. If the returned list is empty, the queried business object does not exist. See Listing 9-41.
Listing 9-41. An Alternative to nullptr: Returning an Empty List if the Lookup for a Customer Fails
#include "Customer.h"
#include <vector>
using CustomerList = std::vector<Customer>;
CustomerList CustomerService::findCustomerByName(const std::string& name) {
//Code that searches the customer by name...
//...and if a customer with the given name does not exist: return CustomerList();
}
431
Chapter 9 Design Patterns and Idioms
The returned list can now be queried in the program sequence whether it is empty. But what semantics does an empty list have? Was an error responsible for the emptiness of the list? Well, the member function std::vector<T>::empty() cannot answer this question. Being empty is a state of a list, but this state has no domain-specific semantics.
Folks, no doubt, this solution is much better than returning a nullptr, but maybe not good enough in some cases. What would be much more comfortable is a return value that can be queried about its origination cause, and about what can be done with it. The answer is the Special Case pattern!
“A subclass that provides special behavior for particular cases.”
—Martin Fowler, Patterns of
Enterprise Application Architecture [Fowler02]
The idea behind the Special Case pattern is that we take advantage of polymorphism, and that we provide classes representing the special cases, instead of returning nullptr or some other odd value. These special case classes have the same interface as the “normal” class that is expected by the callers. The class diagram in Figure 9-14 depicts such a specialization.
Figure 9-14. The class(es) representing a special case are derived from the Customer class
In C++ source code, an implementation of the Customer class and the NotFoundCustomer class representing the special case looks something like Listing 9-42 (only the relevant parts are shown).
432
Chapter 9 Design Patterns and Idioms
Listing 9-42. An Excerpt from the Customer.h File with the Customer and NotFoundCustomer Classes
#ifndef CUSTOMER_H_ #define CUSTOMER_H_
#include "Address.h" #include "CustomerId.h"
#include <memory> #include <string>
class Customer { public:
// ...more member functions here...
virtual ~Customer() = default;
virtual bool isPersistable() const noexcept {
return (customerId.isValid() && ! forename.empty() && ! surname.empty()
&&
billingAddress->isValid() && shippingAddress->isValid());
}
private:
CustomerId customerId; std::string forename; std::string surname;
std::shared_ptr<Address> billingAddress; std::shared_ptr<Address> shippingAddress;
};
class NotFoundCustomer final : public Customer { public:
bool isPersistable() const noexcept override { return false;
}
};
using CustomerPtr = std::unique_ptr<Customer>;
#endif /* CUSTOMER_H_ */
433
Chapter 9 Design Patterns and Idioms
The objects that represent the special case can now be used largely as if they were valid (normal) instances of class Customer. Permanent null-checks, even when the object is passed around between different parts of the program, are superfluous, since there is always a valid object. Many things can be done with the NotFoundCustomer object, as if it were an instance of Customer, for example, presenting it in a user interface. The object can even reveal whether it is persistable. For the “real” Customer, this is done by analyzing its data fields. In the case of the NotFoundCustomer, however, this check always has a negative result.
Compared to the meaningless null-checks, a statement like the following one makes significantly more sense:
if (customer.isPersistable()) {
// ...write the customer to a database here...
}
STD::OPTIONAL<T> [C++17]
Since C++17, there is another interesting alternative that could be used for a possibly missing result or value: std::optional<T> (defined in the <optional> header). Instances of this class template represent an “optional contained value,” that is, a value that may or may not be present.
The Customer class can be used as an optional value using std::optional<T> by introducing a type alias as follows:
#include "Customer.h" #include <optional>
using OptionalCustomer = std::optional<Customer>;
Our search function CustomerService::findCustomerByName() can now be implemented as follows:
class CustomerRepository { public:
OptionalCustomer findCustomerByName(const std::string& name) { if ( /* the search was successful */ ) {
return Customer(); } else {
434