This is basically reading notes on Large-Scale C++ Software Design, by John Lakos.

Physical Design Rules

  • (Major) Logical entities declared within a component should not be defined outside that component.
  • (Major) The .c file of every component should include its own .h file as the first substantive line of code.
  • (Guideline) Clients should include header files providing required type definitions directly; Except for non-private inheritance, avoid relying on one header file to include another.
  • (Major) Avoid definitions with external linkage in the .c file of a component that are not declared explicitly in the corresponding .h file.

Principles

The DependOn relation for component is transitive.

A component defining a function will usually have a physical dependency on any component defining a type used by that function.

A component defining a class that IsA or HasA user-defined type always has a compile-time dependency on the component defining that type.

The include graph generated by C++ preprocessor #include directives should alone be sufficient to infer all physical dependencies within a system provided the system compiles.

Friendship within a component is an implementation detail of that component.

Granting (local) friendship to classes defined within the same component does not violate encapsulation.

Defining an iterator class along with a container class in the same component enables user extensibility, improves maintainability, and enhances reusability while preserving encapsulation.

Granting (long-distance) friendship to a logical entity defined in a seperate physical part of the system violates the encapsulation of the class granting that friendship.

Every directed acyclic graph can be assgined unique level numbers; a graph with cycles cannot.

In most real-world situations, large designs must be levelizable if they are to be tested effectively.

Hierarchical testing requires a seperate test driver for every component.

Testing only the functionality directly implemented within a component enables the complexity of the test to be proportional to the complexity of the component.

Cyclic physical dependencies among components inhibit understanding, testing, and reuse.

Acyclic physical dependencies can dramatically reduce link-time costs associated with developing, maintaining, and testing large systems.

Minimizing CCD for a given set of components is a design goal.

Allowing two components to “know” about each other via #include directives implies cyclic physical dependency.

If peer components are cyclicly dependent, it may be possible to escalate the interdependent functionality from each of these components to static members in a potentially new higher-level component that depends on each of the original component.

Cyclic physical dependencies is large, low-level subsystems have the greatest capacity to increase the overall cost of maintaining a system.

If peer components are cyclicly dependent, it may be possible to demote the interdependent functionality from each of the components to a potentially new lower-level (shared) component upon which each of the original components depends.

Demoting common code enables independent reuse.

Escalating policy and demoting the infrastructure can be combined to enhance independent use.

Factoring a concrete class into two classes containing higher and lower levels of functionality can facilitate levelization.

Factoring an abstract base class into two classes - one defining a pure interface, the other defining its partial implementation - can facilitate levelization.

Factoring a system into smaller components makes it both more flexible and also more complex, since there are now more physical pieces to work with.

Components that use objects in name only can be thoroughly tested independently of the named object.

If a contained object holds a pointer to its container and implements functionality that depends substantively on that container, then we can eliminate mutual dependency by (1) making the pointer in the contained class opaque, (2) providing access to the container pointer in the public interface of the contained class, and (3) escalating the affected methods of the contained class to static member of the container class.

Dumb data can be used to break in-name-only dependencies, facilitate testability, and reduce implementation size. However, opaque pointers can preserve both type safety and encapsulation; dumb data, in general cannot.

The additional coupling associated with some forms of reuse may outweigh the advantage gained from that reuse.

Supplying a small amout of redundant data can enable the use of an object in name only, thus eliminating the cost of linking to the definition of that object’s type.

Packaging subsystems so as to minimize the cost of linking to other subsystems is a design goal.

The indiscriminate use of callbacks can lead to designs that are difficult to understand, debug, and maintain.

The need for callbacks can be a symptom of a poor overall architecture. (But in some context, we really need it)

Establishing hierarchical ownership of lower-level objects makes a system easier to understand and more maintainable.

Factoring out and demoting independently testable implementation details can reduce the cost of maintaining a collection of cyclicly dependent classes.

Granting friendship does not create dependencies but can induce physical coupling in order to preserve encapsulation.

What is and what is not an implementation detail depends on the level of abstraction within the physical hierarchy.

Escalating the level at which encapsulation occurs can remove the need to grant private access to cooperating components within a subsystem.

Private header files are not a substitute for proper encapsulation because they inhibit side-by-side reuse.

Granting higher-level clients the authority to modify the interface of a lower-level shared resource implicitly couples all clients.

A protocol class is a nearly perfect insulator.

A protocol class can be used to eliminate both compile- and link-time dependencies.

Holding only a single opaque pointer to a structure containing all of a class’s private members enables a concrete class to insulate its implementation from its clients.

The physical structures of all fully insulating classes appear outwardly to be identical.

All fully insulating implementations can be modified without affecting any header file.

Guidelines

A component x should include y.h only if x makes direct substantive use of a class or free operator function defined in y.

Avoid granting (long-distance) friendship to a logical entity defined in another component.

Some Definitions

We can define implementation dependency for functions loosely by saying that a function depends on a component if that component is needed in order to compile and link the body of that function.

So we have the definition: A component y DependsOn a component x if x is needed in order to compile or link y.

A component y exhibits a compile-time dependency on component x if x.h is need in order to compile y.c.

A component y exhibits a link-time dependency on component x if the object y.o contains undefined symbols for which x.o may be called upon either directly or indirectly to help resolve at link time.

Level number definition:

  • Level 0: A component that is external to our package.
  • Level 1: A component that has no local physical dependencies.
  • Level N: A component that depends physically on a component at level N-1, but not higher.

A level-1 component that depends only on compiler-supplied libraries is called a leaf component. Leaf components are always testable in isolation.

Hierarchical testing refers to the practice of testing individual components at each level of the physical hierarchy.

Incremental testing refers to the practice of deliberately testing only the functionality actually implemented within the component under test.

White-box testing refers to the practice of verifying the expected behaviour of a component by exploiting knowledge of its underlying implementation.

Black-box testing refers to the practice of verifying the expected behaviour of a component based solely on its specification.

Cumulative component dependency(CCD) is the sum over all components C(i) in a subsystem of the number of components needed in order to test each C(i) incrementally.

Average component dependency(ACD) is defined as the ratio of the CCD of a subsystem to the number of components N in the subsystem:

ACD(subsystem) = CCD(subsystem) / N

Normalized cumulative component dependency(NCCD) is defined as the ratio of the CCD of a subsystem containing N components to the CCD of a tree-like system of the same size:

NCCD(subsystem) = CCD(subsystem) / CCD_balanced_binary(N_subsystem)

A subsystem is levelizable if it compiles and the graph implied by the include directives of the individual components (including the .c files) is acyclic.

A component y dominates a component x if y is at a higher level than x and y depends on x physically.

A function f uses a type T in size if compiling the body of f requires having first seen the definition of T.

A function f uses a type T in name only if compiling f and any of the components on which f may depend does not require having first seen the definition of T.

A pointer is said to be opaque if the definition of the type to which it points to is not included in the current translation unit.

Dumb data is any kind of information that an object holds but does not know how to interpret. (Like opaque pointer)

In hierarchical systems, encapsulating a type (defined at file scope within a header file) means hiding its use, not hiding the type itself.

Insulation is the process of avoiding or removing unnecessary compile-time coupling.

A contained implementation detail (type, data, or function) that can be altered, added or removed without forcing clients to recompile is said to be insulated.

An abstract class is a protocol class if:

  1. it neither contains nor inherits from classes that contain member data, non-virtual functions, or private (or protected) members of any kind,
  2. it has a non-inline virtual destructor defined with an empty implementation, and
  3. all member functions other than destructor including inherited functions, are declared pure virtual and left undefined.

A concrete class is fully insulating if it

  1. contains exactly one data member that is an outwardly opaque pointer to a non-const struct (defined in the .c file) specifying the implementation of that class,
  2. does not contain any other private or protected members of any kind,
  3. does not inherit from any class, and
  4. does not declare any virtual or inline functions.

Random Notes

The low-level component test should be updated to expose the errant behaviour before the defect is repaired. (It definitely facilitates the repair)

NCCD is not a measure of the relative quality of a system. NCCD is simply a tool for characterizing the degree of coupling within a subsystem.

The precise numerical value of the CCD (or the NCCD) for a given system is not important. What is important is actively designing systems to keep the CCD for each subsystem from becoming larger than necessary.

If two or more objects share mutual ownership, that functionality should be escalated to a manager class.

In general, the goal of insulation is to shield clients from the compile-time dependency associated with knowing unnecessary, encapsulated implementation details; it is NOT meant to shield clients from the programmatically accessible interface or to compromise type safety.

Allowing inheritance or virtual functions would affect the object layout by introducing additional data and/or additional virtual-function-table pointers.