Lecture Notes on Object-Oriented Programming
The Quality of Classes and OO Design
table of contents
When we use inheritance, we have classes at the top of the hierarchy that are known variously as:
And classes at the bottom of the hierarchy (or those which inherit from some other class) known variously as:
The classes toward the top of an inheritance hierarchy tend to be abstract classes that won't have any objects instantiated from them. The bottom most classes of the hierarchy are the concrete classes from which we actually make objects that do work for us.
- Avoid redundant code by sharing it (coupling?) between classes
- Inheritance relationships let us avoid the redundancy of declaring the same instance variables and behavior in a group of related classes. For instance, Dogs and Cats don't both need a name variable; they can inherit it from Pet.
- Enforcing standards at a design level (interfaces)
- Programming-by-drag-and-drop (components)
- Avoid redundant code by sharing it between projects (reuse)
- Shared code allows for rapid prototyping via reuse.
- Shared code produces greater reliability via greater use (and hence discovery of errors).
Two views: expansion (additional state/behavior) versus contraction (more specialized)
When objects of class X can be substituted for objects of class Y with no observable effects, we say class Y is substitutable for class X.
Why do we care about this?
- Code now, for classes not yet written.
- Shape, Rectangle, Circle
- Pointer to Shape
- Container of Shape objects
- Loop to send drawSelf() message
- Later: add a new shape subclass
Why not define substitutability for type, rather than class?
- If type and class are the same (like in C++) then there is no difference.
- Separating the two has advantages, as we can program to the type and never even know the class. (remote objects example)
Statically and strongly typed languages tend to define substitutability in terms of class.
Dynamically and weakly typed languages tend to define substitutability in terms of type (or behavior).
Example in Java - Pet classes
Here's our Pet class in Java.
The Pet class we had from before was fine, as long as we didn't care much about the specifics of the pets we were making. This might be appropriate if the domain we're modeling doesn't require very pet-specific behavior to be captured.
Suppose now you care about behavior like "come" which is definitely pet species specific. We can still use the baseline functionality of our Pet class for dogs, since after all, pets are dogs. But our Dog class needs to do something that cats, anoles, and fish don't do, or do it in a special, dog-specific manner.
And now the code for our simple Dog class:
Does Java separate interface and implementation as cleanly as C++? Write code to implement the sitUp() method of the Dog class. Assume you have these constants somewhere:
Eventually you write a complicated algorithm for a dog to situp based on whether it is already standing, where it is located, how attentive it is, whether it is in a good mood, etc (in other words, capturing the behavior of a real dog). When you teach your old Dog class this new trick, nothing changes in the interface. Not all pets implement sitUp() and rollOver() - those are dog specific tricks.
Different pets, like cats, might implement come() quite differently.
These differences and the common state and behavior, are what determines how we can use hierarchy.
Example in C++ - abstract class for graphics
Remember the @DisplayItem@ class? We said initially that it was an "abstract" class, designed to subclassed. It alone is too generic to be of any use as living breathing objects (it'll have other uses as we'll see later).
In C++ we can say we mean a class to be abstract, and subclassed, by marking methods "virtual". This is a confusing but essential topic when learning C++, and one that needs treatment all by itself. Fow now, if you see "virtual" on a method, you should think of that method as one in which subclasses have their own implementation.
Reminders of what the C++ means...
- The methods draw, erase, ... are obviously going to need to be subclass defined, since each subclass would draw itself differently, so they are declared virtual and are meant to be overriden in a subclass definition.
- The selector methods are not declared virtual because they can be defined here for all subclasses. (Other OO languages (Java, Smalltalk, Objective C) don't need a keyword to say this same thing.)
Forms of Inheritance
1. For specialization
- The classic or purest form of inheritance: specialization versus generalization.
- The subclass is a special case of the superclass.
- Pushing the common stuff (state + behavior) up the hierarchy.
- The acid test for the purest form of inheritance (i.e. subclassing) is the "is a" question.
- Subclasses are subtypes as well. Everything that is true of Super is true of Sub.
2. For specification
- When you want to constrain how subclasses are designed. You insist that subclasses meet a certain interface. This lets you count on being able to message objects of these subclasses in a uniform manner.
- You want to enforce an interface of some sort, but you aren't providing an implementation for that functionality; that's up to the subclass.
- Subclasses are subtypes. This is a special case of specialization.
3. For extension
- Subclasses add additional, new functionality to the base class.
- Subclasses are subtypes, since they leave the parent class functionality/interface in place.
4. For construction
- Done when the subclass is implemented in terms of the super class. Aggregation is often a more desirable alternative.
- The super class has desired behavior we want, but we need to add to it.
- This form of inheritance is in direct comparison to aggregation (object relationship).
- Subclasses are not subtypes. No "isa" relationship.
5. For generalization
- Just the opposite of the form of inheritance as specialization (the "pure" form).
- Subclasses generalize functionality already found in the base class by overriding methods inherited from the parent.
- Example: monochrom windows versus colored windows.
- If you control the class hierarchy, it can be better to refactor the classes and modify the base classes to support the newly desired general functionality. Often times you don't have this control, so you are forced to use inheritance this way (upside down).
6. For limitation
- Subclasses decrease in some way the behavior or interface inherited from the base class.
- Methods are eliminated in the interface.
- Example: Professor, Programmer, Employee
Professor -> Programmer –> Employee Programmer isa Employee, adds the additional functionality of produceRealCode() as a specialization of Employee (something that Secretary, for instance, wouldn't have). Professor isa Employee, but Professor "turns off" the @produceRealCode()@ method found in Programmer.
- To be avoided, as subclasses are not subtypes, as the parents behavior is intentionally limited in the child. Most often done when the base classes are not controlled locally.
7. For variance
- When you have two unrelated classes that happen to have similar functionality, or a similar interface, then you can make one (arbitrarily) the superclass of the other to share the common implementation. Sometimes this can be the hint you need to recognize a new abstract superclass.
- Java solves some of these problems via separating class and interface. Interface may be common to unrelated classes (i.e. it is orthogonal to the class hierarchy).
- Subclasses are not subtypes.
8. For combination
- Similar to construction, in that a new class is built from the elements of existing classes. Multiple inheritance allows this in some languages. May preserve typing.
- The first three forms cover almost all the cases we want to use inheritance for. The others are special cases, or situations in which some other design is usually better.
This is an alternative design to the List/Set problem above, which was solved with composition.
Set shares many of the same behaviors as List. Can we say that Set isa List?
- Set redefines the behavior of the add() method. This looks like inheritance for specialization. Add still works, just in a way that makes sense for Set.
- Set adds new behavior in the form of the size() method. This looks like inheritance for extension.
Note that the construction of the Set objects is a bit different. The initializer list daisy chains the constructors together.
Note the use of the List methods without any class scope operation. Since Set inherits from List, it may use those methods without any specific naming.
Can we say that methods addToFront(), remove() and includes() are part of class Set? Is this a problem? (yes and yes!) (This design flaw is fatal in the case of Set inheriting from List.)
Disadvantages (in general)
1. Increased class/abstraction coupling
- What is more coupled than super/sub classes? Coupling is bad. So inheritance is bad?
- The more general a body of code (class hierarchy) is, the less likely it is the optimal perfomance solution for any given specific problem.
- Experts versus novices, and class maturity.
3. Program size
- Using large class hierarchies makes your project bigger.
- Dealing with the run-time dispatch of messages to objects related in an inheritance hierarchy has memory overhead.
4. Understanding a large system (the yo-yo problem, class documentation)
- Inheritance means that it is not sufficient to study the header file (declaration) of a class to completely understand its behavior. You often must look up the hierarchy and consider the inherited behavior and state from its superclasses. On a practical note, this can be a pain for documentation, since you may not find the behavior you seek in the document for the class itself. Class browsers address this problem.
Aggregation vs Inheritance
Composition means less coupling between classes. This is good. Composition is simpler. This is good.
Composition lets you specify exactly an interface for Set, independent of the interface of List. This is good.
Inheritance gives Set an interface it may or may not wish to comply to completely.
To understand Set in the inheritance example, one must look at the List class header file.
The inheritance approach is smaller. Less "wrapper" code has to be written.
With inheritance, it is possible for users of Set to intentionally use the overridden, inherited methods in lieu of those redefined in Set. So for example a client could call addToFront directly, bypassing the check done in the Set add() method.
Coupling to the clients is strong with the List inheritance, since clients may come to rely on the fact that "Set isa List". This means it would be harder to change the implementation of the data structure (a list) someday if necessary. With composition the clients are unaware that a List is used, and so it could be changed easily.
We forgo the potential advantage of using polymorphic behavior with our List/Set objects at some future date if we use composition.
Having to understand the pre-conditions and proper use of List in order to use Set is a drawback to inheritance. With composition everything that needs to be known about Set is contained in Set.
Inheritance is a little bit faster since the overhead of a message send to the aggregated List object is avoided.
Metric: use the question of substitutability to decide which technique is better.
Other authors come down quite forcefully on the side of favoring composition over inheritance.
UML indicates a superclass/subclass relationship as shown below.