Composition over Inheritance: Part 2

Reading Time: 3 minutes

In the last blog, we discussed how using inheritance, even though a useful tool, can become problematic when used without precautions. To counter the violation of abstraction, we can consider using Composition. Which brings us to the obvious question, what is composition?

Composition

Instead of extending the class, we could create a private reference of the Set mentioned in the aforementioned example. This leads to multiple advantages. Let’s take a look.



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


import java.util.*;
public class ForwardingSet<T> implements Set<T> {
private final Set<T> set;
ForwardingSet(Set<T> set) {
this.set = set;
}
public void clear() { set.clear();}
public boolean contains(Object o) { return set.contains(o); }
public int size() { return set.size(); }
public Iterator<T> iterator() { return set.iterator(); }
public boolean add(T t) { return set.add(t); }
public boolean addAll(Collection<? extends T> c){ return set.addAll(c); }
public boolean remove(Object o) { return set.remove(o); }
public boolean containsAll(Collection<?> c) { return set.containsAll(c); }
public boolean removeAll(Collection<?> c) { return set.removeAll(c); }
public boolean retainAll(Collection<?> c) { return set.retainAll(c); }
public Object[] toArray() { return set.toArray(); }
public <T> T[] toArray(T[] a) { return set.toArray(a); }
public boolean isEmpty() {return set.isEmpty();}
@Override public boolean equals(Object o) { return set.equals(o);}
@Override public int hashCode() { return set.hashCode(); }
@Override public String toString() { return set.toString(); }
}



This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters


import java.util.*;
public class InstrumentedSet<T> extends ForwardingSet<T> {
private static int insertionCount;
public InstrumentedSet(Set<T> s) {
super(s);
}
@Override
public boolean add(T t) {
insertionCount++;
return super.add(t);
}
@Override
public boolean addAll(Collection<? extends T> collection) {
insertionCount += collection.size();
return super.addAll(collection);
}
public int getTotalCount() {
return insertionCount;
}
public static void main(String[] args) {
InstrumentedSet<String> set = new InstrumentedSet<>(new HashSet<String>());
List<String> input = Arrays.asList(new String[]{"Ayush","Prashar"});
set.addAll(input);
System.out.println(set.getTotalCount());
}
}

As you can see, InstrumentedSet class contains an instance field of the type Set. On examining the implementation of methods add( ) and addAll( ), you can see that our code is totally detached from how these methods have been implemented internally. Each instance method here calls the same method on the contained Set instance. This process is known as forwarding. Any change in the super-class can not break our expected behavior. Even adding new methods to the super-class makes no impact in our code. That is a major advantage when compared to the inheritance-based approach.

Here, we can see the implementation split into two parts. The set itself and the forwarding class which does the whole forwarding logic. Unlike the inheritance-based approach, this code will produce the value 2!

Screenshot from 2019-12-18 14-13-31

The instrumented set here takes a set as a constructor argument and adds its instrumentation ( in this case, the ability to count all inserted elements ever). It also implements a Set interface, therefore, the InstrumentedSet takes one set and converts it into another.

Advantages of Composition

  1. Inheritance based approach required constructors for each version of constructors supported by the superclass. Unlike that, our InstrumentedSet constructor works in conjunction with every pre-existing constructor.
  2. In our earlier approach, we could only model for one concrete type of Set at a time whereas now we are loosely coupled with the Set interface.
    Eg
    InstrumentedSet<String> set = new InstrumentedSet(new TreeSet<String>());
    
    InstrumentedSet<String> set = new InstrumentedSet(new HashSet<String>());
  3. Similarly, it can convert a pre-existing set to an InstrumentedSet.

Disadvantages of Composition

There is just one major disadvantage of working with Composition. They don’t suit well with callback frameworks. Suppose our wrapped set (A) passes its reference to another object (B) in a function using the “this” keyword. In our wrapper class, when we call the same function, we expose access to A instead of the wrapper instance to B. It is known as the self problem.

Although one could say we need to write multiple forwarding methods in the ForwardingSet class, that doesn’t amount to much of a disadvantage since it’s only done once for Set and can incorporate any implementation later.

In summary

Inheritance is powerful but needs to be used cautiously. Always ask yourself before using inheritance whether the classes form a definite “is-a” relationship and is the superclass well-written or will it propagate any flaws? Even in satisfactory conditions, avoid inheritance outside of the package or if the superclass isn’t designed for an extension as it may lead to fragile code in the long run.

I hope this blog was able to help with deciding your code structure. Feel free to comment your questions below and I will try to address them. This blog is just a piece out of Effective Java by Joshua Bloch so for more information please refer to the book. For more such blogs, stay tuned!

blog-footer

Written by 

Ayush Prashar is a software consultant having more than 1 year of experience. He is familiar with programming languages such as Java, Scala, C, C++ and he is currently working on reactive technologies like Lagom, Akka, Spark, and Kafka. His hobbies include playing table tennis, basketball, watching TV series and football.