- •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
Observer
A well-known architecture pattern for the structuring of software systems is Model- View-Controller (MVC). With the help of this architecture pattern, which is described in detail in the book Pattern-Oriented Software Architecture [Busch96], the presentation part (User Interface) of an application is usually structured. The principle behind it
is separation of concerns (SoC). Among other things, the data to be displayed, which is held in the model, is separated from the manifold visual representations (so-called views) of these data.
In MVC, the coupling between the views and the model should be as loose as possible. This loose coupling is usually realized with the Observer pattern. The Observer is a behavioral pattern that is described in [Gamma95] and it has the following intent:
“Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.”
—Erich Gamma et al., Design Patterns [Gamma95]
As usual, the pattern can best be explained using an example. Let’s consider a spreadsheet application, which is a natural constituent of many office software suites. In such an application, the data can be displayed in a worksheet, in a pie chart graphic, and in many other presentation forms; the so-called views. Different views on the data can be created and closed again.
First we need an abstract element for the views that is called Observer. See Listing 9-31.
Listing 9-31. The Observer Abstract
#include <memory>
class Observer { public:
virtual ~Observer() = default;
virtual int getId() const noexcept = 0; virtual void update() = 0;
};
bool operator==(const Observer& lhs, const Observer& rhs) {
416
Chapter 9 Design Patterns and Idioms
return lhs.getId() == rhs.getId();
}
using ObserverPtr = std::shared_ptr<Observer>;
The Observers observe a so-called Subject. For this purpose, they can be registered at the Subject, and they can also be deregistered. See Listing 9-32.
Listing 9-32. Observers Can Be Added to and Removed From a Subject
#include "Observer.h" #include <algorithm>
#include <vector>
;
class Subject { public:
void addObserver(const ObserverPtr& observerToAdd) { if (isNotYetObservingThisSubject(observerToAdd)) {
observers.push_back(observerToAdd);
}
}
void removeObserver(ObserverPtr& observerToRemove) { std::erase(observers, observerToRemove);
}
protected:
void notifyAllObservers() const {
for (const auto& observer : observers) { observer->update();
}
}
private:
std::vector<ObserverPtr> observers;
};
417
Chapter 9 Design Patterns and Idioms
In addition to the Subject class, a functor named IsEqualTo is also defined (see Chapter 7 about functors), which is used for comparisons when adding and removing observers. The functor compares the IDs of the Observer. It would also be conceivable that it compares the memory addresses of the Observer instances. Then it would even be possible for several observers of the same type to register at the Subject.
The core is the notifyAllObservers() member function. It is protected since it is intended to be called by the concrete subjects that are inherited from this one. This
function iterates over all registered observers and calls their update() member function. Let’s look at a concrete subject, the SpreadsheetModel. See Listing 9-33.
Listing 9-33. The SpreadsheetModel Is a Concrete Subject
#include "Subject.h" #include <iostream> #include <string_view>
class SpreadsheetModel : public Subject { public:
void changeCellValue(std::string_view column, const int row, const double value) {
std::cout << "Cell [" << column << ", " << row << "] = " << value << std::endl;
// Change value of a spreadsheet cell, and then...
notifyAllObservers();
}
};
This, of course, is only an absolute minimum of a SpreadsheetModel. It just serves to explain the functional principle of the pattern. The only thing you can do here is call a member function that calls the inherited notifyAllObservers() function.
The three concrete observers in our example that implement the update() member function of the Observer interface are the three views TableView, BarChartView, and PieChartView. See Listing 9-34.
418
Chapter 9 Design Patterns and Idioms
Listing 9-34. Three Concrete Views Implement the Abstract Observer Interface
#include "Observer.h" #include "SpreadsheetModel.h"
class TableView : public Observer { public:
explicit TableView(SpreadsheetModel& theModel) : model { theModel } { }
int getId() const noexcept override { return 1;
}
void update() override {
std::cout << "Update of TableView." << std::endl;
}
private: SpreadsheetModel& model;
};
class BarChartView : public Observer { public:
explicit BarChartView(SpreadsheetModel& theModel) : model { theModel } { }
int getId() const noexcept override { return 2;
}
void update() override {
std::cout << "Update of BarChartView." << std::endl;
}
private: SpreadsheetModel& model;
};
class PieChartView : public Observer { public:
419
Chapter 9 Design Patterns and Idioms
explicit PieChartView(SpreadsheetModel& theModel) : model { theModel } { }
int getId() const noexcept override { return 3;
}
void update() override {
std::cout << "Update of PieChartView." << std::endl;
}
private: SpreadsheetModel& model;
};
I think it is time again to show an overview in the form of a class diagram. Figure 9-11 depicts the structure (classes and dependencies) that have arisen.
Figure 9-11. When the SpreadsheetModel gets changed, it notifies all its observers
In the main() function, we now use the SpreadsheetModel and the three views, as shown in Listing 9-35.
420
Chapter 9 Design Patterns and Idioms
Listing 9-35. Our SpreadsheetModel and the Three Views Assembled Together and in Action
#include "SpreadsheetModel.h" #include "Views.h"
int main() {
SpreadsheetModel spreadsheetModel { };
ObserverPtr observer1 = std::make_shared<TableView>(spreadsheetModel); spreadsheetModel.addObserver(observer1);
ObserverPtr observer2 = std::make_shared<BarChartView>(spreadsheetModel); spreadsheetModel.addObserver(observer2);
spreadsheetModel.changeCellValue("A", 1, 42);
spreadsheetModel.removeObserver(observer1);
spreadsheetModel.changeCellValue("B", 2, 23.1);
ObserverPtr observer3 = std::make_shared<PieChartView>(spreadsheetModel); spreadsheetModel.addObserver(observer3);
spreadsheetModel.changeCellValue("C", 3, 3.1415926);
return 0;
}
After compiling and running the program, we see the following on the standard output:
Cell [A, 1] = 42
Update of TableView.
Update of BarChartView.
Cell [B, 2] = 23.1
Update of BarChartView.
Cell [C, 3] = 3.14153
Update of BarChartView.
Update of PieChartView.
421