본문으로 건너뛰기

Packages

This chapter introduces the package as a higher-level unit of physical design for organizing very large C++ systems.

7.1 From Components to Packages

Package:
A collection of components organized as a physically cohesive unit.

A package groups related components that together serve a common semantic purpose.

System
└── Package
├── Component A
├── Component B
└── Component C

A package may contain:

  • A horizontal collection of reusable components.
  • A specialized subsystem.
  • A hierarchy of cooperating implementation components.
  • Wrapper components that present the package interface.

Packages allow architects to reason about large systems at a higher level than individual components.

Package Structure

A package physically consists of:

  • Component header files.
  • Component implementation files.
  • Component test drivers.
  • A package library.
  • Dependency information.
  • A list of exported headers.

Conceptually:

package/
├── source/
│ ├── pkg_a.h
│ ├── pkg_a.c
│ ├── pkg_a.t.c
│ └── ...
├── dependencies
└── exported

Dependencies File:
Specifies the other packages on which the current package is authorized to depend.

Exported File:
Specifies the component headers that are made available to clients outside the package.

Not every component header in a package must be exported.

Package Dependency

Package DependsOn:
A package x DependsOn package y if one or more components in x DependsOn one or more components in y.

Package X
└── Component A

Package Y
└── Component B

Therefore:

Package X DependsOn Package Y

Package dependencies abstract away individual cross-package component dependencies.

Package Composition

A package should be formed according to:

  • Semantic cohesion.
  • Physical dependency structure.
  • Development ownership.
  • Potential for independent reuse.

Principle:
Highly coupled parts of a system are often better placed in the same package.

7.2 Registered Package Prefixes

Large systems require a systematic way to identify where globally visible constructs belong.

Registered Package Prefix:
A unique prefix assigned to a package and prepended to globally visible identifiers and source-file names belonging to that package.

Example:

Package: geom

Component:
geom_point

Class:
geom_Point

7.2.1 The Need for Prefixes

Major Design Rule:
Prepend every global identifier with its package prefix.

Example:

class geom_Point
{
};

instead of:

class Point
{
};

The package prefix prevents collisions among similarly named constructs from different packages.

The prefix applies to file-scope identifiers such as:

  • Classes.
  • Structs.
  • Unions.
  • Free operators.
  • Exceptional global functions or variables.
  • File-scope typedefs or enumerations when they must exist.

Identifiers inside class scope do not require the package prefix because the enclosing class already provides scope.

Names with internal linkage confined to one implementation file also do not normally require the prefix.

Source File Prefixes

Major Design Rule:
Prepend every source-file name with its package prefix.

geom_point.h
geom_point.c
geom_polygon.h
geom_polygon.c

The prefix helps ensure unique source-file names and identifies the package in which a component is defined.

7.2.2 Namespaces

Namespace:
A C++ language mechanism for placing declarations in a named scope to avoid global-name collisions.

namespace geom
{
class Point
{
};
}

Usage:

geom::Point point;

Namespaces and package prefixes both help prevent name collisions, but the book treats them as serving different purposes.

Principle:
The dominant purpose of a package prefix is to identify uniquely the physical package in which a component or class is defined.

Namespace
└── Language-level name scope

Package Prefix
└── Physical package identification

A prefix helps developers identify:

  • The package containing a class.
  • The library that must be linked.
  • The source location of a component.
  • The package dependencies visible in source code.

7.2.3 Preserving Prefix Integrity

A package prefix should correspond to one physical package.

Principle:
Ideally, a package prefix conveys logical and organizational meaning while identifying the physical library in which the component or class is defined.

Do not use the same package prefix for components stored in unrelated physical libraries.

Prefix

One Physical Package

One Cohesive Ownership Boundary

If a component is developed locally, either:

  1. Give it the local package prefix and keep it local.
  2. Transfer it to the package that semantically owns it and adopt that package's prefix.

Avoid claiming another package's prefix while keeping the component physically elsewhere.

7.3 Package Levelization

Package-level dependencies should form the same kind of acyclic hierarchy required for components.

7.3.1 The Importance of Levelizing Packages

Major Design Rule:
Avoid cyclic dependencies among packages.

Good

Package C

Package B

Package A

Bad

Package A ↔ Package B

Avoiding cycles among packages improves:

  1. Development.
  2. Marketing.
  3. Usability.
  4. Production.
  5. Reliability.

Development

Acyclic package dependencies provide at least one valid library-link order.

Cyclic package dependencies can require libraries to be searched repeatedly and can make link order fragile.

Marketing

Optional packages cannot remain truly optional if the core system depends on them.

Mutually dependent add-on packages cannot be supplied independently.

Usability

Reducing package dependencies reduces the number of libraries a client must link.

Production

Acyclic package dependencies support staged releases in dependency order.

Reliability

Acyclic package hierarchies support hierarchical and incremental testing at a large scale.

7.3.2 Package Levelization Techniques

Techniques used to levelize components can also be applied to packages.

These include:

  • Escalation.
  • Demotion.
  • Factoring.
  • Repackaging.

Escalation

Mutually dependent functionality can be moved to a new higher-level package.

Before

Package A ↔ Package B

After

Package C
/ \
↓ ↓
Package A Package B

Presentation Packages

A single package may not be able to contain every type directly exposed to clients of a multi-package subsystem.

Principle:
It is not necessarily possible to assign a single package prefix to every component directly used by clients of a multi-package subsystem.

A common solution is to separate:

  • Low-level protocol packages.
  • High-level wrapper packages.
High Level
└── Wrapper Package

Middle
└── Implementation Packages

Low Level
└── Protocol Package

This preserves package-level levelization.

7.3.3 Partitioning a System

Levelizability alone is not sufficient for good package design.

A package must also be cohesive.

Principle:
When adding a new component to a package, both its logical and physical characteristics should be considered.

A component may be semantically related to a package but introduce expensive new dependencies.

In that case, placing it in a separate package may be preferable.

Package Character
├── Semantic Cohesion
├── Dependency Weight
├── Ownership
└── Reuse Characteristics

Package boundaries should therefore reflect both meaning and physical dependency cost.

7.3.4 Multi-Site Development

The geographic distribution of developers should be considered together with package dependencies.

Cross-site dependencies increase coordination cost.

Site A
├── Package A
└── Package B

Site B
├── Package C
└── Package D

When possible, package ownership should be assigned so that highly interacting packages are developed within the same site or team.

The physical architecture of a large system therefore reflects not only application structure but also development organization.

7.4 Package Insulation

Package Insulation:
Reducing the physical interface of a package so that clients depend only on the component headers they actually need.

Principle:
Minimizing the number and size of exported header files enhances usability.

Package
├── Exported Headers
│ └── Client Interface

└── Unexported Headers
└── Internal Implementation

A component header must generally be exported when:

  1. Clients need the component directly to use package functionality.
  2. An exported component fails to insulate clients from that component's definition.
  3. Another package must reuse that component independently.

If none of these applies, the header can remain internal to the package.

Horizontal Packages

A horizontal reusable package may naturally export many component headers.

Package
├── A
├── B
├── C
└── D

Many independently reusable interfaces

Tree-Like Packages

A hierarchical application package may expose only a small number of wrapper headers.

Client

Wrapper

Internal Components

This creates both logical and physical abstraction.

Exporting Internal Components

Exporting an internal header enables reuse but also creates additional interpackage coupling.

Once a header is exported:

  • Clients may depend on it.
  • Changing it may trigger client recompilation.
  • Package-level dependencies may increase.
  • The package becomes harder to modify freely.

Therefore, exporting headers should be deliberate.

7.5 Package Groups

For very large systems, packages themselves may be too fine-grained for architectural reasoning.

Package Group:
A collection of packages organized as a physically cohesive unit.

System
└── Package Group
├── Package A
├── Package B
└── Package C

A package is commonly owned by one developer.

A package group is commonly owned by a project manager or principal engineer and implemented by a team.

Group Dependency

Package Group DependsOn:
A package group g DependsOn group h if one or more packages in g DependsOn one or more packages in h.

Group dependencies should also be acyclic.

Group Level 3
└── Application

Group Level 2
└── Core

Group Level 1
└── Reusable Libraries

Group-Level Presentation

A group can use:

  • Low-level protocol packages.
  • Intermediate implementation packages.
  • High-level wrapper packages.

Principle:
Demoting protocols and escalating wrappers within a package group can help avoid cyclic dependencies between exported presentation packages and unexported implementation packages.

High Level
└── Wrapper Package

Middle
└── Internal Packages

Low Level
└── Protocol Package

Group Library

Individual package libraries may be combined into a single group library for client convenience.

However, individual package identity must still be preserved.

Principle:
Grouping packages does not eliminate the need for unique package prefixes.

During development, separately instrumented package libraries should remain accessible for debugging and testing.

7.6 The Release Process

Large systems require stable internal releases.

A release provides a known, tested snapshot on which higher-level developers can depend.

Layer

Layer:
All package groups at the same group level.

Layer 3
└── Groups at Level 3

Layer 2
└── Groups at Level 2

Layer 1
└── Groups at Level 1

Because groups at a given level are independent, they can be released without depending on one another.

Release progression should move upward through the dependency hierarchy.

Release Layer 1

Release Layer 2

Release Layer 3

Higher-level development can continue temporarily against the previous lower-level release until the new one is adopted.

This provides stability while allowing staged development.

7.6.1 The Release Structure

A release structure should preserve:

  • Source files.
  • Exported headers.
  • Libraries.
  • Dependency versions.
  • Historical releases.

Conceptually:

group/
└── release/
├── dependencies/
├── source/
├── include/
├── lib/
└── exported

Dependencies Directory:
Identifies the specific releases of lower-level groups used to build the current group.

Source Directory:
Contains package source organized by package prefix.

Include Directory:
Contains headers exported by the release.

Lib Directory:
Contains the released libraries.

A new release is built and tested in levelized order against known releases of its dependencies.

Exported Release Interface

Not every header needed internally to build a group must be published to higher-level clients.

Exporting only necessary headers improves:

  • Insulation.
  • Abstraction.
  • Compile-time performance.
  • Usability.

7.6.2 Patches

Patch:
A local change to previously released software that repairs faulty or grossly inefficient functionality within a component.

A patch is intended to avoid rebuilding and rereleasing the entire system.

Released Component

Local Implementation Fix

Patch

Principle:
A patch must not affect the internal layout of any existing object.

The safest patches generally modify only implementation files.

Relatively safe changes include:

  • Changing the body of a non-inline function.
  • Changing constructs with internal linkage in an implementation file.
  • Adding a new exported header.
  • Carefully relaxing access.
  • Carefully adding non-virtual functions or operators.

Changes that can invalidate a release include:

  • Adding, removing, reordering, or modifying data members.
  • Adding, removing, or reordering virtual functions.
  • Changing function signatures or return types.
  • Changing inheritance relationships.
  • Reducing member access.
  • Other changes affecting object layout or binary compatibility.

A valid patch must preserve:

  1. Link-time compatibility.
  2. Client build stability.
  3. The ability to rebuild the complete system successfully.

Principle:
The more insulated a component is, the more likely implementation bugs can be repaired through patches.

7.7 The main Program

A large C++ system does not have one architectural "top."

It usually contains multiple executables, each with its own main.

Principle:
Factoring independently testable and potentially reusable functionality out of a translation unit that defines main allows almost the entire program implementation to be reused in a larger program.

The purpose of main should be limited to:

  • Providing the command-line interface.
  • Interpreting environment variables.
  • Managing global resources.
  • Creating and connecting reusable subsystems.
main
├── Command-Line Processing
├── Environment
├── Global Resources
└── Delegate Real Work

Reusable Components

Do not place substantial application logic directly in main.

Such logic:

  • Cannot be tested incrementally through ordinary component drivers.
  • Cannot be reused easily in another executable.
  • Couples reusable behavior to one program entry point.

Global Authority

The translation unit defining main has special responsibility for system-wide resources.

Guideline:
Avoid granting one component a privilege that would damage the whole system if every component took the same privilege.

Ordinary components should avoid unilateral global behavior.

Global new and delete

Major Design Rule:
Only the implementation file defining main is authorized to redefine global new and delete.

If independent subsystems each redefine global resources, they may become impossible to integrate.

7.8 Start-Up

Start-Up:
The period between program invocation and entry into main.

Start-Up Time / Invocation Time:
The elapsed time from program invocation until control enters main.

During start-up, non-local static objects may be constructed.

Program Invoked

Static Initialization

main()

The order of initialization across translation units is not generally under application control.

Large numbers of non-local static objects can make invocation time unacceptably long.

Principle:
Construction of each non-local static object potentially contributes to invocation time.

Modules

A logical module can be implemented as a class containing only static members.

Guideline:
Prefer modules to non-local static object instances, especially when:

  1. The construct must be accessed outside one translation unit.
  2. The construct may not be needed immediately and initialization is expensive.

7.8.1 Initialization Strategies

The chapter presents four initialization techniques:

  1. Wake-Up Initialized.
  2. Explicit init Function.
  3. Nifty Counter.
  4. Check Every Time.

The appropriate strategy depends on:

  • Initialization cost.
  • Probability of use.
  • Work performed per function call.
  • Call frequency.
  • Number of clients.
  • Need for cleanup.

7.8.1.1 Wake-Up Initialized

Wake-Up Initialized:
Arrange the module so its static state is already valid without runtime initialization work.

Use fundamental static data that can be initialized at load time.

class Registry
{
static RecordLink* s_list_p;
};
RecordLink* Registry::s_list_p = 0;
Load Time

Fundamental Static State Valid

No Start-Up Work

This is the preferred strategy when practical.

7.8.1.2 Explicit init Function

Explicit Initialization:
Provide an initialization function that must be called before the component is used.

Module::init();

Advantages:

  • Initialization can be deferred.
  • The caller controls initialization order.
  • Expensive work occurs only when desired.

Disadvantage:

  • Clients can forget to call init.

This approach is flexible but error-prone.

7.8.1.3 Nifty Counter

Nifty Counter:
A static helper object in the component header ensures initialization before client use and cleanup after the last dependent translation unit is destroyed.

Conceptually:

Header Included

Dummy Static Object

Reference Count

Initialize on First
Cleanup on Last

Advantages:

  • Automatic initialization.
  • Clients do not need explicit initialization calls.

Disadvantages:

  • Initialization occurs during start-up.
  • It can increase invocation time.
  • Library-link behavior can complicate self-registration.

7.8.1.4 Check Every Time

Check-Every-Time:
Each relevant function checks whether the component has been initialized and initializes it on first use if necessary.

if (!initialized)
{
init();
}

Advantages:

  • Initialization is deferred until actual use.
  • Clients cannot forget to initialize.
  • Unused subsystems pay no initialization cost.

Disadvantages:

  • Every call pays the check cost.
  • It may be unsuitable for lightweight, frequently called operations.
  • New functions must remember to perform the check.

7.8.2 Clean-Up

Static constructs may retain dynamically allocated memory until program termination.

This can make memory-leak regression testing difficult.

Major Design Rule:
Provide a mechanism for freeing any dynamic memory allocated to static constructs within a component.

Static Construct

Dynamic Allocation

cleanup()

Memory Released

Even if normal users simply terminate the program, explicit cleanup is valuable for testing.

7.8.3 Initialization Strategy Summary

Wake-Up Initialized
├── No runtime start-up cost
└── Best when fundamental static state is sufficient

Explicit init
├── Flexible
├── Deferred
└── Client can forget

Nifty Counter
├── Automatic
├── Initializes before use
└── Adds start-up work

Check Every Time
├── Automatic deferred initialization
├── Pay only if used
└── Per-call check overhead

7.9 Key Concepts

Large-Scale Physical Design

Package

Package
├── Cohesive Components
├── Package Library
├── Dependencies
└── Exported Headers

Package Prefix
├── Identifies Physical Package
├── Avoids Global Name Collisions
└── Prefixes Files and Global Identifiers

Package Dependencies

Must Be Acyclic

Package Levelization

Package Design
├── Semantic Cohesion
├── Physical Dependencies
├── Team Ownership
└── Reuse

Package Insulation

Minimize Exported Headers

Very Large System

Package Groups

Layers

Staged Releases

Release Maintenance
├── Stable Release
└── Patch

main
├── Command Line
├── Environment
├── Global Resources
└── Delegates Reusable Functionality

Start-Up

Static Initialization

Initialization Strategies
├── Wake-Up Initialized
├── Explicit init
├── Nifty Counter
└── Check Every Time

Cleanup

Release Dynamic Memory Held by Static Constructs