36

From : http://www.artima.com/lejava/articles/designprinciples4.html

Erich Gamma: I still think it's true even after ten years. Inheritance is a cool way to change behavior. But we know that it's brittle, because the subclass can easily make assumptions about the context in which a method it overrides is getting called. There's a tight coupling between the base class and the subclass, because of the implicit context in which the subclass code I plug in will be called. Composition has a nicer property. The coupling is reduced by just having some smaller things you plug into something bigger, and the bigger object just calls the smaller object back. From an API point of view defining that a method can be overridden is a stronger commitment than defining that a method can be called.

I don't understand what he means. Could anyone please explain it?

5 Answers 5

63

A commitment is something that reduces your future options. Publishing a method implies that users will call it, therefore you can't remove this method without breaking compatibility. If you'd kept it private, they couldn't (directly) call it, and you could some day refactor it away without problems. Therefore, publishing a method is a stronger commitment than not publishing it. Publishing an overridable method is an even stronger commitment. Your users can call it, and they can create new classes where the method doesn't do what you think it does!

For instance, if you publish a clean-up method, you can ensure that resources are properly deallocated as long as users remember to call this method as the last thing they do. But if the method is overridable, someone might override it in a subclass and not call super. As a result, a third user might use that class and cause a resource leak even though they dutifully called cleanup() at the end! This means that you can no longer guarantee the semantics of your code, which is a very bad thing.

Essentially, you can no longer rely on any code running in user-overridable methods, because some middleman might override it away. This means that you have to implement your clean-up routine entirely in private methods, with no help from the user. Therefore it's usually a good idea to publish only final elements unless they are explicitly intended for overriding by API users.

13
  • 11
    This is possibly the best argument against inheritance that I've ever read. Of all the reasons against it that I've encountered, I've never come across these two arguments before (coupling and breaking functionality through overriding), yet both are very powerful arguments against inheritance.
    – David Arno
    Commented Dec 4, 2015 at 16:34
  • 5
    @DavidArno I don't think it's an argument against inheritance. I think it's an arguments against "make everything overridable by default". Inheritance is not dangerous by itself, using it without thought is.
    – svick
    Commented Dec 4, 2015 at 19:35
  • 15
    While this sounds like a good point I can't really see how "a user may add his own buggy code" is an argument. Enabling inheritance allows users to add lacking functionality without loosing updateability, a measure which can prevent and fix bugs. If an users code atop of your API is broken it its not an API flaw.
    – Sebb
    Commented Dec 4, 2015 at 20:40
  • 4
    You could easily turn that argument around into: the first coder makes a cleanup argument, but makes mistakes and doesn't clean everything up. The second coder overrides the cleanup method and does a good job, and coder #3 uses the class and doesn't have any resource leaks even though coder #1 made a mess of it.
    – Pieter B
    Commented Dec 5, 2015 at 9:07
  • 6
    @Doval Indeed. Which is why it's a travesty that inheritance is lesson number one in just about every introductory OOP book and class. Commented Dec 6, 2015 at 6:41
30

If you publish a normal function, you give a one-sided contract:
What does the function do if called?

If you publish a callback, you also give a one-sided contract:
When and how will it be called?

And if you publish an overridable function, it's both at once, so you give a two-sided contract:
When will it be called, and what must it do if called?

Even if your users aren't abusing your API (by breaking their part of the contract, which might be prohibitively expensive to detect), you can easily see that the latter needs far more documentation, and everything you document is a commitment, which limits your further choices.

An example of reneging on such a two-sided contract is the move from show and hide to setVisible(boolean) in java.awt.Component.

3
  • +1. I'm not sure why the other answer was accepted; it makes some interesting points, but it's definitely not the right answer to this question, in that it's definitely not what is meant by the quoted passage.
    – ruakh
    Commented Dec 5, 2015 at 18:47
  • This is the correct answer, but I don't understand the example. Replacing show and hide with setVisible(boolean) seems to break code that doesn't use inheritance too. Am I missing something?
    – eigensheep
    Commented Dec 31, 2015 at 23:09
  • 3
    @eigensheep: show and hide still exist, they're just @Deprecated. So the change doesn't break any code that merely invokes them. But if you've overridden them, your overrides will not be called by clients who migrate to the new 'setVisible'. (I've never used Swing, so I don't know how common it is to override those; but since it happened a long time ago, I imagine the reason that Deduplicator remembers it is that it bit him/her painfully.)
    – ruakh
    Commented Jan 1, 2016 at 1:53
13

Kilian Foth's answer is excellent. I'd just like to add the canonical example* of why this is a problem. Imagine an integer Point class:

class Point2D {
    public int x;
    public int y;

    // constructor
    public Point2D(int theX, int theY) { x = theX; y = theY; }

    public int hashCode() { return x + y; }

    public boolean equals(Object o) {
        if (this == o) { return true; }
        if ( !(o instanceof Point2D) ) { return false; }

        Point2D that = (Point2D) o;

        return (x == that.x) &&
               (y == that.y);
    }
}

Now let's sub-class it to be a 3D point.

class Point3D extends Point2D {
    public int z;

    // constructor
    public Point3D(int theX, int theY, int theZ) {
        super(x, y); z = theZ;
    }

    public int hashCode() { return super.hashCode() + z; }

    public boolean equals(Object o) {
        if (this == o) { return true; }
        if ( !(o instanceof Point3D) ) { return false; }

        Point3D that = (Point3D) o;

        return super.equals(that) &&
               (z == that.z);
    }
}

Super simple! Let's use our points:

Point2D p2a = new Point2D(3, 5);
Point2D p2b = new Point2D(3, 5);
Point2D p2c = new Point2D(3, 7);

p2a.equals(p2b); // true
p2b.equals(p2a); // true
p2a.equals(p2c); // false

Point3D p3a = new Point3D(3, 5, 7);
Point3D p3b = new Point3D(3, 5, 7);
Point3D p3c = new Point3D(3, 7, 11);

p3a.equals(p3b); // true
p3b.equals(p3a); // true
p3a.equals(p3c); // false

You are probably wondering why I'm posting such an easy example. Here's the catch:

p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!

When we compare the 2D point to the equivalent 3D point, we get true, but when we reverse the comparason, we get false (because p2a fails instanceof Point3D).

Conclusion

  1. It is usually possible to implement a method in a subclass in such a way that it's no-longer compatible with how the super-class expects it to work.

  2. It is generally impossible to implement equals() on a significantly different subclass in a way that is compatible with it's parent class.

When you write a class that you intend to allow people to subclass, it's a really good idea to write out a contract for how each method should behave. Even better would be a set of unit tests people could run against their implementations of overridden methods to prove that they do not violate the contract. Almost nobody does that because it's too much work. But if you care, that's the thing to do.

A great example of a well-spelled out contract is Comparator. Just ignore what it says about .equals() for the reasons described above. Here's an example of how Comparator can do things .equals() can't.

Notes

  1. Josh Bloch's "Effective Java" Item 8 was the source of this example, but Bloch uses a ColorPoint which adds a color instead of a third axis and uses doubles instead of ints. Bloch's Java example is basically duplicated by Odersky/Spoon/Venners who made their example available online.

  2. Several people have objected to this example because if you let the parent class know about the sub-class, you can fix this issue. That's true if there are a small enough number of sub-classes and if the parent knows about them all. But the original question was about making an API which someone else will write sub-classes for. In that case, you generally can't update the parent implementation to be compatible with the sub-classes.

Bonus

Comparator is also interesting because it works around the issue of implementing equals() correctly. Better yet, it follows a pattern for fixing this type of inheritance issue: the Strategy design pattern. The Typeclasses that Haskell and Scala people get excited about are also the Strategy pattern. Inheritance isn't bad or wrong, it's just tricky. For further reading, check out Philip Wadler's paper How to make ad-hoc polymorphism less ad hoc

15
  • 1
    SortedMap and SortedSet don't actually change the definitions of equals from how Map and Set define it, though. Equality completely ignores the ordering, with the effect that, for example, two SortedSets with the same elements but different sort orders still compare equal. Commented Dec 4, 2015 at 18:08
  • 1
    @user2357112 You are right and I have removed that example. SortedMap.equals() being compatible with Map is a separate issue which I will proceed to complain about. SortedMap is generally O(log2 n) and HashMap (the canonical implmementation of Map) is O(1). Therefore, you would only use a SortedMap if you really care about ordering. For that reason I believe the order is important enough to be a critical component of the equals() test in SortedMap implementations. They should not share an equals() implementation with Map (they do through AbstractMap in Java). Commented Dec 4, 2015 at 18:18
  • 3
    "Inheritance isn't bad or wrong, it's just tricky." I understand what you're saying, but tricky things typically lead to errors, bugs, and problems. When you can accomplish the same things (or nearly all the same things) in a more reliable way, then the more tricky way is bad.
    – jpmc26
    Commented Dec 5, 2015 at 0:31
  • 7
    This is an awful example, Glen. You just used inheritance in a way it should not be used, no wonder the classes do not work the way you intended. You broke the Liskov's substitution principle by providing a wrong abstraction (the 2D point), but just because inheritance is bad in your wrong example does not mean it is bad in general. Although this answer might look reasonable, it will only confuse people who do not realize it breaks the most basic inheritance rule.
    – Andy
    Commented Dec 5, 2015 at 8:28
  • 3
    ELI5 of Liskov's Substituton Principle says: Should a class B be a child of a class A and should you instantiate an object of a class B, you should be able to cast the class B object to it's parent and use the casted variable's API without losing any implementation details of the child. You broke the rule by providing the third property. How do you plan to access the z coordinate after you cast the Point3D variable to Point2D, when the base class has no idea such property exists? If by casting a child class to its base you break the public API, your abstraction is wrong.
    – Andy
    Commented Dec 6, 2015 at 16:00
4

Inheritance Weakens Encapsulation

When you publish an interface with inheritance permitted, you substantially increase the size of your interface. Each overrideable method could be replaced and so should be thought of as callback provided to the constructor. The implementation provided by your class is merely the default value of the callback. Thus, some kind of contract should be provided indicating what the expectations on the method are. This seldom happens and is a major reason why object oriented code is called brittle.

Below is a real (simplified) example from the java collections framework, courtesy of Peter Norvig (http://norvig.com/java-iaq.html).

Public Class HashTable{
    ...
    Public Object put(K key, V value){
        try{
            //add object to table;
        }catch(TableFullException e){
            increaseTableSize();
            put(key,value);
        }
    }
}

So what happens if we subclass this?

/** A version of Hashtable that lets you do
 * table.put("dog", "canine");, and then have
 * table.get("dogs") return "canine". **/

public class HashtableWithPlurals extends Hashtable {

    /** Make the table map both key and key + "s" to value. **/
    public Object put(Object key, Object value) {
        super.put(key + "s", value);
        return super.put(key, value);
    }
}

We have a bug: Occasionally we add "dog" and the hashtable gets an entry for "dogss". The cause was someone providing an implementation of put that the person designing the Hashtable class did not expect.

Inheritance Breaks Extensibility

If you allow your class to be subclassed, you are committing to not add any methods to your class. This could otherwise be done without breaking anything.

When you add new methods to an interface, anyone who has inherited from your class will need to implement those methods.

0
3

If a method is meant to be called, you only have to ensure it works correctly. That's it. Done.

If a method is designed to be overridden, you also have to think carefully about the scope of the method: if the scope is too large, child class will often need to include copy-pasted code from parent method; if it is too small, many methods will need to be overridden to have desired new functionality - this adds complexity and needless line count.

Therefore creator of parent method needs to make assumptions on how the class and its methods might be overridden in the future.

However, the author is talking about a different issue in the quoted text:

But we know that it's brittle, because the subclass can easily make assumptions about the context in which a method it overrides is getting called.

Consider method a which is being normally called from method b, but in some rare and non-obvious cases from method c. If author of overriding method overlooks the c method and its expectations on a, it's obvious how things can go wrong.

Therefore it's more important that a is defined clearly and unambiguously, well documented, "does one thing and does it well" -- more so than if it were a method only designed to be called.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.