Effective Java Notes (18) Composite takes precedence over inheritance

        Inheritance is a powerful means of code reuse, but it's not always the best tool for the job. Improper use can lead to software becoming very fragile. It is very safe to use inheritance inside packages, where both the subclass and superclass implementations are under the control of the same programmer. It is also very safe to use inheritance for classes that are specifically designed for inheritance and are well documented. However, it is very dangerous to inherit from ordinary concrete classes across package boundaries. As a reminder, this article uses the word "inheritance" to mean implementation inheritance (when a class extends another class). The issues discussed in this item do not apply to interface inheritance (when a class implements an interface, or when an interface extends another interface).

        Unlike method calls, inheritance breaks encapsulation. In other words, a subclass depends on the implementation details of a particular functionality in its superclass. It's possible that the superclass's implementation may change from release to release, and if it does change, the subclass may break even though its code hasn't changed at all. Thus, a subclass must evolve with updates to its superclass, unless the superclass is specifically designed for extension and is well documented.

        In order to illustrate a little more concretely, we assume that there is a program using HashSet. To tune the performance of this program, you need to query the HashSet to see how many elements have been added since it was created (not to be confused with its current number of elements, which decrements as elements are removed). To provide this functionality, we have to write a HashSet variant that defines addCount the number of elements the record is trying to insert, and derives an accessor method for that count.
        The HashSet class contains two methods that can add elements: add and addAll, so these two methods must be overridden:

public class InstrumentedHashSet<E> extends HashSet<E>
{
    private int addCount = 0;

    public InstrumentedHashSet() {
    }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @Override public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addA11(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

        This class looks very reasonable, but it doesn't work properly. Suppose we create an instance and add three elements using the addAll method. Incidentally, notice that we create a list using the static factory method List.of, which was added in Java 9. If using an earlier version, use Arrays.asList instead:

InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("Snap","Crackle", "Pop"));

        At this point we expect the getAddCount method to return 3, but instead it returns 6. What went wrong? Inside the HashSet, the addAll method is implemented based on its add method. Even if such implementation details are not stated in the HashSet documentation, it is reasonable. The addAll method in InstrumentedHashSet first adds 3 to addCount, and then uses supper.addAll to call the HashSet's addAll implementation. Then call the add method covered by InstrumentedHashSet in turn, calling once for each element. Each of these three calls adds 1 to addCount, so a total of 6 is added: each element added by the addAll method is counted twice.

        We can "fix" the subclass simply by removing the overridden addAll method. While the resulting class works fine, its functional correctness relies on the fact that HashSet's addAll method is implemented on its add method. This "self-use" is an implementation detail, not a promise, and is not guaranteed to remain the same across all implementations of the Java platform, nor is it guaranteed to change from release to release. Therefore, the resulting InstrumentedHashSet class will be very fragile.

        The problems all come from overriding methods. You might think that when extending a class, it's safe to just add new methods without overriding existing ones. Although this expansion method is safer, it is not completely risk-free. If the superclass gets a new method in a subsequent release, and unfortunately you give the subclass a method with the same signature but a different return type, then such a subclass will fail to compile. If you give the subclass a method with exactly the same signature and return type as the new superclass method, you're effectively overriding the superclass method, so you're back at the problem. Also, it's very doubtful that your method will obey the contract of the new superclass method, because when you write the subclass method, this contract has not yet been published.

        Fortunately, there is a way to avoid all of the aforementioned problems. That is, instead of extending the existing class, a private field is added to the new class that refers to an instance of the existing class. This design is called "composition" because the existing class becomes a component of the new class. Each instance method in the new class can call the corresponding method in the contained existing class instance and return its result. This is called forwarding, and the methods in the new class are called forwarding methods. The resulting class will be very solid, it does not depend on the implementation details of the existing class. Even if a new method is added to an existing class, it will not affect the new class. For a more concrete illustration, see the following example, which replaces the InstrumentedHashSet class with a composite/forward approach. Note that this implementation is split into two parts: the class itself and the reusable forwarding class, which contains all the forwarding methods and nothing else:

// Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E>
{
    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addA11(Collection<? extends E> c) {
        addCount += c.size();
        return super.addA11(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

// Reusable forwarding class
public class ForwardingSet<E> implements Set<E>
{
    private final Set<E> s;

    public ForwardingSet(Set<E> s) {
        this.s = s;
    }

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

    public boolean contains(Object o { return s. contains(o); }

    public boolean isEmpty() {
        return s.isEmpty();
    }

    public int size() {
        return s.size();
    }

    public Iterator<E> iterator() {
        return s.iterator();
    }

    public boolean add(E e) {
            return s.add(e);

    public boolean remove(Object o) {
        return s.remove(o);
    }

    public boolean containsA11(Collection<?> c) {
        return s.containsA11(c);
    }

    public boolean addAll(Collection<? extends E> c) {
        return s.addAll(c);
    }

    public boolean removeAll(Collection<?> c) {
        return s.removeAll(c);
    }

    public boolean retainAll(Collection<?> c) {
        return s.retainAll(c);
    }

    public Object[] toArray() {
        return S.toArray();
    }

    public <T> T[] toArray(T[] a) {
        return s.toArray(a);
    }

    @Override
    public boolean equals(Object o) {
        return s.equals(o);
    }

    @Override
    public int hashCode() {
        return s.hashCode();
    }

    @Override
    public String toString() {
        return s.toString();
    }
}

        The existence of the Set interface makes the design of the InstrumentedSet class possible, because the Set interface preserves the functional characteristics of the HashSet class. In addition to gaining robustness, this design also brings more flexibility. The InstrumentedSet class implements the Set interface and has a single constructor whose parameter is also of type Set. Essentially, this class turns a Set into another Set, adding counting functionality. Unlike the previously mentioned inheritance-based approach, which only works on a single concrete class and requires a separate constructor for each constructor supported in the superclass, here the wrapper class ) can be used to wrap any Set implementation, and work in conjunction with any pre-existing constructor:

Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp));
Set<E> s = new InstrumentedSet<> (new HashSet<> (INIT_CAPACITY));

The InstrumentedSet class can even be used to temporarily replace a Set instance that does not have counting properties:

static void walk(Set<Dog> dogs) {
    InstrumentedSet<Dog> iDogs = new InstrumentedSet<>(dogs) ;
    ...// Within this method use iDogs instead of dogs
}

        Because each instance of InstrumentedSet wraps another instance of Set, the InstrumentedSet class is called a wrapper class. This is exactly the Decorator (decorator) pattern, because the InstrumentedSet class decorates a collection, adding counting features to it. Sometimes the combination of compounding and forwarding is also loosely called "delegation". Technically, this is not delegation, unless the wrapping object passes itself to the wrapped object.

        Wrapper classes have few downsides. One thing to note is that wrapper classes are not suitable for use in callback frameworks (callback frameworks); in callback frameworks, objects pass references to themselves to other objects for subsequent calls ("callbacks"). Because the wrapped object doesn't know its wrapper object, it passes a reference to itself
( this ), and avoids the wrapper object when calling back. This is known as the SELF problem. Some people worry about the performance impact of forwarding method calls, or the memory footprint of wrapped objects. In practice, neither makes a big difference. Writing the forwarding method is a bit trivial, but you only need to write the constructor once for each interface, and the forwarding class can be provided by the package containing the interface. For example, Guava provides forwarding classes for all collection interfaces.

        Inheritance is only appropriate when the subclass is truly a subtype of the superclass. In other words, for two classes A and B, class B should extend class A only if there really is an "is-a" relationship between the two. If you're going to have class B extend class A, you should ask yourself: Is every B really an A? If you can't be sure the answer to this question is yes, then B should not extend A. If the answer is no, in general, B should contain a private instance of A and expose a smaller, simpler API: A is not per se a part of B, just an implementation detail of it .

        There are many clear violations of this principle in the Java platform class libraries. For example, a stack (stack) is not a vector (vector), so Stack should not extend Vector. Likewise, a property list is not a hash table, so Properties should not extend Hash table. In both cases, the Composite pattern is appropriate.

        If inheritance is used where composition is appropriate, implementation details are unnecessarily exposed. The resulting API will limit you to the original implementation, forever limiting the performance of the class. What's more, since the internal details are exposed, it is possible for the client to directly access these internal details. This would at least lead to semantic confusion. For example, if p points to a Properties instance, then p.getProperty(key) may produce different results than p.get(key): the former method takes into account the default property table, while the latter method inherits from the Hash table, without Consider the default property list. Worst of all, it is possible for a client to modify the superclass directly, thus breaking the constraints of the subclass. In the case of Properties, the designer's goal was to allow only strings as keys ( key ) and values ​​( value ), but direct access to the underlying Hashtable allows violation of this constraint. Once the constraint is violated, it is not possible to use other parts of the Properties API (load and store). By the time this problem is discovered, it is too late to correct it, because the client relies on using non-string keys and values.

        There is one final set of questions you should ask yourself before deciding to use inheritance instead of composition. Is there a bug in the API of the class you're trying to extend? If so, are you willing to propagate those defects into the API of the class? Inheritance mechanism propagates any deficiencies in the superclass API to subclasses, while composition allows new APIs to be designed to hide these deficiencies.

        In short, inheritance is very powerful, but also has many problems, because it violates the principle of encapsulation. Using inheritance is only appropriate when there is a real subtyping relationship between the subclass and the superclass. Even so, inheritance can lead to fragility if the subclass and superclass are in different packages, and the superclass was not designed for inheritance. To avoid this fragility, composition and forwarding mechanisms can be used instead of inheritance, especially when there is an appropriate interface to implement the wrapper class. Wrapper classes are not only more robust than subclasses, but also more powerful.

Guess you like

Origin blog.csdn.net/java_faep/article/details/132054021