- •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 6 Modularization
Figure 6-6. The impact of circular dependencies between classes in different components
The Customer and Account classes are each located in different components. Perhaps there are many more classes in each of these components, but these two classes have a circular dependency. The consequence is that this circular dependency has a impact on the architectural level. The circular dependency at the class level leads to
a circular dependency at the component level. CustomerManagement and Accounting are tightly coupled (remember the section about loose coupling in Chapter 3) and cannot be reused independently. And of course, also, an independent component test is not possible anymore. The modularization on architecture level has been practically reduced to absurdity.
The acyclic dependency principle states that the dependency graph of components or classes should have no cycles. Circular dependencies are a bad form of tight coupling and should be avoided at all costs.
Don’t sweat it! It is always possible to break a circular dependency, and the following section will show you how to avoid or break them.
Dependency Inversion Principle (DIP)
In the previous section, we experienced that circular dependencies are bad and should be avoided under all circumstances. As with many other problems related to unwanted dependencies, the concept of the interface (in C++, interfaces are simulated using abstract classes) is our friend when dealing with such troubles.
262
Chapter 6 Modularization
The goal should therefore be to break the circular dependency without losing the necessary possibility that the Customer class can access the Account class and vice versa.
The first step is that we no longer allow one of the two classes to have direct access to the other class. Instead we allow access only via an interface. Basically, it does not matter from which one of classes (Customer or Account) the interface is extracted. I’ve decided to extract an interface named Owner from Customer. Exemplary, the Owner interface declares just one pure virtual member function, which must be overridden by classes that implement this interface. See Listings 6-22 and 6-23.
Listing 6-22. An Exemplary Implementation of the New interface Owner (Owner.h)
#pragma once
#include <memory> #include <string>
class Owner { public:
virtual ~Owner() = default;
virtual std::string getName() const = 0;
};
using OwnerPtr = std::shared_ptr<Owner>;
Listing 6-23. The Customer Class Implements the Owner Interface (Customer.h)
#pragma once
#include "Owner.h" #include "Account.h"
class Customer : public Owner { public:
void setAccount(AccountPtr account) { account_ = account;
}
263
Chapter 6 Modularization
std::string getName() const override {
// return the Customer's name here...
}
// ...
private:
AccountPtr account_; // ...
};
using CustomerPtr = std::shared_ptr<Customer>;
As can easily be seen, the Customer class still knows its Account. But when we take a look at the changed implementation of the Account class, there is no dependency to Customer anymore. See Listing 6-24.
Listing 6-24. The Changed Implementation of the Account Class (Account.h)
#pragma once
#include "Owner.h"
class Account { public:
void setOwner(OwnerPtr owner) { owner_ = owner;
}
//...
private:
OwnerPtr owner_;
};
using AccountPtr = std::shared_ptr<Account>;
Depicted as an UML class diagram, the changed design at class level is shown in Figure 6-7.
264
Chapter 6 Modularization
Figure 6-7. Adding the interface has eliminated the circular dependency on class level
Excellent! With this first step in the redesign, there are no more circular dependencies at the class level. The Account class knows absolutely nothing about the Customer class. But how does the situation look from the component level, as depicted in Figure 6-8?
Figure 6-8. The circular dependency between the components is still there
265
Chapter 6 Modularization
Unfortunately, the circular dependency between the components has not been broken. The two association relationships still go from one element in the one component to one element in the other component. However, the step to achieve this goal is blindingly easy: we need to relocate the Owner interface to the other component, as depicted in Figure 6-9.
Figure 6-9. Relocating the interface also fixes the circular dependency problem at the architecture level
Great! The circular dependencies between the components have disappeared. The Accounting component is no longer dependent on CustomerManagement, and as a result, the quality of the modularization has been significantly improved. Furthermore, the Accounting component can now be tested independently.
In fact, the bad dependency between both components was not literally eliminated. On the contrary, through the introduction of the Owner interface, we have one additional dependency at the class level. What we really have done is invert the dependency.
The dependency inversion principle (DIP) is an object-oriented design principle that decouples software modules. The principle states that the basis of an object-oriented design is not the special properties of concrete software modules. Instead, their common
266
Chapter 6 Modularization
features should be consolidated in a shared used abstraction (e.g., an interface). Robert C. Martin a.k.a. “Uncle Bob,” formulated the principle as follows:
“A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.”
—Robert C. Martin [Martin03]
Note The terms “high-level modules” and “low-level modules” in this quote can be misleading. They refer not necessarily to their conceptual position within a layered architecture. A high-level module in this particular case is a software module that requires external services from another module, the so-called low- level module. High-level modules are those where an action is invoked; low-level modules are the ones where the action is performed. In some cases, these two categories may also be located on different levels of a software architecture (e.g., layers), or as in our example in different components.
The principle of dependency inversion is fundamental for what is perceived as a good object-oriented design. It fosters the development of reusable software modules by defining the provided and required external services solely through abstractions (e.g., interfaces). Consistently applied to our discussed case, we would also have to redesign the direct dependency between the Customer and the Account accordingly, as depicted in Figure 6-10.
267
Chapter 6 Modularization
Figure 6-10. Dependency inversion principle applied
The classes in both components are solely dependent on abstractions. Therefore, it is no longer important to the client of the Accounting component which class requires the Owner interface or provides the Account interface (remember the section about information hiding in Chapter 3). I have insinuated this circumstance by introducing a class that is named AnyClass, which implements Account and uses Owner.
For instance, if we have to change or replace the Customer class now, for example, because we want to mount the Accounting against a test fixture for component testing, then nothing has to be changed in the AnyClass class to achieve it. This also applies to the reverse case.
The dependency inversion principle allows software developers to design dependencies between modules purposefully, that is, to define in which direction dependencies are pointing. You want to inverse the dependency between the components, that is, Accounting should be dependent on CustomerManagement? No problem: simply relocate both interfaces from Accounting to the CustomerManagement and the dependency turns around. Bad dependencies, which reduce the maintainability and the testability of the code, can be elegantly redesigned and reduced.
268