38

What is the fragile base class problem in java?

drewh
  • 10,077
  • 7
  • 34
  • 43
SunilRai86
  • 990
  • 6
  • 16
  • 26
  • 4
    resisting urge to reference A Major Award! or a NIN album... – scunliffe May 27 '10 at 13:15
  • 5
    Sorry to break it to you, @scunliffe, but you failed ;-) – Joachim Sauer May 27 '10 at 13:19
  • If one says that FBC problem is having a base class with an unclearly (or incompletely) specified contract so that changing the class will possibly break the derived ones relying on it, will that suffice? And can we have derived interfaces, btw? – mlvljr May 27 '10 at 14:41

4 Answers4

38

A fragile base class is a common problem with inheritance, which applies to Java and any other language which supports inheritance.

In a nutshell, the base class is the class you are inheriting from, and it is often called fragile because changes to this class can have unexpected results in the classes that inherit from it.

There are few methods of mitigating this; but no straightforward method to entirely avoid it while still using inheritance. You can prevent other classes inheriting from a class by labelling the class declaration as final in Java.

A best practice to avoid the worst of these problems is to label all classes as final unless you are specifically intending to inherit from them. For those to intend to inherit from, design them as if you were designing an API: hide all the implementation details; be strict about what you emit and careful about what you accept, and document the expected behaviour of the class in detail.

Colin Pickard
  • 45,724
  • 13
  • 98
  • 148
  • 1
    As a more concrete example - Say you don't put final everywhere, and you refactor a private function to call a public one. Now, assume someone else was inheriting from your class and overloading your public function in an unexpected way. You may have just broken someone else's code. The ways in which you can refactor without introducing a breaking change is much more limited if you allow others to inherit from it. It's fragile. – Scotty Jamison Nov 04 '21 at 15:21
15

A base class is called fragile when changes made to it break a derived class.

class Base{
    protected int x;
    protected void m(){
       x++;
    }

    protected void n(){
      x++;      // <- defect 
      m();
     }
 }


class Sub extends Base{
        protected void m(){
            n();
        }
    }
stacker
  • 68,052
  • 28
  • 140
  • 210
  • 1
    It's fragile when simple changes to the parent class break the child class. If you're changing everything in the parent and that causes children to break, that doesn't necessarily mean the parent was fragile; if you change something seemingly benign and things fall apart, it was fragile. – Dean J May 27 '10 at 14:13
  • 4
    But which changes are simple then, i.e. what's the criteria? – mlvljr May 27 '10 at 14:32
3

All of what Colin Pickard said is true, but here I want to add some of the best practices when you are writing code that may cause this kind of issue, and especially if you are creating a framework or a library.

  1. Make all your concrete classes final by default, because you probably don't want them to be inherited. You can find this behavior as a feature of many languages, such as Kotlin. Besides, if you need to extend it, you can always remove the final keyword. In this way the absence of final on a class can be interpreted as a warning to not rely on specific functionality for other methods on this that are not private and/or final.

  2. For classes that cannot be marked as final, make as many methods as possible final to ensure they're not modified by subclasses. Additionally, do not expose methods that are not meant to be overridden—prefer private over protected. Assume that any method not private and/or final will be overridden and ensure that your superclass code will still work.

  3. Try to not use an inheritance ("Bar is a Foo") relationship. Instead use a helper ("Bar uses a Foo") relationship between your classes. Use interfaces rather than abstract classes to ensure that classes using this helper have a uniform interface.

  4. Remember that almost* every extends can be replaced by implements. This is true event when you want to have a default implementation; an example of such a conversion is shown below:

Old Code:

class Superclass {
    void foo() {
        // implementation
    }
    void bar() {
        // implementation
    }
}

class Subclass extends Superclass {
    // don't override `foo`
    // override `bar`
    @Override
    void bar() {
        // new implementation
    }
}

New Code:

// Replace the superclass with an interface.
public interface IClass {
    void foo();
    void bar();
}

// Put any implementation in another, final class.
final class Superclass implements IClass {
    public void foo() {
        // implementation for superclass
    }
    public void bar() {
        // implementation for superclass
    }
}

// Instead of `extend`ing the superclass and overriding methods,
// use an instance of the implementation class as a helper.
// Naturally, the subclass can also forgo the helper and
// implement all the methods for itself.
class Subclass implements IClass {
    private Superclass helper = new Superclass();

    // Don't override `foo`.
    public void foo() {
        this.helper.foo();
    }

    // Override `bar`.
    public void bar() {
        // Don't call helper; equivalent of an override.
        // Or, do call helper, but the helper's methods are
        // guaranteed to be its own rather than possibly
        // being overridden by yours.
    }
}

The advantage of this is that the methods of the superclass are able to be sure they are working with one another, but at the same time you can override methods in your subclass.

*If you actually wanted the superclass to use your overridden method you are out of luck using this approach unless you also want to reimplement all of those methods on the "subclass". That said, the superclass calling the subclass can be confusing so it may be good to reevaluate that type of usage, its incompatibility with this approach notwithstanding.

AndroidLover
  • 1,171
  • 1
  • 13
  • 16
3

It is widely described in below article By Allen Holub on JavaWorld

Why extends is evil. Improve your code by replacing concrete base classes with interfaces

bastiat
  • 1,799
  • 2
  • 19
  • 38