본문으로 건너뛰기

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:

  1. A cohesive physical unit containing related logical entities.
  2. A place to consider physical design issues.
  3. 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:
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.

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