본문으로 건너뛰기

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?