- •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
When using the Command pattern in practice, you’re often confronted with the need to be able to compose a more complex command from several simple commands or to record and replay commands (scripting). In order to be able to implement such requirements in an elegant manner, the following design pattern is suitable.
Composite
A data structure widely used in computer science is that of a tree. Trees can be found everywhere. For instance, the hierarchical organization of a filesystem on a data media (e.g., a hard disk) conforms to that of a tree. The project browser of an integrated development environment (IDE) usually has a tree structure. In compiler design, the abstract syntax tree (AST), is, as the name suggests, a tree representation of the abstract syntactic structure of the source code that is usually the result of the syntax analysis phase of a compiler.
The object-oriented blueprint for a tree-like data structure is called the Composite pattern. This pattern has the following intent:
“Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.”
—Erich Gamma et al., Design Patterns [Gamma95]
Our previous example from the “Command” and “Command Processor” sections should be extended by the possibility that we can build composite commands, and that commands can be recorded and replayed. So we add a new class to the previous design, a CompositeCommand. See Listing 9-29.
Listing 9-29. A New Concrete UndoableCommand That Manages a List of Commands
#include "Command.h"
#include <ranges> #include <vector>
class CompositeCommand : public UndoableCommand { public:
412
Chapter 9 Design Patterns and Idioms
void addCommand(CommandPtr& command) { commands.push_back(command);
}
void execute() override {
for (const auto& command : commands) { command->execute();
}
}
void undo() override {
const auto& commandsInReverseOrder = std::ranges::reverse_ view(commands);
for (const auto& command : commandsInReverseOrder) { command->undo();
}
}
private:
std::vector<CommandPtr> commands;
};
The composite command has a member function called addCommand(), which allows you to add commands to an instance of CompositeCommand. Since the CompositeCommand class also implements the UndoableCommand interface, its instances can be treated
like ordinary commands. In other words, it is also possible to assemble composite commands with other composite commands hierarchically. Through the recursive structure of the Composite pattern, you can generate command trees.
The UML class diagram in Figure 9-10 depicts the extended design.
413
Chapter 9 Design Patterns and Idioms
Figure 9-10. With the added CompositeCommand (on the left), commands can now be scripted
The newly added CompositeCommand class can now be used, for example, as a macro recorder in order to record and replay command sequences. See Listing 9-30.
Listing 9-30. The New CompositeCommand in Action as a Macro Recorder
int main() {
CommandProcessor commandProcessor { }; DrawingProcessor drawingProcessor { };
auto macroRecorder = std::make_shared<CompositeCommand>();
Point circleCenterPoint { 20, 20 };
CommandPtr drawCircleCommand = std::make_shared<DrawCircleCommand> (drawingProcessor, circleCenterPoint, 10); commandProcessor.execute(drawCircleCommand);
414
Chapter 9 Design Patterns and Idioms
macroRecorder->addCommand(drawCircleCommand);
Point rectangleCenterPoint { 30, 10 };
CommandPtr drawRectangleCommand = std::make_shared<DrawRectangleCommand> (drawingProcessor, rectangleCenterPoint, 5, 8); commandProcessor.execute(drawRectangleCommand); macroRecorder->addCommand(drawRectangleCommand);
commandProcessor.execute(macroRecorder); commandProcessor. undoLastCommand();
return 0;
}
With the help of the Composite pattern, it is now very easy to assemble complex command sequences from simple commands (the latter are referred to as “leafs” in the canonical form). Since CompositeCommand also implements the UndoableCommand
interface, they can be used exactly like the simple commands. This greatly simplifies the usage through client code.
On closer inspection, there is a small disadvantage. You may have noticed that an access to the member function CompositeCommand::addCommand() is possible only if you use an instance (macroRecorder) of the concrete type CompositeCommand (see the source code). This member function is not available via the UndoableCommand interface. In other words, the promised equal treatment (remember the pattern’s intent) of composites and leafs is not given here!
If you look at the general Composite pattern in [Gamma95], you’ll see that the administrative functions for managing child elements are declared in the abstraction. In our case, however, this would mean that we would have to declare an addCommand() in the UndoableCommand interface (which would be a violation of the ISP, by the way). The fatal consequence would be that the leaf elements would have to override addCommand(), and must provide a meaningful implementation for this member function. This is not possible! What should happen, what doesn’t violate the principle of least astonishment (see Chapter 3), if we add a command to an instance of DrawCircleCommand?
If we do that, it would be a violation of the Liskov Substitution Principle (LSP; see Chapter 6). Therefore, it is better to make a tradeoff in this case and do without the equal treatment of composites and leafs.
415