Applying the principles of structured design to object-oriented programming.
For many software engineers, the phrase "coupling between modules" has a familiar ring to it. It should; a good percentage of software engineers have taken courses in structured methods, including structured design[ 1 ]. In such courses, formal definitions are given to the various types and strengths of couplings between modules. These courses also emphasise that a low degree of coupling is preferable, and a high degree of coupling is undesirable. The provided rationale normally appeals to notions of localising decisions reducing dependencies, and making modules more comprehensible in isolation.
As we move from the module to the object realm, we need to introduce notions of coupling between objects or classes. Design decisions should seek to achieve goals similar to the ones just mentioned for modules.
If these goals are part of the fabric that makes up how we think about software systems, we may notice something profound. Competing goals often are at work when we factor a type system over an inheritance scheme. On one hand is the desire to create abstract classes and to optimise for reuse. On the other hand, we are conscious that in doing so we are breaking up a single type manager over several separate, yet tightly interconnected, configuration items. This conflict has been referred to by Grady Booch[ 2 ] as a "healthy tension" between goals, in which case a designer should make tradeoffs that are good for the maintainability of the system under construction. This article focuses on the class design aspect of C++ [ 3 ].
If we look at C++ classes and ignore inheritance for the moment, we
see a mechanism supporting good, black box encapsulation and possessing
the benefits of data abstraction and information hiding. We can achieve
data abstraction because using classes effectively extends the
available object types in the language. We achieve information hiding
by hiding the representation of an objects internal state. We do so by
limiting visibility to a class's instance variables by declaring them
non-public (private or protected). A class and its clients are
connected through a Use relationship. Objects related in this manner
are referred to as being interfaced coupled (Fig 1). Interface coupling
is the lowest form of coupling between objects.
Figure 1 - Interface Coupled Objects
Interface coupling may vary in strength, but this notion is not new. To classify the strength of interface coupling between any pair of objects, you need to determine the nature of the specific module-to-module interconnections. The classification criteria can be adopted directly from structured design, using the same coupling classification scheme.
C++ allows the protective wall around classes to be penetrated under
several circumstances. First, friend functions or classes can reference
a class's internals, but they can only do this with classes that
declare them as friends. Second, the parts of a class's internals
declared protected are visible to a class's subclasses. The subclasses
have visibility to protected components and can manipulate them
directly. Objects related in these two manners are internally coupled
Figure 2 - Internally Coupled Objects
Friend functions and classes are special entities singled out for relaxation of the encapsulation around a class's state information. Permission to enter is granted on an entity-by-entity basis. Authors of classes should select the classes' friends carefully; normally classes should have few friends. Before assigning friends, a concerted effort should be made to let a class present the required information through its callable interface. This keeps the class in control of its own destiny and isolates concerns about internal representations.
It is common for a class to have a number of subclasses. A subclass, by its nature, has a strong relationship with its superclass. This relationship is an 'Is_a' or derivation relationship. A true subclass inherits everything from its parent and has a number of augmentations. A proper subclass only adds, never subtracts, attributes. This honours the true meaning of a full inheritance (Is_a) relationship.
In such a restricted scheme, it will be typical to find two kinds of functions in a subclass. First, over-riding functions may exist. These functions may call their corresponding superclass function and then perform local processing concerning the subclasses local attributes. Second there may be local functions, which are introduced in the subclass and only manipulate the unique attributes of the subclass. This type of subclassing is natural, has low complexity (due to the interface-coupled nature of the interaction between the subclass and its superclass), and honours encapsulation at all levels. It is a safe kind of inheritance. It yields very maintainable class systems.
Another kind of inheritance that results from the activities of the ambitious subclasser is partial inheritance. A programmer who is optimising for code reuse at all costs will let a subclass be placed into the system where they do not strictly honour the Is_a relationship. Often, the parent class's internals are made protected so that the child class can gain control over them, letting the child class's implementation modify its parent's essential characteristics. This kind of inheritance is not generally safe and creates class systems that are more complex, due to the internal coupling that normally accompanies it. In the worst of cases, some of the inherited functions may no longer be applicable in the subclass. Internal coupling between a subclass and its superclass creates strong dependencies that are very difficult to track and maintain.
The One and the Many.
Arguments have been presented to the effect that internal coupling between subclasses and their parent classes is defensible because the resulting composite class should be thought of as one object, not a co-operating group of components.
To the contrary, an increasing number of people are looking at class systems from the point of view that subclasses are actually clients of their parent classes, and that in the general case, they should be no more privileged to manipulate their parent's internals than any other client. Some have proposed sets of rules to govern the construction of classes, such as the "Law of Dementer''[ 4 ] or guidelines centred around a "Separation of Concerns."[ 5 ] These movements are attractive because they are consistent with classical concepts of modularity as it relates to managed abstractions, such as those proposed by David Parnas.[ 6 ]
Class Depth and Complexity.
From a class's client's perspective, the more ancestor classes a given class has, the more difficult it is to get a good idea of exactly what an instance of the class looks like. At some levels, attributes are added; at others, they are overridden. If the number of inheritance relationships is too large, it is difficult for people to read the declarations and perform enough mental bookkeeping to form a composite picture of the class's interface. This means that a class's ease-of-use characteristics are degraded proportionally to its depth of occurrence in the class system.
For obvious reasons, ease-of-maintainability characteristics follow a similar pattern. A given class follows a behaviour scenario that is a composite of everything local to its implementation and everything it inherits. To inherit is good. We are reusing existing software; we are making it perform double or triple (or higher) duty. But there comes a point where principles of localisation have been mostly lost for the class's behaviour as a whole.
Minimising Levels of Inheritance.
What should we do? It's good to strike a balance between both generalisation and specialisation. Some attributes of a class can be manipulated without subclassing. Where possible, trade-offs should be made toward the side of generalisation. For example, a menu class can posses functions to set its items. It would be a poor choice to subclass from a general menu class and use the subclass constructors to load the specific menu items for each kind of menu in the system. To do so would multiply the number of configuration items in the system and would pull the ratio of classes to their instances closer toward 1:1. We want to reduce the number of configuration items in a system and increase the utility of the classes we find necessary to create.
Classes should be essential in nature and should correspond to actual distinctions between object types in the application's solution space. As a parallel example, let's look at our structured programming experience. How do we create modules? Do we seek to factor out coincidentally common code fragments and give them contrived module names, or do we identify and factor out essential functions? We have learned that it is the latter criteria that makes for the most well-structured systems, while the former leads to non intuitive ones. The same is true when identifying classes in a class system. If we create a contrived abstract classes for the purpose of maximising code reuse, we are doing ourselves a bad turn in ease-of-use, intuitiveness, and complexity. You should seek to create only essential and intuitive classes, even at the expense of some code replication.
We have discussed notions of interface and internal coupling between objects. Interface coupling is the lowest strength of coupling. Internal coupling is the highest. As with module coupling, lower strengths of coupling between objects is desirable. Class systems involving inheritance invite coupling between subclasses and their superclasses. Inheritance relationships have better complexity and maintainability characteristics when subclasses are interface coupled to their superclasses. When subclasses are internally coupled to their superclasses, inheritance relationships have degraded complexity and maintainability characteristics.