Blog-Archiv

Sonntag, 20. Dezember 2020

Why Delegation is Not Better than Inheritance

The object-oriented idea often is associated with inheritance and obscure hierarchies. Lots of books and web articles recommend to prefer delegation to inheritance. Do they implicitly recommend to not use object-oriented languages?

In this Blog I want to bring more light into the discussion about "inheritance versus delegation", which I consider unbalanced. Mind that, from a technical point of view, both delegation and inheritance are about code reusage. Nevertheless, from the business perspective, inheritance may represent a determining and deliberate concept that is nice to have also in source code.

(Table of contents generated by JavaScript.)

Code Examples

We are going to implement following application by both inheritance and delegation:

public class Main
{
    public static void main(String[] args)  {
        Dog lupo = new Dog();
        lupo.eat("sausage");
        lupo.bark();

        Cat garfield = new Cat();
        garfield.eat("milk");
        garfield.miaow();
    }
}

Both the dog lupo and the cat garfield can eat, but only the dog can bark, and only the cat can miaow. We want to put the reusable eat() logic into a common class, first using inheritance, then delegation.

Inheritance

Inheritance represents the "IS A" concept:

  • A cat is an animal

This is categorization, one of the oldest scientific ways of working, closely related to classification, generalization, abstraction.

class Animal
{
    private String eatenFood;
    
    public void eat(String food)   {
        this.eatenFood = food;
    }

    public String getEatenFood() {
        return eatenFood;
    }
}

class Dog extends Animal
{
    public void bark() {
        System.out.println("Wow Wow");
    }
}

class Cat extends Animal
{
    public void miaow()  {
        System.out.println("Miaaaaaow");
    }
}

Both Dog and Cat inherit from Animal, and thus can eat(). A Dog has the special ability to bark(), a Cat the ability to miaow(). This code is short and concise and contains no duplication of any kind.

Delegation

Delegation can represent both the "HAS A" and the "IS A" concept:

  • A cat has an animal (inside, thus it is an animal)

Doesn't sound very intuitive, but is acceptable from a technical perspective, because inheritance is resolved to delegation by the compiler!

class Animal
{
    private String eatenFood;
    
    public void eat(String food)    {
        this.eatenFood = food;
    }
    
    public String getEatenFood()    {
        return eatenFood;
    }
}

class Dog
{
    private Animal animal = new Animal();
    
    public void eat(String food) {
        animal.eat(food);
    }
    
    public String getEatenFood()    {
        return animal.getEatenFood();
    }
    
    public void bark() {
        System.out.println("Wow Wow");
    }
}

class Cat
{
    private Animal animal = new Animal();
    
    public void eat(String food) {
        animal.eat(food);
    }
    
    public String getEatenFood()    {
        return animal.getEatenFood();
    }
    
    public void miaow()  {
        System.out.println("Miaaaaaow");
    }
}

Both Dog and Cat have an Animal within them, but calls to eat() and getEatenFood() have to be forwarded to that delegation object. This is purely technical code that duplicates Animal method signatures. The specific abilities like bark() and miaow() can hardly be seen, because all the delegate methods are public. (Yes, this clutters code!)

When to Use What

Use delegation when having a clear containment hierarchy like "A cat has a tail".

Deriving a LabeledButton from HorizontalLayout just to be able to use the layout methods without having to create a delegate is not worth it. You would expose the HorizontalLayout API to all callers of LabeledButton. Moreover the next release may demand the layout to be switchable to VerticalLayout at runtime.

Only inherit from a class that you want 100% inside your class. There should not be a single public or protected super-method that doesn't fit to your class.

A Stack with push(), pop(), peek() is not expected to inherit from List with add(), remove(), contains(), as such an API would look absurd for a Stack.
Why extend Thread when you just want some logic to be runnable in a separate thread? Most likely you won't even use the inherited Thread methods in your logic. Better implement Runnable and let the caller decide about a thread of its choice.
UI controllers (callback method containers) are a border case. Practice shows that here inheritance leads to very high and incomprehensible hierarchies that end up to be mixins actually.

When you need to reuse more than one class, use delegation, even if one of the super-classes would fit 100% to your class.

When you need to inherit dynamically at runtime, you need to use delegation, because inheritance is compile-time bound.

Use inheritance in any other case, especially when having a clear inheritance hierarchy like "A cat is a mammal". Inheritance is a much better reusage concept than delegation.

We still live in times of manually written source code, and very frequently the source code is the only representation of a business logic that came from experts that are long gone and left no documentation ("we are agile"). Thus we need well readable und quickly understandable source code without duplications, easily maintainable, because maintenance makes up 70% of software production efforts.

Once again I want to remark that inheritance is resolved to delegation at compile time, so why implement delegation when the compiler can do it for you, without typos and code duplication?

Advantages and Disadvantages

Mind that any such list always is driven by the point of view: A compiler engineer may give other pros and cons than a database expert. I am a business developer.

Inheritance

Advantages

  • You effortlessly get whatever the super-class provides, without any code duplication
  • You could fix bugs or customize behavior in a class you have no editing-access to (external frameworks), when inheriting from it and overriding methods
  • No repetitive delegation code clutters the implementation

Disadvantages

  • You can not inherit at runtime, binding to a super-class at runtime normally is not supported
  • Mostly you can not reuse several super-classes at the same time; some languages provide multiple inheritance (→ mixins), but you are going to be ambiguous, misleading and complex when you use that feature
  • Inheritance is a too intimate relationship, meaning the derived class can bring down the super-class by overrides or use of protected fields that are meant to be internal-only; that can be avoided only by accurate and well written access modifiers and field wrappers in the super-class, see chapter below

Delegation

Advantages

  • You can lazily load the delegate
  • You can bind the super-class delegate at runtime
  • You can simulate multiple inheritance

Disadvantages

  • Delegation is code duplication
  • Mostly the delegate needs an interface, so that all delegators can also implement it and forward to the delegate, which gives an additional interface and lots of purely technical method-forwarding in all delegators
  • When the delegate interface changes, you need to change both the delegate and all delegators
  • Without a delegate interface you can be sure that you will forget to change all delegators when the delegate changes!

How to Avoid Inheritance Break Encapsulation

Inheritance is said to be a too intimate relationship. E.g. you could modify non-final protected fields of a super-class, causing unexpected behavior. You could override a method and forget to call super, or do it in a way the super-class did not expect.

To avoid that, do the following in any super-class:

  1. Try to make all fields private, and as many methods as possible private, package-visible, or at least protected (minimize access modifiers)

  2. Make immutable protected fields final

  3. Make mutable protected fields private and a add a protected final getter for it (when necessary), try to avoid the setter

  4. When a private field has protected getter and setter, the field must not be accessed directly (in its class), only by its getter and setter, as they could be overwritten when being non-final

  5. Make all non-private methods called from constructor final, see here

  6. Add assertions to all non-private methods to uncover illegal object states

  7. Make all non-private methods final that are not explicitly meant to be overwritten, or that may endanger the object's state when overwritten

Should be self-evident, but belongs here: public fields always must be final (immutable)!

Conclusion

We need both of them, inheritance AND delegation!

Obviously delegation is the more flexible concept. But with more freedom comes less security, meaning the code is hard to maintain when having those big delegation sections in it.

Absence of abstraction means low quality of both specification and software. Code without abstractions will contain many duplications due to missing code reusage.

Respect business logic and its concepts. When the specification comes with an "IS A" relation, then use inheritance. When it comes with a "HAS A" relation, use delegation. When it comes with neither, tell them to introduce it :-)




Keine Kommentare: