Object-Oriented Programming

CSCI-UA.0470-001

NYU, Undergraduate Division, Computer Science Course - Fall 2014

OOP Class Notes 10/06/14

When should one use class inheritance and when not? What are OO design alternatives to class inheritance?

Advantages/Disadvantages of Class Inheritance

Advantages:
  • New implementation is easy, since most of it is inherited
  • Easy to modify or extend the implementation being reused
Disadvantages:
  • Breaks encapsulation, since it exposes a subclass to implementation details of its superclass
  • "White-box" reuse, since internal details of superclasses are often visible to subclasses
  • Subclasses may have to be changed if the implementation of the superclass changes
  • Implementations inherited from superclasses can not be changed at run-time

The following example from Joshua Bloch's book "Effective Java" illustrates how class inheritance can break encapsulation.

Example: Breaking encapsulation using class inheritance

Suppose we have a Java program that uses the HashSet data structure from the java.util package and we want to monitor the number of attempted element insertions into the data structure. One potential solution is to extend the HashSet class as follows:

public class InstrumentedHashSet extends HashSet {
  // The number of attempted element insertions
  private int addCount = 0;

  public InstrumentedHashSet() { super(); }

  public boolean add(Object o) {
    addCount++;
    return super.add(o);
  }

  public boolean addAll(Collection c) {
    addCount += c.size();
    return super.addAll(c);
  }

  public int getAddCount() {
    return addCount;
  }
}

The following test program reveals the problem with this solution:

public static void main(String[] args) {
  InstrumentedHashSet s = new InstrumentedHashSet();
  s.addAll(Arrays.asList(new String[] {"apple", "orange", "banana"}));
  System.out.println(s.getAddCount());
}

The test program outputs "6" instead of the expected result "3". The problem is that the addAll method of the class HashSet is implemented by calling the add method for all elements of the collection c. Since add is overriden by the subclass every inserted element is counted twice. We could solve this problem by removing the increment of addCount in the overriding addAll method. However, the new solution would remain susceptible to the introduction of bugs whenever the implementation of HashSet changes. That is, the solution with subclass inheritance breaks encapsulation.

A more viable solution is to wrap a HashSet object inside a new object that delegates all work to the HashSet and only counts the number of attempted insertions. This approach is called delegation by composition. In fact, by using composition we can make the wrapper class independent of the concrete implementation of the set data structure:


public class InstrumentedSet implements Set {
  private final Set s;
  private int addCount = 0;
  public InstrumentedSet(Set s) {this.s = s;}
  public boolean add(Object o) {
    addCount++;
    return s.add(o);
  }

  public boolean addAll(Collection c) {
    addCount += c.size();
    return s.addAll(c);
  }

  public int getAddCount() {
    return addCount;
  }

  public void clear() { 
    s.clear();
  }

  // Forwarding methods for the remaining methods of the Set interface
  ...
}
Not how we use the Set interface to achieve polymorphism without making InstrumentedSet depend on any concrete implementation of the set data structure.

Example: Implementation of classes cannot be changed at run-time

Suppose we want to model the business logic of an airline. The software consists of different components. One component handles transactions with passengers such as ticket reservation and purchase. The other component handles payroll. The ticketing component involves passengers and agents, while the payroll component involves only agents. The ticketing component needs to keep track of the name and address of each customer. The payroll component needs to maintain the same information for each agent.

A design for the two components might involve the following class hierarchy:

  • A class Person is used to store the name and address of each person.
  • There are two classes, Passenger and Agent, to model agents and passengers, respectively. Both of these classes are subclasses of Person.
What is wrong with this design?
  • The roles of a person may change over time. A person may be a passenger at one point in time and an agent at a another point in time, or even both at the same time. This is not reflected in the static class hierarchy.
One viable solution is to use subclassing and composition together:
  • Introduce a new class PersonRole that has a reference to a Person.
  • Define Passenger and Agent as subclasses of PersonRole.

Advantages/Disadvantages of Composition

Advantages:
  • Contained objects are accessed by the containing class solely through their interfaces
  • "Black-box" reuse, since internal details of contained objects are not visible
  • Good encapsulation
  • Fewer implementation dependencies
  • Each class is focused on just one task
  • The composition can be defined dynamically at run-time through objects acquiring references to other objects of the same type
Disadvantages:
  • Resulting systems tend to have more objects
  • Interfaces must be carefully defined in order to use many different objects as composition blocks
  • Potential "boiler plate" code for forwarding methods that delegate work to the contained objects.

Guideline: favor composition over class inheritance.

Of course, the available set of composable classes can be enlarged using inheritance, i.e., composition and inheritance work together.

So when should one use class inheritance?

Coad's Rules: Use class inheritance only when all of the following criteria are satisfied:

  1. A subclass expresses "is a special kind of" and not "is a role played by a" (and also not "has a").
  2. An instance of a subclass never needs to become an object of another class.
  3. A subclass extends, rather than overrides or nullifies, the responsibilities of its superclass.
  4. A subclass does not extend the capabilities of what is merely a utility class.
  5. For a class in the actual problem domain, the subclass specializes a role, transaction, or device.

Beware of Pitfalls

The relationships between objects in the problem domain do not always carry over to an OO design.

For example, suppose we want to model geometric objects such as rectangles, squares, circles, etc. The following class implements rectangles:

public class Rectangle {
  protected double width;
  protected double height;

  public Rectangle(double w, double h) {
    width = w;
    height = h;
  }

  public double getWidth() { return width; }
  public double getHeight() { return height; }

  public void setWidth(double w) { width = w; }
  public void setHeight(double h) { height = h; }

  public double area() {return (width * height); }
}
Since a square is a special kind of a rectangle, we might want to implement squares as a subclass of Rectangle:
public class Square extends Rectangle {
  public Square(double s) { super(s, s); }

  public void setHeight(double h) {
    height = h;
    width = h;
  }

  public void setWidth(double w) {
    height = w;
    width = w;
  }
}

Note that we override the setHeight and setWidth to maintain the invariant that the height and width of a square coincide.

What is the problem with this solution?
  • Each square object stores redundant information because we don't need to keep track of both height and width for squares. That is, we waste 8 bytes of memory per allocated square object. However, this might be negligible.
  • More importantly, a Square object does not behave like a Rectangle object because the methods setHeight and setWidth of class Square modify both height and width. Client code that uses the Rectangle class might assume that only one of the two attributes is modified when the corresponding mutator method is called, as the names of the methods suggest. Hence, we cannot safely use a Square object when a Rectangle object is expected, even though the subclass relationship suggests otherwise. This is a violation of Liskov's substitution principle.

Static and Dynamic Scoping

Scope rules define the visibility rules for names in a programming language. What if you have references to a variable named k in different parts of the program? Do these refer to the same variable or to different ones?

Most languages, including Algol, Ada, C, Pascal, Scheme, and Haskell, are statically scoped. A block defines a new scope. Variables can be declared in that scope, and aren't visible from the outside. However, variables outside the scope -- in enclosing scopes -- are visible unless they are overridden. In Algol, Pascal, Haskell, and Scheme (but not C or Ada) these scope rules also apply to the names of functions and procedures.

Static scoping is also sometimes called lexical scoping.

Simple Static Scoping Example

int m, n;

void hardy() {
       print("in hardy -- n = ", n);
}

void laurel(int n) {
       print("in laurel -- m = ", m);
       print("in laurel -- n = ", n);
       hardy();
}

void main() {
    m = 50;
    n = 100;
    print("in main program -- n = ", n);
    laurel(1);
    hardy();
}
The output is:
in main program -- n = 100
in laurel -- m = 50
in laurel -- n = 1
in hardy -- n = 100    /* note that here hardy is called from laurel */
in hardy -- n = 100    /* here hardy is called from the main program */

Blocks can be nested an arbitrary number of levels deep.

Dynamic Scoping

Some languages also support dynamic scoping. Dynamic scoping was used in early dialects of Lisp, and some older interpreted languages such as SNOBOL and APL. It is available as an option in Common Lisp. Using this scoping rule, we first look for a local definition of a variable. If it isn't found, we look up the calling stack for a definition. (See Lisp book.) If dynamic scoping were used, the output would be:
in main program -- n = 100
in laurel -- m = 50
in laurel -- n = 1
in hardy -- n = 1    ;; NOTE DIFFERENCE -- here hardy is called from laurel 
in hardy -- n = 100  ;; here hardy is called from the main program 

Another example

// start pseudo-code
String y = "global";

void printY() {
    print(y);
}

void test-scope() {
    String y = "local";
    printY();
}

test-scope(); // statically scoped languages print "global"
              // dynamically languages print "local"

print-y();   // all languages should print "global"

Static Scoping with Nested Procedures

In Algol, Pascal, Simula, Ada, and other languages in the Algol family, you can also nest procedure and function declarations inside of other procedure and function declarations. The same static scope rules apply.

    begin
    integer m, n;

    procedure laurel(n: integer);
        begin

        procedure hardy;
            begin
            print("in hardy -- n = ", n);
            end;

        print("in laurel -- m = ", m);
        print("in laurel -- n = ", n);
        hardy;
        end;

    m := 50;
    n := 100;
    print("in main program -- n = ", n);
    laurel(1);
    /* can't call hardy from the main program any more */
    end;
The output is:
in main program -- n = 100
in laurel -- m = 50
in laurel -- n = 1
in hardy -- n = 1 
Adapted from this page