6.8 Inheritance vs. Aggregation

Encapsulation

An object has properties and behaviors that are encapsulated inside the object. The services it offers to its clients comprises its contract. Only the contract defined by the object is available to the clients. The implementation of its properties and behavior is not a concern of the clients. Encapsulation helps to make clear the distinction between an object's contract and implementation. This has major consequences for program development. The implementation of an object can change without implications for the clients. Encapsulation also reduces complexity, as the internals of an object are hidden from the clients, who cannot influence its implementation.

Choosing between Inheritance and Aggregation

Figure 6.6 is a UML class diagram, showing several aggregation relationships and one inheritance relationship. The class diagram shows a queue defined by aggregation, and a stack defined by inheritance. Both are based on linked lists. A linked list is defined by aggregation. The implementation of these data structures is shown in Example 6.15. The purpose of the example is to illustrate inheritance and aggregation, not industrial-strength implementation of queues and stacks. The class Node at (1) is straightforward, defining two fields: one denoting the data and the other denoting the next node in the list. The class LinkedList at (2) keeps track of the list by managing a head and a tail reference. Nodes can be inserted in the front or back, but deleted only from the front of the list.

Example 6.15 Implementing Data Structures by Inheritance and Aggregation
class Node {                                                   // (1)
    private Object data;    // Data
    private Node   next;    // Next node

    // Constructor for initializing data and reference to the next node.
    Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    // Methods
    public void   setData(Object obj) { data = obj; }
    public Object getData()           { return data; }
    public void   setNext(Node node)  { next = node; }
    public Node   getNext()           { return next; }
}

class LinkedList {                                             // (2)
    protected Node head = null;
    protected Node tail = null;

    // Methods
    public boolean isEmpty() { return head == null; }
    public void insertInFront(Object dataObj) {
        if (isEmpty()) head = tail = new Node(dataObj, null);
        else head = new Node(dataObj, head);
    }
    public void insertAtBack(Object dataObj) {
        if (isEmpty())
            head = tail = new Node(dataObj, null);
        else {
            tail.setNext(new Node(dataObj, null));
            tail = tail.getNext();
        }
    }
    public Object deleteFromFront() {
        if (isEmpty()) return null;
        Node removed = head;
        if (head == tail) head = tail = null;
        else head = head.getNext();
        return removed.getData();
    }
}

class QueueByAggregation {                                     // (3)
    private LinkedList qList;

    // Constructor
    QueueByAggregation() {
        qList = new LinkedList();
    }

    // Methods
    public boolean isEmpty() { return qList.isEmpty(); }
    public void enqueue(Object item) { qList.insertAtBack(item); }
    public Object dequeue() {
        if (qList.isEmpty()) return null;
        else return qList.deleteFromFront();
    }
    public Object peek() {
        return (qList.isEmpty() ? null : qList.head.getData());
    }
}

class StackByInheritance extends LinkedList {                  // (4)
    public void push(Object item) { insertInFront(item); }
    public Object pop() {
       if (isEmpty()) return null;
       else return deleteFromFront();
    }
    public Object peek() {
        return (isEmpty() ? null : head.getData());
    }
}

public class Client {                                           // (5)
    public static void main(String[] args) {
        String string1 = "Queues are boring to stand in!";
        int length1 = string1.length();
        QueueByAggregation queue = new QueueByAggregation();
        for (int i = 0; i<length1; i++)
            queue.enqueue(new Character(string1.charAt(i)));
        while (!queue.isEmpty())
            System.out.print((Character) queue.dequeue());
        System.out.println();

        String string2 = "!no tis ot nuf era skcatS";
        int length2 = string2.length();
        StackByInheritance stack = new StackByInheritance();
        for (int i = 0; i<length2; i++)
            stack.push(new Character(string2.charAt(i)));
        stack.insertAtBack(new Character('!'));                 // (6)
        while (!stack.isEmpty())
            System.out.print((Character) stack.pop());
        System.out.println();
    }
}

Output from the program:

Queues are boring to stand in!
Stacks are fun to sit on!!
Figure 6.6. Implementing Data Structures by Inheritance and Aggregation

graphics/06fig06.gif

Choosing between inheritance and aggregation to model relationships can be a crucial design decision. A good design strategy advocates that inheritance should be used only if the relationship is-a is unequivocally maintained throughout the lifetime of the objects involved; otherwise, aggregation is the best choice. A role is often confused with an is-a relationship. For example, given the class Employee, it would not be a good idea to model the roles an employee can play (such as a manager or a cashier) by inheritance if these roles change intermittently. Changing roles would involve a new object to represent the new role every time this happens.

Code reuse is also best achieved by aggregation when there is no is-a relationship. Enforcing an artificial is-a relationship that is not naturally present, is usually not a good idea. This is illustrated in Example 6.15 at (6). Since the class StackByInheritance at (4) is a subclass of the class LinkedList at (2), any inherited method from the superclass can be invoked on an instance of the subclass. Also, methods that contradict the abstraction represented by the subclass can be invoked, as shown at (6). Using aggregation in such a case results in a better solution, as demonstrated by the class QueueByAggregation at (3). The class defines the operations of a queue by delegating such requests to the underlying class LinkedList. Clients implementing a queue in this manner do not have access to the underlying class and, therefore, cannot break the abstraction.

Both inheritance and aggregation promote encapsulation of implementation, as changes to the implementation are localized to the class. Changing the contract of a superclass can have consequences for the subclasses (called the ripple effect) and also for clients who are dependent on a particular behavior of the subclasses.

Polymorphism is achieved through inheritance and interface implementation. Code relying on polymorphic behavior will still work without any change if new subclasses or new classes implementing the interface are added. If no obvious is-a relationship is present, then polymorphism is best achieved by using aggregation with interface implementation.