Architecting a Component
This chapter explains how to design a component as both a logical and physical unit of abstraction.
8.1 Abstractions and Components
Abstraction:
An abstract specification of a collection of objects and related behaviors that fulfills a common purpose.
A single class is often too small to represent a complete abstraction.
An abstraction may include:
- One or more classes.
- Iterators.
- Free operators.
- Friend classes.
- Other cooperating functions.
Principle:
A class is a concrete specification of an ADT; a component is a concrete specification of an abstraction.
ADT
↓
Class
Abstraction
↓
Component
A component is therefore:
- The smallest independently reusable physical unit.
- The appropriate level for detailed logical interface design.
- A cohesive implementation of an abstraction.
8.2 Component Interface Design
A component interface must provide enough functionality for its intended clients while avoiding unnecessary complexity.
The chapter distinguishes four important interface properties.
Principle:
Private Interface
└── Sufficient
Public Interface
└── Complete
Class Interface
└── Primitive
Component Interface
└── Minimal Yet Usable
Sufficient Interface
Sufficient Interface:
An interface containing the functionality required by its known clients.
A private component used by a fixed set of clients should normally provide only what those clients need.
Known Clients
↓
Required Operations Only
↓
Sufficient Interface
Complete Interface
Complete Interface:
An interface capable of efficiently supporting the operations commonly expected for the abstraction.
Widely reusable components may need a more complete interface because future clients cannot be predicted precisely.
Private Component
└── Sufficient
Widely Reusable Component
└── More Complete
A complete interface has costs:
- More implementation work.
- More maintenance.
- Greater runtime complexity.
- Larger interface surface.
- Greater difficulty for clients to learn and use.
Principle:
Where practical, deferring unneeded functionality reduces development and maintenance cost and avoids premature commitment to a specific interface or behavior.
8.2.1 Primitive Operations
Primitive Operation:
An operation whose efficient implementation requires direct access to the private details of an object.
Example:
Set
├── add()
├── remove()
└── contains()
An operation may be logically expressible using other operations but still deserve primitive status if the alternative would be inefficient.
Can Be Implemented Externally
+
External Version Is Inefficient
↓
May Still Be Primitive
The interface of a class should contain a minimal set of primitive operations.
Useful but non-primitive operations should generally be implemented outside the class.
Primitive
└── Member Function
Useful but Non-Primitive
└── Free Function / Operator / Utility
This reduces the amount of functionality requiring access to private state.
Component Minimality
Principle:
Keeping functionality to a practical minimum enhances usability and reusability.
A component should not provide every conceivable operation.
However, frequently duplicated useful operations may justify inclusion even when they are non-primitive.
Too Little
└── Component is difficult to use
Too Much
└── Interface becomes cluttered
Goal
└── Minimal Yet Usable
8.2.2 Logical Coupling
Logical Coupling:
A dependency caused by using externally defined types in the logical interface of a component.
Example:
class my_String;
class my_Engine
{
public:
my_Engine(const my_String& name);
};
This forces clients of my_Engine to also understand or depend on my_String.
When a more general representation is sufficient:
class my_Engine
{
public:
my_Engine(const char* name);
};
the interface becomes usable in more contexts.
Principle:
Minimizing the use of externally defined types in a component's interface facilitates reuse in a wider variety of contexts.
More External Types in Interface
↓
More Logical Coupling
↓
Less General Reuse
Logical coupling can also cause physical coupling.
Therefore, unnecessary user-defined types should be avoided in widely used interfaces.
8.3 Degrees of Encapsulation
Encapsulation:
A property that allows an object's implementation to change without requiring changes to its logical interface.
Encapsulation is not necessarily absolute.
More Encapsulation
↓
More Implementation Freedom
Less Encapsulation
↓
Potentially Better Performance
Encapsulation Test
Principle:
A good test for encapsulation is whether one interface can support two significantly different implementation strategies without modification.
If changing a private representation requires changing the public interface, the implementation is not fully encapsulated.
Writable References
Returning writable references to private data weakens encapsulation.
Bad:
class Point
{
int d_x;
public:
int& x();
};
Client:
point.x() = 5;
The interface exposes the representation type.
Better:
class Point
{
public:
void setX(int value);
int x() const;
};
Now the representation can change independently of the interface.
Encapsulation and Performance
Principle:
A fully encapsulating interface may impose a significant performance burden on a particular implementation.
Suppose a Box may be represented as either:
Representation A
├── Lower-Left Point
└── Upper-Right Point
Representation B
├── Center Point
├── Width
└── Height
Returning an internally stored object by const reference is efficient, but doing so reveals which objects are physically stored.
A completely representation-independent interface may require returning all such values by value.
This increases implementation freedom but can increase runtime cost.
8.3.1 Partial Encapsulation
Some high-performance interfaces deliberately expose limited implementation assumptions.
Examples include:
- A string exposing
const char*. - An array returning a writable reference to an element.
Point& operator[](int index);
This restricts implementation choices because clients may retain the reference.
The implementation can no longer freely:
- Relocate the object.
- Compress the representation.
- Swap the object elsewhere.
- Generate the value only temporarily.
Principle:
Settling for less than full encapsulation is sometimes the right choice.
The design must balance:
Implementation Flexibility
↕
Runtime Performance
8.3.2 Return by Value
A fully encapsulating accessor can return an object by value.
Point point(int index) const;
This avoids exposing the physical location of the stored object.
However, returning heavy objects by value may require:
- Construction.
- Copying.
- Destruction.
- Dynamic allocation.
This can make total encapsulation expensive.
8.3.3 Return by Argument
Return by Argument:
Passing the address of an existing object into a function and assigning the result to that object instead of returning a new object by value.
void getPoint(Point* result, int index) const;
Principle:
Passing in the address of a previously constructed object to receive the return value can improve performance while preserving total encapsulation.
Return by Value
└── Create Temporary Object
Return by Argument
└── Reuse Existing Object
```
The benefit becomes more significant for heavyweight objects.
### Interface Comparison
~~~text
Partial Encapsulation
Point& operator[](int)
Full Encapsulation
Point point(int) const
Full Encapsulation with Return by Argument
void getPoint(Point*, int) const
The experiments in the chapter show that return by argument can significantly reduce the runtime cost of total encapsulation for heavy objects.
8.4 Auxiliary Implementation Classes
Auxiliary Implementation Class:
A small class used solely to implement a component and not intended for direct use or reuse by clients.
Two characteristics identify such a class:
- It exists only to implement the component.
- It is usually simple enough that independent testing may not be necessary.
Example:
List
└── Link
Link is an implementation detail of List.
The chapter presents several implementation strategies.
8.4.1 File-Scope Class in the Header
The auxiliary class can be defined at file scope in the component header.
class my_Link
{
int d_data;
my_Link* d_next_p;
};
class my_List
{
my_Link* d_head_p;
};
Advantages:
- Simple.
- Directly testable.
- Usable in inline function bodies.
Disadvantages:
- Affects the global name space.
- Not insulated.
- Physically coupled to clients.
This is the simplest and most common approach.
8.4.2 Separate Component
The auxiliary class can be placed in its own component.
my_list
↓
my_link
Advantages:
- Directly testable.
- Independently reusable.
- Can be insulated from clients of the principal component.
- Most flexible physical organization.
Disadvantages:
- Adds another physical component.
- Adds coupling caused by reuse.
- Often excessive for trivial implementation classes.
Use this approach when the auxiliary class is complex enough to justify independent testing or reuse.
8.4.3 Slave Class
Slave Class:
An auxiliary class whose operations are inaccessible to ordinary clients and are made available only to the primary class through friendship.
class my_List;
class my_Link
{
friend class my_List;
int d_data;
my_Link* d_next_p;
};
Advantages:
- Enforces exclusive use by the primary class.
- Can be used in inline function bodies.
Disadvantages:
- Not directly testable.
- Not insulated.
- Still affects the global name space.
- Not independently reusable.
8.4.4 Local Class in the Implementation File
The auxiliary class can be defined entirely in the implementation file.
my_list.h
└── my_List
my_list.c
├── my_Link
└── my_List implementation
Advantages:
- Insulated from clients.
- Does not need to affect the external physical interface.
- Keeps implementation details local.
Disadvantages:
- Cannot be directly tested independently.
- Cannot be reused.
- Cannot be substantively used by inline functions defined in the header.
Use this form when:
- The class does not require direct testing.
- Inline functions do not need its definition.
- Insulation is desirable.
8.4.5 Nested Class
The auxiliary class can be nested inside the primary class.
class my_List
{
class my_Link
{
int d_data;
my_Link* d_next_p;
};
my_Link* d_head_p;
};
Private Nested Class
Advantages:
- Does not add a separate global class name.
- Encapsulated from clients.
- Can be used by inline member functions.
Disadvantages:
- Not directly testable.
- Not insulated from clients.
- Not independently reusable.
- Harder to move later to another component or implementation file.
Public Nested Class
A public nested class can be accessed directly by clients but remains scoped within the primary class.
The chapter argues that if direct access is needed, a prefixed file-scope class may often be simpler than a public nested class.
8.4.6 Choosing an Auxiliary-Class Strategy
The chapter recommends answering three questions.
Question 1
Does the auxiliary class require direct testing?
Yes
└── Avoid Slave / Local / Private Nested
No
└── These remain possible
If it is complex enough to require direct testing, prefer:
- File-scope header class.
- Separate component.
Question 2
Do inline functions require access to the auxiliary class?
Yes
└── Definition must be visible in header
No
└── Local implementation-file class may be possible
If inline access is required, a local class in the implementation file is unsuitable.
Question 3
Will the component be widely used?
Widely used components benefit more from hiding unnecessary implementation details.
If the component is used only by a small subsystem, the simple file-scope-header implementation may be sufficient.
Decision Summary
Need Direct Testing?
├── Yes
│ ├── Need Inline Access?
│ │ ├── Yes → Separate Component
│ │ └── No → File Scope or Separate Component
│
└── No
├── Need Inline Access?
│ ├── No → Local Class
│ └── Yes → File Scope / Slave / Private Nested
The exact choice depends on the combination of:
- Testability.
- Global-name-space impact.
- Inline accessibility.
- Physical coupling.
- Insulation.
- Reusability.
8.5 Key Concepts
Abstraction
↓
Abstract Specification of
Related Objects + Behaviors
Component
↓
Concrete Specification
of an Abstraction
Interface Design
├── Private → Sufficient
├── Public → Complete
├── Class → Primitive
└── Component → Minimal Yet Usable
Primitive Operation
↓
Efficient Implementation Requires
Private Access
Logical Coupling
↓
External Types in Interface
↓
Minimize Where Possible
Encapsulation
↓
Implementation Can Change
Without Interface Change
Performance Trade-Off
├── Full Encapsulation
└── Partial Encapsulation
Return Techniques
├── Return by Value
└── Return by Argument
Auxiliary Implementation Classes
├── File Scope in Header
├── Separate Component
├── Slave Class
├── Local Class in .c
└── Nested Class
Selection Criteria
├── Direct Testing Needed?
├── Inline Access Needed?
└── Widely Used Component?