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:
- Give it the local package prefix and keep it local.
- 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:
- Development.
- Marketing.
- Usability.
- Production.
- 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:
- Clients need the component directly to use package functionality.
- An exported component fails to insulate clients from that component's definition.
- 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:
- Link-time compatibility.
- Client build stability.
- 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:
- The construct must be accessed outside one translation unit.
- The construct may not be needed immediately and initialization is expensive.
7.8.1 Initialization Strategies
The chapter presents four initialization techniques:
- Wake-Up Initialized.
- Explicit
initFunction. - Nifty Counter.
- 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