Lecture Notes on Object-Oriented Programming
The Quality of Classes and OO Design
table of contents
The Quality of Classes and OO Design
Class Quality
Object oriented analysis, design, and implementation are an iterative process. It is usually impossible to fully and correctly design the classes of an OO system at the outset of a project.
Booch proposes five metrics to measure the quality of classes:
- Coupling
- Cohesion
- Sufficiency
- Completeness
- Primitiveness
Coupling
How closely do classes rely on each other?
Inheritance makes for strong coupling (generally a bad thing) but takes advantage of the re-use of an abstraction (generally a good thing).
Cohesion
How tied together are the state and behavior of a class? Does the abstraction model a cohesive, related, integrated thing in the problem space?
Sufficiency
Does the class capture enough of the details of the thing being modeled to be useful?
Completeness
Does the class capture all of the useful behavior of the thing being modeled to be re-usable? What about future users (reusers) of the class? How much more time does it take to provide completeness? Is it worth it?
Primitive
Do all the behaviors of the class satisfy the condition that they can only be implemented by accessing the state of the class directly? If a method can be written strictly in terms of other methods in the class, it isn't primitive.
Primitive classes are smaller, easier to understand, with less coupling between methods, and are more likely to be reused. If you try to do too much in the class, then you're likely to make it difficult to use for other designers.
Sometimes issues of efficiency or interface ease-of-use will suggest you violate the general recommendation of making a class primitive. For example, you might provide a general method with many arguments to cover all possible uses, and a simplified method without arguments for the common case.
The truism "Make the common case fast" holds, but in this case ÒfastÓ means "easy". Easy may violate primitive.
Relationships Between Classes
The Law of Demeter provides a useful guideline: The methods of a class should not depend on the structure of other classes.
A class should communicate with as few other classes as possible.
Applying this law results in loosely coupled classes.
Class inheritance hierarchy shape
The shape of the class hierarchy may be:
- Wide and shallow – Classes are loosely coupled; some commonality may be redundantly implemented.
- Narrow and deep – Classes are smaller, but are more tightly coupled to their parent classes.
There is no single "right" shape. This is very problem dependent. Don't expect to see one shape or another in your projects; you may be disappointed.
Pitfalls in class designThese are adapted from Bruce Webster's book.
Confusing class relationships
Is-a, Has-a, Association are all different and must be applied properly confusing interface inheritance with implementation inheritance
C++ gives you lots of control; you need to use it properly.
As a base class, do you want your derived classes to inherit:
- An interface and implementation (normal class function)
- An interface and default implementation (virtual class function)
- An interface only (pure virtual function)
Mis-using inheritance
Not everyone thinks inheritance is a necessary part of OO (small minority, but OLE 2.0 is an example). Potential problem of coupling; lets a subclass get into the guts of a base class.
Some specific things to watch out for:
- Using multiple inheritance to turn the is-a hierarchy upside down by creating a general class out of two more specific classes. (e.g. Clock, Calendar, ClockCalendar)
- Using multiple inheritance without restrictions, or a clear understanding of OO ideas
- Functionality of base classes - too much or too little
- Base classes which do too little may:
- have no public or protected interface (i.e. they impose no protocol)
- have no implementation (i.e. they are only a protocol)
- have subclasses which do too much (duplicated code in subclasses)
- Base classes which do too much:
- Offer an implementation that is mostly overridden by derived classes
Base class invariants
Base classes have assumptions built into them about their state, pre and post conditions, relationships between instance variables, timing, invocation of methods, etc.
Since a derived class "is-a" base class, the same invariants must hold true. But these invariants aren't necessarily visible from the header, or even sometimes from the implementation file, so it is easy for the designer of the derived class to mess up the invariants she inherits. Documentation and review are the keys.
Class bloat
A class becomes unwieldy at some point: too many instance variables, too much functionality. This can happen over time, and may indicate the need to split the class via inheritance or aggregation, or simply other classes.
"Swiss Army Knife" classes are a special case of bloat. Too much functionality unrelated to the abstraction can creep into the class. When it does, people will rely on it, then you're stuck.
Finding classes/objects
All classes show up as nouns in a functional description of the project. This is too simple.
Some classes are obvious, have corresponding physical entities in the problem domain. Others are less obvious and are "discovered" or invented during analysis/design. Gamma says these sort are key to making designs flexible. Many of the objects in the pattern catalog are of this type. This is why it's important to know the catalog.
Quality OO Design
Type versus class
A type is an interface. An interface is a collection of methods which an object responds to. Different kinds of objects may share the same type. An object can have many types.
An object's implementation is defined by its class. The class tells you about the state an object maintains. It also tells you how an object implements the methods in its interface.
In C++ the class and type of an object are the same, so you probably aren't used to separating these ideas. In C++ the class of an object also specifies its type.
Class inheritance is a means of sharing implementation (both state and behavior) between classes.
Interface inheritance (subtyping) only specifies when one object can be used in place of another.
The distinction is important because it is the type of an object that is important to flexible design. Interface is everything in good design; type is interface. Designing with an interface in mind (rather than an implementation) helps because:
- Clients remain unaware (hence decoupled) of the specific kinds of objects they use.
- Clients remain unaware of the classes of the objects they use. They just know the interfaces.
These points together describe what Gamma calls the first "principle of OO design": Program to an interface, not an implementation.
Variables should not be concrete classes. Rather, they should be to abstract classes that just specify an interface. Java gives us an even better means of doing this. There are creational patterns for instantiating objects of concrete classes (since you have to get work done somewhere) while remaining unaware of their class.
In C++ you can do pure interface inheritance via public inheritance of pure abstract classes. You can do pure implementation inheritance via private inheritance.
This distinction is important because the design patterns in Gamma often make it. Another interesting aspect of this issue is that it helps explain the features of other OO languages (e.g. the "interface"s of Java and "protocols" of Objective C).
Inheritance and composition
Class inheritance is great for implementation re-use. But it goes against the general goal of decoupled classes. It's also a static, compile-time relationship. This makes it easier to understand and program to, but less flexible.
Composition (aka association) is another means of achieving re-use. Put an object inside another object and the first object can re-use the code behind the composed object. The difference is that the composed object can be determined at run-time. The only stipulation is that the composed object must be of the proper type (not class). If you view composition as an alternative to inheritance then you can in effect change the inheritance or class of an object at run-time.
In C++ we can make a useful distinction between aggregation and composition (aka association). Aggregation is done by value (life times the same between whole/part). Composition is done by reference (reference or pointer, lifetimes de-coupled) so that the object being referred to can change at run-time.
Gamma's second principle of good OO design is to: Favor object composition over class inheritance.
Systems that follow this rule have fewer classes and more objects. Their power is in the ways that the objects can interact with each other. It would be important to have good dynamic models.
Delegation
Delegation is an important design pattern itself. It's used a lot in Objective C and NEXTSTEP programming as an alternative to mandatory inheritance from a complex framework and multiple inheritance.
If B is a subclass of A then sending B a message for something in A's interface means that B can re-use the implemtation found in A. Delegation uses composition to put re-use the implementation from A. B's interface can be extended to have some of the same functionality of A. But when a B object is sent a message corresponding to a method in A's type, it forwards the message to an A object.
The aggregation design we saw earlier for implementing a Set in terms of a List is an example of the use of delegation.
Example from NEXTSTEP AppKit
Application object (one per application) knows about connection to window server, the program's icons, starting and stopping the app, power down, etc. Suppose you want to make an app that won't blow away unsaved work when the app is going to quit running due to logout. Subclass Application and add this functionality? Lots of work for minimal additional functionality. Think of all the X and X' classes you'd have in your app. Instead, Application has hooks for delegation built into it like this (translated to C++ like syntax):
Application::userLogout(){ if( delegate != NULL ) delegate->willShutdown();}
Advantages: someday Windows may not be rectangular (or 2 dimensional, or ...) Design for change now by settling only on an interface, not an implementation.
Disadvantage: Your Window class may be harder to read and understand. More messages means more time.
Static vs. dynamic structure
Some things are apparent from the static structure and models of a system (e.g. inheritance, aggregation).
An executing OO program is a network of messaging objects. The objects come and go. The message patterns shift. The relationships vary over time. Little of this is apparent from the static models of the system. The dynamic models(object diagrams, etc) are crucial for understanding this behavior. If the dynamic behavior relationships have been created in the form of well known patterns then they will be that much easier to document and understand.
For the distinction between the static and dynamic elements of a system, consider an aquarium. What would you know about how an aquarium looks, behaves, captivates, rots, etc, by examining a description of the separate parts (tank, filter, rocks, fish, food, etc)? Now imagine how rich and complicated the interactions of the various components of an aquarium are. Think of what the pieces give rise to.
Design for change
Here's how patterns address change that can otherwise force re-design:
- Creating an object by specifying a class explicitly.
- Specifying one particular operation. Example: NEXTSTEPAppKit has a responder chain which is used to determine who will handle a particular GUIinput event.
- Limit the spread of platform dependencies. Helps in porting and maintenance to isolate particulars of the GUI or OS.
- Knowing how an object is represented, stored, located or implemented. Some of this is naturally avoided by encapsulation. But you can take this further to de-couple classes, as in a Distributed Objects NXProxy. (Graph, Node, List examp.
- Algorithmic dependencies. Encapsulate algorithms that are likely to change so that change can't ripple. Could even be within the same class.
- Avoid tight coupling. Classes that are tightly coupled can't be separated or changed independently. Also less likely to re-use.
- Limiting re-use to inheritance. Composition and delegation provide alternatives which can be simpler and lead to more flexible systems.
- Classes you can't alter. Patterns give you ways of using classes you can't touch which keeps your design flexible. NEXTSTEP AppKit and use of delegation is good example of this.