- •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
Don’t Talk to Strangers (The Law of Demeter)
Do you remember the car I talked about earlier in this chapter? I described this car as a composition of several parts, for example, body, engine, gears, and so on. And I explained that these parts can consist of parts, which for themselves can also consist of several parts, etc. This leads to a hierarchical top-down decomposition of a car. Of course, a car can have a driver who wants to drive it.
Visualized as an UML class diagram, an excerpt from the car’s decomposition can look like Figure 6-11.
Figure 6-11. The hierarchical decomposition of a simple car
According to the single responsibility principle discussed in Chapter 5, everything is fine, because every class has a well-defined responsibility.
Now let’s assume that the driver wants to drive the car. This could be implemented in the Driver class, as shown in Listing 6-25.
269
Chapter 6 Modularization
Listing 6-25. An Excerpt from the Implementation of the Driver Class
class Driver { public:
// ...
void drive(Car& car) const { Engine& engine = car.getEngine();
FuelPump& fuelPump = engine.getFuelPump(); fuelPump.pump();
Ignition& ignition = engine.getIgnition(); ignition.powerUp();
Starter& starter = engine.getStarter(); starter.revolve();
}
// ...
};
What is the problem here? Would you expect as a driver of a car to directly access your car’s engine, to turn on the fuel pump, turn on the ignition system, and let the starter revolve? I go even further: are you even interested in the fact that your car consists of these parts if you just want to drive it?!
I’m pretty sure your clear answer would be no!
Now let’s take a look at Figure 6-12, depicting the relevant part from the UML class diagram to see what impact this implementation has on the design.
270
Chapter 6 Modularization
Figure 6-12. The bad dependencies of the Driver class
As can easily be seen in Figure 6-12, the Driver class has many awkward dependencies. The Driver is not only dependent from Engine. The class has also several dependency relationships to parts of the Engine. It is easy to imagine that this has some disadvantageous consequences.
What would happen, for example, if the combustion engine was replaced by an electric power train? An electric drive doesn’t have a fuel pump, an ignition system, and a starter. Thus, the consequences would be that the implementation of the class driver would have to be adapted. This violates the open-closed principle (see the earlier
271
Chapter 6 Modularization
section). Furthermore, all public getters that expose the innards of the Car and the Engine to their environment are violating the information hiding principle (see Chapter 3).
Essentially, the previous software design violates the Law of Demeter (LoD), also known as the Principle of Least Knowledge. The Law of Demeter can be regarded
as a principle that says something like “don’t talk to strangers”, or “only talk to your immediate neighbors.” This principle states that you should do shy programming, and the goal is to govern the communication structure within an object-oriented design.
The Law of Demeter postulates the following rules:
•\ |
A member function is allowed to call other member functions in its |
|
own class scope directly. |
•\ |
A member function is allowed to call member functions on member |
|
variables that are in its class scope directly. |
•\ |
If a member function has parameters, the member function is |
|
allowed to call the member functions of these parameters directly. |
•\ |
If a member function creates local objects, the member function is |
|
allowed to call member functions on those local objects. |
If one of these four aforementioned kinds of member function calls returns an object that is structurally farther than the immediate neighbors of the class, it is forbidden to call a member function on that object.
WHY THIS RULE IS NAMED LAW OF DEMETER
The name of this principle goes back to the Demeter Project about aspect-oriented software development, where these rules were formulated and strictly applied. The Demeter Project was a research project in the late 1980s with a main focus on making software easier to maintain and expand through adaptive programming. The Law of Demeter was discovered and proposed by Ian M. Holland and Karl Lieberherr who worked in that project. In Greek mythology, Demeter is the sister of Zeus and the goddess of agriculture.
So, what is now the solution in our example to get rid of the bad dependencies? Quite simply, we should ask ourselves what does a driver really want to do? The answer is easy: he wants to start the car! See Listing 6-26.
272
Chapter 6 Modularization
Listing 6-26. The Only Thing the Refactored Class Driver Has To Do Is Start the Car
class Driver { public:
// ...
void drive(Car& car) const { car.start();
}
// ...
};
And what does the car do with this start command? Also, quite simple: it delegates this method call to its engine. See Listing 6-27.
Listing 6-27. The Car Delegates the Start Command to its Engine
class Car { public:
// ...
void start() { engine.start();
}
// ...
private:
Engine engine;
};
Last but not least, the engine knows how it can execute the start process by calling the appropriate member functions in the correct order on its parts, which are its immediate neighbors in the software design. See Listing 6-28.
Listing 6-28. The Engine Internally Causes Everything to Be Fired Up
class Engine { public:
// ...
void start() { fuelPump.pump(); ignition.powerUp();
273
Chapter 6 Modularization
starter.revolve();
}
// ...
private:
FuelPump fuelPump; Ignition ignition; Starter starter;
};
The positive effect of these changes on the object-oriented design can be very clearly seen in the class diagram depicted in Figure 6-13.
Figure 6-13. Fewer dependencies after the application of the Law of Demeter
The annoying dependencies of the driver to the car’s parts are gone. Instead, the driver can start the car, regardless of the internal structure of the car. The Driver
class doesn’t know that there is an Engine, a FuelPump, etc. All those bad public getter functions, which revealed the innards of the car or the engine to all other classes, are gone. This also means that changes to the Engine and its parts have very local impacts and will not result in cascading changes straight through the whole design.
274