- •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
class Shape, and not on the embedded instance of the Rectangle used. This is one small drawback of this solution: some parts inherited from the base class Shape are idle.
Obviously, with this solution we will lose the possibility that an instance of Square can be assigned to a Rectangle:
std::unique_ptr<Rectangle> rectangle = std::make_unique<Square>(); // Compiler error!
The principle behind this solution to cope with inheritance problems in OO is called “favor composition over inheritance” (FCoI), sometimes also named “favor delegation over inheritance.” For the reuse of functionality, object-oriented programming basically has two options: inheritance (“white box reuse”) and composition or delegation (“black box reuse”). It is sometimes better to treat another type in a way as it would be a black box, that is, to use it only through its well-defined public interface, instead of deriving
a subtype from this type. Reuse by composition/delegation fosters looser coupling between classes than reuse by inheritance.
Interface Segregation Principle (ISP)
We know interfaces as a way to foster loose coupling between classes. In a previous section about the open-closed principle, you learned that interfaces are a way to have an extension and variation point in the code. An interface is like a contract: classes may request services through this contract, which may be offered by other classes that fulfill the contract.
But what problems can arise when these contracts become too extensive, that is, if an interface becomes too broad or “fat”? The consequences can best be demonstrated with an example. Check out the interface in Listing 6-12.
Listing 6-12. An Interface for Birds
class Bird { public:
virtual ~Bird() = default;
virtual void fly() = 0; virtual void eat() = 0; virtual void run() = 0; virtual void tweet() = 0;
};
255
Chapter 6 Modularization
This interface is implemented by several concrete birds, for example, by a Sparrow. See Listing 6-13.
Listing 6-13. The Sparrow Class Overrides and Implements All Pure Virtual Member Functions of Bird
class Sparrow : public Bird { public:
void fly() override { //...
}
void eat() override { //...
}
void run() override { //...
}
void tweet() override { //...
}
};
So far, so good. And now assume that we have another concrete Bird: a Penguin. See Listing 6-14.
Listing 6-14. The Penguin Class
class Penguin : public Bird { public:
void fly() override { // ???
}
//...
};
Although a penguin is undoubtedly a bird, they cannot fly. Although our interface is relatively small, because it declares only four simple member functions, these declared services cannot, obviously, be offered by each bird species.
256
Chapter 6 Modularization
The interface segregation principle (ISP) states that an interface should not be bloated with member functions that are not required by implementing classes, or that these classes cannot implement in a meaningful way. In our example, the Penguin class cannot provide a meaningful implementation for Bird::fly(), but Penguin is enforced to overwrite that member function.
The interface segregation principle says that we should segregate a “fat interface” into smaller and highly cohesive interfaces. The resulting small interfaces are also referred to as role interfaces. See Listing 6-15.
Listing 6-15. The Three Role Interfaces as a Better Alternative to the Broad Bird Interface
class Lifeform { public:
virtual ~Lifeform() = default; virtual void eat() = 0; virtual void move() = 0;
};
class Flyable { public:
virtual ~Flyable() = default; virtual void fly() = 0;
};
class Audible { public:
virtual ~Audible() = default; virtual void makeSound() = 0;
};
These small role interfaces can now be combined very flexibly. This means that the implementing classes only need to provide a meaningful functionality for those declared member functions, which they can implement in a sensible manner. See Listing 6-16.
257
Chapter 6 Modularization
Listing 6-16. The Sparrow and Penguin Classes Implement the Relevant Interfaces
class Sparrow : public Lifeform, public Flyable, public Audible { //...
};
class Penguin : public Lifeform, public Audible { //...
};
Acyclic Dependency Principle
Sometimes there is the need for two classes to “know” each other. For example, let’s assume that we’re developing a web shop. So that certain use cases can be implemented, the class representing a customer in this web shop must know its related account.
For other use cases, it is necessary that the account can access its owner, which is a customer.
In UML, this mutual relationship looks like Figure 6-5.
Figure 6-5. The association relationships between the Customer and Account classes
This is known as a circular dependency. Both classes, either directly or indirectly, depend on each other. In this case, there are only two classes. Circular dependencies can also occur with several software units involved.
Let’s look at how that circular dependency shown in Figure 6-4 can be implemented in C++.
What definitely would not work in C++ is Listings 6-17 and 6-18.
258
Chapter 6 Modularization
Listing 6-17. The Contents of the Customer.h File
#pragma once
#include "Account.h"
class Customer { // ...
private:
Account account_;
};
Listing 6-18. The Contents of the Account.h File
#pragma once
#include "Customer.h"
class Account { private:
Customer owner_;
};
I think that the problem is obvious here. As soon as someone used the Account or Customer classes, they would trigger a chain reaction while compiling. For example, the Account owns an instance of Customer who owns an instance of Account who owns an instance of Customer, and so on, and so on… Due to the strict processing order of C++ compilers, this implementation will result in compiler errors.
These compiler errors can be avoided, for example, by using references or pointers in combination with forward declarations. A forward declaration is the declaration of an identifier (e.g., of a type, like a class) without defining the full structure of that identifier. Therefore, such types are sometimes also called incomplete types. Hence, they can only be used for pointers or references, but not for an instance member variable, because the compiler knows nothing about its size. See Listings 6-19 and 6-20.
259
Chapter 6 Modularization
Listing 6-19. The Modified Customer with a Forward-Declared Account
#pragma once
class Account;
class Customer { public:
// ...
void setAccount(Account* account) { account_ = account;
}
// ...
private:
Account* account_;
};
Listing 6-20. The Modified Account with a Forward-Declared Customer
#pragma once
class Customer;
class Account { public:
//...
void setOwner(Customer* customer) { owner_ = customer;
}
//...
private:
Customer* owner_;
};
Hand on heart: do you feel a little bit unwell with this solution? If yes, it’s for good reasons! The compiler errors are gone, but this “fix” produces a bad gut feeling. Listing 6-21 shows how both classes are used.
260
Chapter 6 Modularization
Listing 6-21. Creating the Instances of Customer and Account and Wiring Them Circularly Together
#include "Account.h" #include "Customer.h" // ...
Account* account = new Account { }; Customer* customer = new Customer { }; account->setOwner(customer); customer->setAccount(account);
// ...
I’m sure that a serious problem is obvious: what happens if, for example, the instance of Account will be deleted, but the instance of Customer still exists? Well, the instance
of Customer will contain a dangling pointer then, that is, a pointer to No-Man’s Land! Using or dereferencing such a pointer can cause serious issues, like undefined behavior and application crashes. Don’t have high hopes: using std::shared_ptr<T> instead of regular pointers is not a solution either. On the contrary, that will result in memory leaks.
Forward declarations are pretty useful for certain things, but using them to deal with circular dependencies is a really bad practice. It is a creepy workaround that is supposed to conceal a fundamental design problem.
The problem is the circular dependency itself. This is bad design. The Customer and Account classes cannot be separated. Thus, they cannot be used independently of one another, nor are they testable independently of one another. This makes unit testing considerably more difficult.
The problem gets even worse if we have the situation depicted in Figure 6-6.
261