본문으로 건너뛰기

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:

  1. It exists only to implement the component.
  2. 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?