- •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 4 Basics of Clean C++
Listing 4-6. The Class’s Name Provides Enough Context Information for the Attribute
class CustomerRepository { private:
unsigned int numberOfIncompleteEntries; // ...
};
“You’re creating a vocabulary, not writing a program. Be a poet for a moment. The simple, the punchy, the easily remembered will be far more effective in the long run than some long name that says it all, but in such a way that no one wants to say it at all.”
—Kent Beck, Smalltalk Best Practice Patterns, 1995
Use Names from the Domain
Maybe you have already heard of software design methodologies like object-oriented analysis and design (OOAD) or domain-driven design (DDD)? OOAD was first described by Peter Coad and Edward Yourdon in the early 1990s. It was one of the first software design methodologies in which the so-called domain of the system to be developed plays a central role. More than 10 years later, Eric Evans coined the term “domain-driven design” in his eponymous book from 2004 [Evans04]. Like OOAD, DDD is an approach in the complex object-oriented software development that primarily focuses on the core domain and domain logic.
WHAT IS A DOMAIN?
A domain in the realm of systems and software engineering commonly refers to the subject area—the sphere of knowledge, influence, or activity—in which a system of interest is intended to be used. Some examples of domains are Automotive, Medical, Healthcare, Agriculture, Space and Aviation, Online Shopping, Music Production, Railway-Transportation, Energy Economy, etc.
When a system of interest is operated just in a subarea of a domain, this is called a subdomain. For instance, subdomains of the Medical domain are Intensive Care Medicine, and imaging techniques like radiography or magnetic resonance imaging (MRI).
70
Chapter 4 Basics of Clean C++
Simply put, both methodologies (OOAD and DDD) are about trying to make your software a model of a real-life system by mapping business domain things and concepts into the code. For instance, if the software to be developed will support the business processes in a car rental, then things and concepts of car rental (e.g., rented car, car pool, rentee, rental period, rental confirmation, car usage report, accounting, etc.) should be discoverable in the design of this software. If, on the other hand, the software is developed for a certain area in the aerospace industry, things and concepts from this domain should be reflected in it.
The advantages of such an approach are enormous: the use of terms from the domain facilitates, above all, the communication between the developers and other stakeholders. DDD helps the software development team create a common model between the business and IT stakeholders in the company that the team can use to communicate about the business requirements, data entities, and process models.
A detailed introduction to OOAD and DDD is far beyond the scope of this book. If you are interested, I recommend a good, practice-oriented training to learn these methodologies.
However, it is basically always a very good idea to name modules, classes, and functions in a way that elements and concepts from the application’s domain can be rediscovered. This enables you to communicate software designs as naturally as possible. It will make code more understandable to anyone involved in solving a problem, for example, a tester or a business expert.
Take, for example, the aforementioned car rental. The class that is responsible for the use case of the reservation of a car for a certain customer could be as shown in Listing 4-7.
Listing 4-7. The Interface of a Use Case Controller Class to Reserve a Car
class ReserveCarUseCaseController { public:
Customer identifyCustomer(const UniqueIdentifier& customerId); CarList getListOfAvailableCars(const Station& atStation,
const RentalPeriod& desiredRentalPeriod) const; ConfirmationOfReservation reserveCar(const UniqueIdentifier& carId,
const RentalPeriod& rentalPeriod) const;
private:
Customer& inquiringCustomer;
};
71
Chapter 4 Basics of Clean C++
Now take a look at all those names used for the class, the methods, the arguments, and return types. They represent things that are typical for the car rental domain. If you read the methods from top to bottom, these are the individual steps that are required to rent a car. This is C++ code, but there is a great chance that nontechnical stakeholders with domain knowledge can also understand it.
Note Software developers should speak the language of their stakeholders and use domain-specific terms in their code whenever possible.
Choose Names at an Appropriate Level of Abstraction
To keep the complexity of today’s software systems under control, these systems are usually hierarchically decomposed. Hierarchical decomposition of a software system means that the entire problem is broken down and partitioned into smaller parts as subtasks, until developers get the confidence that they can manage these smaller parts. I will deepen this topic again in Chapter 6, when covering modularization of a software system.
With such decomposition, software modules are created at different levels of abstraction: starting from large components or subsystems down to very small building blocks like classes. The task, which a building block at a higher abstraction level fulfills, should be fulfilled by an interaction of the building blocks on the next lower abstraction level.
The abstraction levels introduced by this approach also have an impact on naming. Every time we go one step deeper down the hierarchy, the names of the elements get more concrete.
Imagine a web shop. On the top level there might exist a large component whose single responsibility is to create invoices. This component could have a short and descriptive name like Billing. Usually, this component consists of further smaller components or classes. For instance, one of these smaller modules could be responsible for calculating a discount. Another module could be responsible for creating invoice line items. Thus, good names for these modules could be DiscountCalculator
and LineItemFactory. If we now dive deeper into the decomposition hierarchy, the identifiers for components, classes, and functions or methods become more and more concrete, verbose, and thus also longer. For example, a small method in a class at the deepest level could have a very detailed and elongated name, like calculateReducedValueAddedTax().
72
Chapter 4 Basics of Clean C++
Note Always choose names that reflect the level of abstraction of the module, class, or (member-) function you are working in. Look to it that all instructions within a function are on the same abstraction level.
Avoid Redundancy When Choosing a Name
It is redundant to pick up a class name or other names that provide a clear context and use them as a part to build the name of a member variable. Listing 4-8 shows an example of this.
Listing 4-8. Don’t Repeat the Class’s Name in its Attributes
#include <string>
class Movie { private:
std::string movieTitle; // ...
};
Don’t do that! It is an, albeit, only very tiny violation of the DRY principle we discussed in Chapter 3. Instead, just name it title. The member variable is in the namespace of class Movie, so it’s clear without ambiguity whose title is meant: the movie’s title!
Listing 4-9 shows another example of redundancy.
Listing 4-9. Don’t Include the Attribute’s Type in its Name
#include <string>
class Movie { // ...
private:
std::string stringTitle;
};
73