- •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
Following the Law of Demeter when designing software can reduce the number of dependencies significantly. This leads to loose coupling and fosters both the information hiding principle and the open-closed principle. As with many other principles and rules, too, there may be some justified exceptions, where a developer must vary from this principle for very good reasons.
Avoid Anemic Classes
In several projects, I’ve seen classes that looked like the one in Listing 6-29.
Listing 6-29. A Class Without Functionality that Serves Only as a Bucket for a Bunch of Data
class Customer { public:
void setId(const unsigned int id); unsigned int getId() const;
void setForename(std::string_view forename); std::string getForename() const;
void setSurname(std::string_view surname); std::string getSurname() const;
//...more setters/getters here...
private:
unsigned int id; std::string forename; std::string surname;
// ...more attributes here...
};
This domain class, representing a customer in an arbitrary software system, does not contain any logic. The logic is in a different place, even that logic which represents exclusive functionality for the Customer, that is, operating only on attributes of the
Customer.
Programmers who did this are using objects as bags for a bunch of data. This is just procedural programming with data structures, and it has nothing to do with object- orientation. Also all those setters/getters are totally foolish and violate the information hiding principle severely—actually we could use a simple C-structure (struct) here.
275
Chapter 6 Modularization
Such classes are called anemic classes and should be avoided at all costs. They can often be found in a software design that is an anti-pattern that has been called the
Anemic Domain Model by Martin Fowler [Fowler03]. It is the exact opposite of the basic idea of object-oriented design, which is to combine data and the functionality that works with the data together into cohesive units.
As long as you do not violate the Law of Demeter, you should insert logic into (domain) classes, if this logic is operating on attributes of that class or collaborates only with the immediate neighbors of the class.
Tell, Don’t Ask!
The principle Tell, Don’t Ask has some similarities with the previously discussed Law of Demeter. This principle is the “declaration of war” to all those public get methods, which reveals something about the internal state of an object. Tell Don’t Ask also fosters encapsulation and strengthens information hiding (see Chapter 3). But first and foremost, this principle is about strong cohesion.
Let’s examine a small example. Let’s assume that the member function Engine::start() from the Car example from the section about the Law Of Demeter is implemented as shown in Listing 6-30.
Listing 6-30. A Possible, But Not Recommendable, Implementation of the Engine::start() Member Function
class Engine { public:
// ...
void start() {
if (! fuelPump.isRunning()) { fuelPump.powerUp();
if (fuelPump.getFuelPressure() < NORMAL_FUEL_PRESSURE) { fuelPump.setFuelPressure(NORMAL_FUEL_PRESSURE);
}
}
if (! ignition.isPoweredUp()) { ignition.powerUp();
}
if (! starter.isRotating()) {
276
Chapter 6 Modularization
starter.revolve();
}
if (engine.hasStarted()) { starter.openClutchToEngine(); starter.stop();
}
}
// ...
private:
FuelPump fuelPump; Ignition ignition; Starter starter;
static const unsigned int NORMAL_FUEL_PRESSURE { 120 };
};
As it is easy to see, the start() method of the Engine class queries many states from its parts and responds accordingly. Furthermore, the Engine checks the fuel pressure of the fuel pump and adjusts it if it is too low. This also means that the Engine must know the value for the normal fuel pressure. Due to the numerous if branches, the cyclomatic complexity (see Chapter 4) is high.
The Tell Don’t Ask principle reminds us that we should not ask an object to expose information about its internal state and decide outside of this object what to do, if this object could decide it on its own. Basically, this principle reminds us that in object- orientation, data, and the operations operating on these data, are to be combined to cohesive units.
If we apply this principle to the example, the Engine::start() method would only tell its parts what they should do, as shown in Listing 6-31.
Listing 6-31. Delegating Stages of the Starting Procedure to the Responsible Parts of the Engine
class Engine { public:
// ...
void start() { fuelPump.pump(); ignition.powerUp();
277
Chapter 6 Modularization
starter.revolve();
}
// ...
private:
FuelPump fuelPump; Ignition ignition; Starter starter;
};
The parts can decide for themselves how they want to execute this command, because they have the knowledge about it. For example, the FuelPump can do all the things what it has to do to build up fuel pressure, as shown in Listing 6-32.
Listing 6-32. An Excerpt from the FuelPump Class
class FuelPump { public:
// ...
void pump() {
if (! isRunning) { powerUp(); setNormalFuelPressure();
}
}
// ...
private:
void powerUp() { //...
}
void setNormalFuelPressure() {
if (pressure != NORMAL_FUEL_PRESSURE) { pressure = NORMAL_FUEL_PRESSURE;
}
}
278