Lecture 11: Inheritance vs composition
Inheritance vs Composition Lecture Video.
11.1 Code reuse
Code reuse is good practice when done properly. Code reuse refers to an implementation being used in places where it was not originally intended. When an implementation is practically used, refined and enhanced, it has the advantage of being tested periodically and realistically and thus becomes robust with age. It is a good idea to reuse it whenever applicable. Such code is likely to be more robust than whatever we can write from scratch. Moreover reusing existing code lessens time and effort. It is important to note that both these advantages are contingent upon the reliability of the code. Thus code reuse certainly does not mean using “anything available on the internet” because its correctness, robustness and reliability are unverified. An equally important consideration is whether the code reuse is authorized (but that is an ethical/legal issue rather than technical).
From the design perspective code must be explicitly designed for reuse. A broad rule is that the more generic a design is (i.e. not focused on a particular domain of which it is a part, not overly coupled to other components that may not be useful in other situations, etc.), the more reusable it is. Reusability should be an explicit goal in design.
How does one “reuse” code? The simplest case is where existing code is exactly what we want. Unfortunately such a perfect match does not happen often. A more realistic scenario for reuse is when an existing component can be used in some way to build another different, but related component. There are many ways to do this. One of the simpler and more obvious ways is to use inheritance.
11.2 Inheritance
Inheritance is a simple example of code reuse. The derived class, by inheriting from the base class, reuses its functionality (instead of reimplementing it). But inheritance can create subtle issues, as illustrated by the following example.
11.2.1 A set of integers
Suppose person A has a requirement for a set of integers. Following good design practice she creates an interface for it and an implementation that (re)-uses Java’s Set implementations.
/** * Interface for simple sets of integers. public interface IntSet { /** * Adds the given value to this set. * * @param value the integer add */ void add(int value); /** * Adds all the values in the given array to this set. * * @param values the integers to add */ void addAll(int... values); /** * Removes the given value from this set, if present. * * @param value the integer to remove */ void remove(int value); /** * Determines whether a particular integer value is a member of this set. * * @param value the integer to check * @return whether value is a member of this set */ boolean member(int value); }
import java.util.HashSet; import java.util.Set; /** * An implementation of IntSet using the Collections Framework's * HashSet. * */ public class IntSet1 implements IntSet { private final Set<Integer> set = new HashSet<>(); @Override public void add(int value) { set.add(value); } // addAll() is implemented in terms of add() in order avoid code duplication. // In this example the implementation of add() is minuscule, but in a real // implementation it might be complicated, and we wouldn't want to repeat // it in addAll() as well. @Override public void addAll(int... values) { for (int i : values) { add(i); } } @Override public void remove(int value) { set.remove(value); } @Override public boolean member(int value) { return set.contains(value); } }
This implementation has been thoroughly tested and works well.
11.2.2 Code reuse
Person B requires a set implementation, but in addition to the above operations, she needs a new operation: keep track of how many numbers were added to the set (note that this is not the same as the size of the set). In an attempt to reuse the above implementation, B creates a new interface and an implementation that inherits from the above.
public interface InstrumentedIntSet extends IntSet { /** * Returns the count of how many times an element has been added to the set. * * @return the count of added elements */ int getAddCount(); }
/** * An integer set that is instrumented to count the number of times an * element is added. It works by extending IntSet1, which * provides the basic set operations. . */ public class InstrumentedIntSet1 extends IntSet1 implements InstrumentedIntSet { private int addCount = 0; @Override public int getAddCount() { return addCount; } @Override public void add(int value) { super.add(value); // Update the count for the one element we added: ++addCount; } @Override public void addAll(int... values) { super.addAll(values); // Update the count for all the elements we added: addCount += values.length; } }
11.2.3 The perils of inheritance
To her dismay person B’s tests for this class fail.
Do Now!
Look at the above code and try to determine why this is so.
Specifically she discovers that the number of added elements is not being counted correctly. As it often happens, B does not have access to the source code that A wrote. Thus she does not have the opportunity to read the code written by A, which would have indeed pointed her to the cause of the failure. Nevertheless she makes the following correction to her addAll method that removes the bug.
@Override public void addAll(int... values) { super.addAll(values); }
Person A originally decided to reuse the add method for legitimate reasons: to avoid code replication. But she soon discovers that this can create other problems, as public methods are open to being overridden (as B did). So A changes the implementation of addAll in the IntSet1 class to the following:
// addAll() is implemented in terms of add() in order avoid code duplication. // In this example the implementation of add() is minuscule, but in a real // implementation it might be complicated, and we wouldn't want to repeat // it in addAll() as well. @Override public void addAll(int... values) { for (int i : values) { set.add(i); //<--CHANGE } }
As a result, person B’s code now again fails her own tests. The reason for this is that inheritance has coupled the InstrumentedIntSet1 class written by A to the IntSet1 class written by B. Due to this coupling any changes in the former changes the way in which the latter works. This is not surprising, but the behavior is changing due to overriding, which is difficult to imagine just by looking at the code symptoms.
11.2.4 Controlling inheritance
We can choose to guard an implementation against future and unforeseen overriding. Person A changes her code to the following:
public class IntSet3 implements IntSet { private final Set<Integer> set = new HashSet<>(); // Here's the private method to factor out add() and addAll()'s common code: private void _add(int value) { set.add(value); } // Now add() is implemented using _add(): @Override public void add(int value) { _add(value); } // And so is addAll(): @Override public void addAll(int... values) { for (int i : values) { _add(i); } } @Override public void remove(int value) { set.remove(value); } @Override public boolean member(int value) { return set.contains(value); } }
This implementation refactors the earlier implementation by putting the common code snippets (set.add(value); in a private helper method. This helper method is used in both add and addAll methods. Since it is private, it cannot be overridden. Thus Person B’s code is more reliable because overriding the add method has not changed how the addAll method works.
To ensure this never happens, Person A can take a drastic measure: make IntSet1 so that it cannot be extended.
import java.util.HashSet; import java.util.Set; /** * An implementation of IntSet using the Collections Framework's * HashSet. * */ public final class IntSet1 implements IntSet { ... }
How can this class be reused?
11.2.5 Reuse via composition
In this case reuse can happen by using an object of the class rather than the class itself.
public class InstrumentedIntSetComposition implements InstrumentedIntSet { private int addCount = 0; private final IntSet delegate; /** * Constructs a new instrumented integer set. */ public class InstrumentedIntSetComposition { delegate = new IntSet1(); } /** * Returns the count of how many times an element has been added to the set. * * @return the count of added elements */ public int getAddCount() { return addCount; } @Override public void add(int value) { delegate.add(value); ++addCount; } @Override public void addAll(int... values) { delegate.addAll(values); addCount += values.length; } @Override public void remove(int value) { delegate.remove(value); } @Override public boolean member(int value) { return delegate.member(value); } }
This class keeps a reference to an IntSet object inside it as an instance variable (i.e. it is composed of an IntSet object). To reuse its functionality it simply calls its functions. For example the add method calls the add method of the IntSet object. This is called delegation: simply passing on to another function/class.
As inheritance is not used, this class must explicitly write the remove and member methods, even if they simply delegate. Inheritance resulted in shorter code as methods that were applicable as-is need not be written.
Why is a coupling between a class and an interface better than that between two classes? An interface is less likely to change because it merely defines method signatures and contains no implementation details. Thus although the coupling exists, it is less likely to pose a problem.
What about the coupling issue? The default constructor creates the delegate object delegate = new IntSet1();}, thus coupling this class to the IntSet1 class in much the same way. This creates the possibility that a change in the IntSet1 class may cause this class to change its behavior as well. However we note that this class uses only those methods of the IntSet1 class that it implements from the IntSet interface. In other words the InstrumentedIntSetComposition class can work with any implementation of the IntSet interface, not just the IntSet1 class. This creates a rather looser coupling between IntSet and InstrumentedIntSetComposition class. The onus of creating and passing an IntSet object is up to the client, not this class. Our final design is:
public class InstrumentedIntSetComposition implements InstrumentedIntSet { private int addCount = 0; private final IntSet delegate; //NO MENTION OF IntSet1 ANYWHERE! /** * Constructs an instrumented integer set on top of an existing * IntSet. * * @param base the integer set to instrument (non-null) */ public InstrumentedIntSetComposition(IntSet base) { Objects.requireNonNull(base, "base cannot be null"); delegate = base; } /** * Returns the count of how many times an element has been added to the set. * * @return the count of added elements */ public int getAddCount() { return addCount; } @Override public void add(int value) { delegate.add(value); ++addCount; } @Override public void addAll(int... values) { delegate.addAll(values); addCount += values.length; } @Override public void remove(int value) { delegate.remove(value); } @Override public boolean member(int value) { return delegate.member(value); } }