Components
This chapter introduces physical design and treats the component as the fundamental unit of large-scale C++ software design.
3.1 Components versus Classes
Logical Design:
Describes the relationships and behavior of logical entities such as classes and functions.
Logical design focuses on architectural structure.
Physical Design:
Describes the organization and dependencies of physical entities such as files and components.
Physical design addresses issues such as:
- Compile-time coupling
- Link-time dependency
- Testing
- Reuse
- Build organization
Principle:
Logical design addresses architectural issues; physical design addresses organizational issues.
Component:
The smallest unit of physical design.
A component is not a class.
A component contains a cohesive subset of the logical design and may contain:
- One class
- Several closely related classes
- Iterators
- Free operator functions
- Other related logical entities
In the terminology of the book, a component consists of:
component
├── component.h
└── component.c
The implementation file includes its corresponding header.
// stack.c
#include "stack.h"
Principle:
A component, rather than a class, is the appropriate fundamental unit of design.
A component provides:
- A cohesive physical unit containing related logical entities.
- A place to consider physical design issues.
- A unit that can potentially be reused independently.
Logical Interface
Logical Interface of a Component:
Everything supplied by a component that is programmatically accessible or detectable by a client.
It includes the accessible types and functions defined by the component.
Private implementation details are not part of the logical interface.
Used-In-The-Interface of a Component:
A type is Used-In-The-Interface of a component when it is used in the public or protected interface of a class defined by that component, or by a free operator declared by the component.
Component
└── Logical Interface
├── Public Member Functions
├── Public Types
├── Public Enumerations
└── Free Operators
Physical Interface
Physical Interface of a Component:
Everything contained in the component's header file.
Physical Interface
└── Entire Header File
The physical interface includes implementation details that may not be logically visible to clients.
Therefore, a change to a private implementation detail in a header can still force clients to recompile.
Logical Interface vs. Physical Interface
Logical Interface
└── What clients can use
Physical Interface
└── Everything the compiler sees in the header
Used-In-The-Implementation of a Component:
A type is Used-In-The-Implementation of a component if that type is referred to by name anywhere in the component.
Implementation usage can create physical dependencies on other components.
3.2 Physical Design Rules
Major Design Rule: Keep Definitions Inside the Component
Logical entities declared within a component should not be defined outside that component.
Bad structure:
stack.h
└── declares Stack::push()
another.c
└── defines Stack::push()
A component should remain a self-contained physical unit.
Declaration
↓
Same Component
↓
Definition
Minor Design Rule: Matching File Names
The root names of the header and implementation files of a component should match.
stack.h
stack.c
Matching names improve:
- Maintainability
- Discoverability
- Tool support
Major Design Rule: Include Your Own Header First
The implementation file of every component should include its own header as the first substantive line.
// stack.c
#include "stack.h"
This ensures that the header can compile without depending on declarations accidentally supplied by another header.
Header Self-Sufficiency:
A component header should contain or directly obtain everything required for the header itself to parse.
Bad:
// wildthing.c
#include <iostream.h>
#include "wildthing.h"
If wildthing.h itself needs the declaration of ostream, the previous include can hide that defect.
Preferred structure:
// wildthing.c
#include "wildthing.h"
#include <iostream.h>
Guideline: Include Required Headers Directly
Clients should directly include the header that provides a required type definition.
Avoid depending on one unrelated header to include another header for you.
#include "stack.h"
Do not rely on:
mytype.h
↓ includes
stack.h
```
when the client actually uses `Stack` directly.
The important exception discussed by the book is non-private inheritance, because the derived-class definition inherently depends on the base-class definition.
### Major Design Rule: Declare External Definitions in the Header
Avoid definitions with external linkage in a component's implementation file unless they are explicitly declared in the corresponding header.
Bad:
~~~cpp
// foo.c
int size;
void f(int x, int y)
{
}
when neither size nor f is declared in foo.h.
This creates a hidden or backdoor interface.
Backdoor Interface:
Externally accessible functionality that is implemented by a component but not declared in its physical interface.
Backdoor interfaces reduce:
- Usability
- Reusability
- Maintainability
- Dependency visibility
Major Design Rule: Do Not Access External Definitions through Local Declarations
Do not locally redeclare an externally defined entity from another component.
Bad:
// bar.c
extern int size;
void f(int, int);
Instead, include the header of the component providing the entity.
#include "foo.h"
This makes the physical dependency explicit and lets the compiler detect declaration inconsistencies.
Forward Declaration
A class forward declaration is different from locally redeclaring an external function or object.
class QueueLink;
Forward declarations are useful because they can reduce unnecessary header inclusion and compile-time dependency.
3.3 The DependsOn Relation
DependsOn:
A component y DependsOn a component x if x is needed to compile or link y.
y DependsOn x
DependsOn is a physical relation.
This differs from logical relations such as:
- IsA
- Uses
- HasA
- HoldsA
Logical Relation
└── Between logical entities
DependsOn
└── Between physical entities
Compile-Time Dependency
Compile-Time Dependency:
A component y has a compile-time dependency on component x if x.h is required to compile y.c.
Example:
class String
{
CharArray d_array;
};
If CharArray is embedded directly, the compiler needs its definition.
str DependsOn chararray
Link-Time Dependency
Link-Time Dependency:
A component y has a link-time dependency on component x when y.o contains unresolved symbols that x.o may be required to resolve, directly or indirectly.
A component can depend on another at link time without depending on it directly at compile time.
Compile-Time and Link-Time Dependency
Principle:
A compile-time dependency almost always implies a link-time dependency.
Compile-Time Dependency
↓
Usually
↓
Link-Time Dependency
Transitivity
Principle:
The DependsOn relation between components is transitive.
If:
A DependsOn B
B DependsOn C
then:
A DependsOn C
Therefore, a component depends on every component reachable from it through the dependency graph.
Dependency Path:
A sequence of component dependencies connecting one component to another.
A → B → C → D
A depends on B, C, and D.
3.4 Implied Dependency
Logical relationships can imply physical dependencies between the components containing those logical entities.
Uses
Principle:
A component defining a function will usually depend physically on a component defining a user-defined type used by that function.
int Two::getInfo(const One& one)
{
return one.info();
}
Two uses One, so the component defining Two will usually depend on the component defining One.
The dependency may be:
- Compile-time
- Link-time
- Indirect link-time dependency
A Uses relation does not guarantee compile-time dependency, because a type can sometimes be treated opaquely.
class One;
void process(const One&);
However, substantive use will generally create a physical dependency.
IsA
IsA across components always implies compile-time dependency.
class Word : public String
{
};
Word IsA String
word DependsOn str
The definition of the base class must be available when defining the derived class.
HasA
HasA across components always implies compile-time dependency.
class String
{
CharArray d_array;
};
The compiler must know the complete definition of CharArray.
String HasA CharArray
str DependsOn chararray
HoldsA
HoldsA does not necessarily imply compile-time dependency.
class CharArray;
class String
{
CharArray* d_array_p;
};
A forward declaration may be sufficient in the header.
The implementation can include the complete definition later.
#include "chararray.h"
Dependency Strength
IsA
└── Guaranteed Compile-Time Dependency
HasA
└── Guaranteed Compile-Time Dependency
HoldsA
└── Usually weaker; may avoid header inclusion
Uses
└── Usually implies physical dependency
```
### Transitive Closure
**Transitive Closure:**
A dependency graph containing both direct dependencies and all dependencies implied transitively.
Given:
~~~text
A → B
B → C
the transitive closure also contains:
A → C
A component x DependsOn component y exactly when there is a dependency path from x to y.
Redundant transitive edges can be omitted from diagrams when the indirect dependency is already obvious from the path.
3.5 Extracting Actual Dependencies
During development, the actual physical dependency graph should be compared with the intended design.
Parsing the entire C++ program is possible but expensive.
The book argues that, when the physical design rules are followed, the #include graph is sufficient to infer physical dependencies.
Include Graph:
A graph whose edges represent #include relationships among component files.
x
│
└── #include "y.h"
↓
x DependsOn y
Principle:
The include graph should be sufficient to infer all physical dependencies in a compiling system when the major design rules are followed.
Substantive Use
Substantive Use:
Use of another component that actually requires its definition or functionality.
Guideline
A component x should include y.h only when x directly and substantively uses a class or free operator defined in y.
Unnecessary includes introduce unnecessary compile-time coupling.
Required Use
↓
#include
No Required Use
↓
No #include
If all required external usage is expressed through the proper headers, dependency-analysis tools can recover the actual component dependency graph from the include structure.
3.6 Friendship
Friendship affects both logical encapsulation and physical organization.
Local Friendship
Local Friendship:
Friendship granted to a logical entity defined in the same component.
Principle:
Friendship within a component is an implementation detail of that component.
class Stack
{
friend class StackIter;
};
when both Stack and StackIter belong to the same component.
Local friendship does not change the logical interface of the component.
Principle:
Granting local friendship to entities defined within the same component does not violate component encapsulation.
This is why a container and its iterator can reside in the same component.
component stack
├── Stack
└── StackIter
└── friend of Stack
Principle:
Defining an iterator with its container in the same component can improve extensibility, maintainability, and reuse while preserving encapsulation.
Long-Distance Friendship
Long-Distance Friendship:
Friendship granted to a logical entity defined in another component.
Guideline
Avoid long-distance friendship.
Principle:
Long-distance friendship violates the encapsulation of the class granting friendship.
Component A
└── Class A
↓ friend
Component B
└── Class B
This allows a physically remote component to access private implementation details.
Consequences include:
- Reduced encapsulation
- Reduced modularity
- Increased maintenance coupling
- Greater risk of misuse
3.6.1 Long-Distance Friendship and Implied Dependency
Principle:
Friendship affects access privilege but not implied dependency.
A friend declaration allows access to private information.
It does not, by itself, reverse or create a dependency from the befriending class toward the friend.
Friendship
└── Changes access privilege
Uses / IsA / HasA / HoldsA
└── Determine implied dependency
A free operator remains a separate logical entity even when it is declared as a friend.
int operator==(const Stack&, const Foo&);
The operator uses Stack and Foo.
Declaring it a friend does not make Stack depend on the operator.
Free Operators versus Member Operators
Free operators can often be placed in a separate component.
int operator==(const Stack&, const Foo&);
This can avoid a cyclic logical relationship between Stack and Foo.
By contrast:
int Stack::operator==(const Foo&) const;
int Foo::operator==(const Stack&) const;
places each use directly in the interface of the corresponding class and can create cyclic dependency.
3.6.2 Friendship and Fraud
Long-distance friendship creates an encapsulation hole that clients may exploit.
A client can potentially define a matching friend entity and gain access to private details.
Therefore, friendship should not be treated as a security boundary.
The physical-design lesson is:
Keep logically intimate entities physically close, preferably inside the same component.
High Logical Intimacy
↓
Same Component
↓
Local Friendship
```
Avoid designing systems that require private access across component boundaries.
## 3.7 Key Concepts
~~~text
Large-Scale C++ Design
↓
Physical Design
Component
├── Header File
└── Implementation File
Component Interfaces
├── Logical Interface
│ └── What clients can use
└── Physical Interface
└── Entire header
Physical Design Rules
├── Keep declarations and definitions in same component
├── Match header/implementation root names
├── Include own header first
├── Make headers self-sufficient
├── Declare external definitions in header
└── Access external definitions through headers
DependsOn
├── Compile-Time Dependency
├── Link-Time Dependency
└── Transitive
Logical Relation → Physical Dependency
├── IsA → Compile-Time
├── HasA → Compile-Time
├── HoldsA → Weaker / often Link-Time
└── Uses → Usually Physical Dependency
Include Graph
↓
Actual Dependency Graph
Friendship
├── Local Friendship
│ └── Encapsulated within component
└── Long-Distance Friendship
└── Violates encapsulation