- •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
Dependency Injection (DI)
“Dependency Injection is a key element of agile architecture.”
—Ward Cunningham, paraphrased from the “Agile and Traditional Development” panel discussion at Pacific NW Software Quality Conference (PNSQC) 2004
The fact that I start the section about specific design patterns with one that is not mentioned in the famous book of the Gang of Four has weighty reasons, of course. I am convinced that dependency injection is by far the most important pattern that can help software developers significantly improve their software design. This pattern can be regarded quite rightly as a game changer.
Before we dive deeper into dependency injection, I want to reckon with another pattern that is detrimental to good software design: the singleton!
The Singleton Anti-Pattern
I’m pretty sure that you already know the design pattern named singleton. It is, on first sight, a simple and widespread pattern, not only in the C++ domain (we will see soon that its supposed simplicity can be deceptive). Some code bases are even littered with singletons. This pattern is, for instance, often used for so-called loggers (objects for logging purposes), for database connections, for central user management, or
to represent things from the physical world (e.g., hardware such as USB or printer interfaces). In addition, factories and so-called utility classes, which are a colorful hodgepodge of helper functions, are often implemented as singletons. The latter are a code smell, because they are a sign of weak cohesion (see Chapter 3).
The authors of Design Patterns have been regularly asked by journalists when they will revise their book and publish a new edition. And their regular answer was that they do not see any reason for this, because the contents of the book are still largely valid. In an interview with the online journal InformIT, however, they allowed themselves to give a more detailed answer. Here is a small excerpt from the entire interview, which reveals an interesting opinion from Gamma about singletons (Larry O’Brien was the interviewer, and Erich Gamma gives the answer):
[...]
Larry: How would you refactor “Design Patterns”?
378
Chapter 9 Design Patterns and Idioms
Erich: We did this exercise in 2005. Here are some notes from our session. We have found that the object-oriented design principles and most of the patterns haven’t changed since then. (…)
When discussing which patterns to drop, we found that we still love them all. (Not really—I’m in favor of dropping Singleton. Its use is almost always a design smell.)
—Design Patterns 15 Years Later: An Interview with Erich Gamma, Richard Helm, and Ralph Johnson, 2009 [InformIT09]
So, why did Erich Gamma say that the Singleton pattern is almost always a design smell? What’s wrong with it?
To answer this, let’s first look at what goals are achieved by means of singletons. What requirements can be fulfilled with this pattern? Here is the mission statement of the Singleton pattern from the GoF book:
“Ensure a class only has one instance and provide a global point of access to it.”
—Erich Gamma, et al., Design Patterns [Gamma95]
This statement contains two conspicuous aspects. On the one hand, the mission of this pattern is to control and manage the whole lifecycle of its one-and-only instance. In accordance with the principle of separation of concerns, the management of the lifecycle of an object should be independent and separated from its application—or domain- specific business logic. In a singleton, these two concerns are mixed together.
On the other hand, a global access to its one-and-only instance is provided, so that every other object in the application can use it. This talk about a “global point of access” in the context of object-orientation appears fishy and should raise red flags.
Let’s first look at a general implementation style of a singleton in C++, the so-called Meyers’ Singleton, named after Scott Meyers, the author of the Effective C++ book [Meyers05]. See Listing 9-1.
379
Chapter 9 Design Patterns and Idioms
Listing 9-1. An Implementation of Meyers’ Singleton in Modern C++
#pragma once
class Singleton final { public:
static Singleton& getInstance() { static Singleton theInstance { }; return theInstance;
}
int doSomething() { return 42;
}
// ...more member functions doing more or less useful things here...
private:
Singleton() = default; Singleton(const Singleton&) = delete;
Singleton(Singleton&&) noexcept = delete; Singleton& operator=(const Singleton&) = delete; Singleton& operator=(Singleton&&) noexcept = delete; // ...
};
One of the main advantages of this implementation style of a singleton is that since C++11, the construction process of the one-and-only instance using a static variable inside getInstance() is thread-safe by default. Be careful, because that does not automatically mean that all the other member functions of the singleton are thread-safe too! The latter must be ensured by the developer.
In source code, the use of such a global singleton instance typically looks like Listing 9-2.
Listing 9-2. An Excerpt from the Implementation of an Arbitrary Class That Uses the Singleton
001 #include "AnySingletonUser.h"
002 #include "Singleton.h"
380
Chapter 9 Design Patterns and Idioms
003 #include <string>
004
... // ...
024
025 void AnySingletonUser::aMemberFunction() {
... // ...
040 std::string result = Singleton::getInstance().doThis();
... // ...
050 }
051
... // ...
089
090 void AnySingletonUser::anotherMemberFunction() {
... //...
098 int result = Singleton::getInstance().doThat();
... //...
104 double value = Singleton::getInstance().doSomethingMore();
... //...
110}
111// ...
I think it should now be clear what one of the main problems with singletons is. Due to their global visibility and accessibility, they are simply used anywhere inside the implementation of other classes. That means that in the software design, all the dependencies to this singleton are hidden inside the code. You cannot see these dependencies by examining the interfaces of your classes, that is, their attributes and methods.
And the AnySingletonUser class exemplified in Listing 9-2 is only representative of perhaps hundreds of classes within a large code base, many of which also use the
Singleton at different places. In other words, a singleton in OO is like a global variable in procedural programming. You can use this global object everywhere, and you cannot see that usage in the interface of the using class, but only in its implementation.
This has a significant negative impact on the dependency situation in a project, as depicted in Figure 9-1.
381
Chapter 9 Design Patterns and Idioms
Figure 9-1. Loved by everyone: the singleton!
Note Perhaps you are wondering why, when looking at Figure 9-1, there is a private member variable instance inside the Singleton class EverybodysDarling, which cannot be found in this form in Meyers’s
recommended implementation. Well, UML is programming language agnostic, that is, as a multipurpose modeling language it does not know about C++, Java, or other OO languages. In fact, in Meyers’s Singleton there is a variable that holds the one-and-only instance, but there is not a graphical notation for a variable with static storage duration in UML, because this feature is proprietary in C++. Therefore, I chose to represent this variable as a private static member. This makes the representation compatible with the no longer recommended Singleton implementation described in the GoF book [Gamma95].
I think it’s easy to imagine that all these dependencies will have major drawbacks regarding reusability, maintainablility, and testability. All those client classes of the singleton are tightly coupled to it (remember the good property of loose coupling discussed in Chapter 3).
382