본문으로 건너뛰기

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