Composition over inheritance

Composition over inheritance (or composite reuse principle) in object-oriented programming (OOP) is the principle that classes should achieve polymorphic behavior and code reuse by their composition (by containing instances of other classes that implement the desired functionality) rather than inheritance from a base or parent class.[2] This is an often-stated principle of OOP, such as in the influential book Design Patterns.[3]

This diagram shows how the fly and sound behavior of an animal can be designed in a flexible way by using the composition over inheritance design principle.[1]

Basics

An implementation of composition over inheritance typically begins with the creation of various interfaces representing the behaviors that the system must exhibit. Interfaces enable polymorphic behavior. Classes implementing the identified interfaces are built and added to business domain classes as needed. Thus, system behaviors are realized without inheritance.

In fact, business domain classes may all be base classes without any inheritance at all. Alternative implementation of system behaviors is accomplished by providing another class that implements the desired behavior interface. A class that contains a reference to an interface can support implementations of the interface - a choice that can be delayed until run time.

Example

Inheritance

An example in C++11 follows:

#include <vector>

class Model;

class GameObject {
 public:
  virtual ~GameObject() = default;

  virtual void Update() {}
  virtual void Draw() {}
  virtual void Collide(const std::vector<GameObject*>& objects) {}
};

class Visible : public GameObject {
 public:
  void Draw() override {
    // Draw |model_| at position of this object.
  };

 private:
  Model* model_;
};

class Solid : public GameObject {
 public:
  void Collide(const std::vector<GameObject*>& objects) override {
    // Check and react to collisions with |objects|.
  };
};

class Movable : public GameObject {
 public:
  void Update() override {
    // Update position.
  };
};

Then, suppose we also have these concrete classes:

  • class Player - which is Solid, Movable and Visible
  • class Cloud - which is Movable and Visible, but not Solid
  • class Building - which is Solid and Visible, but not Movable
  • class Trap - which is Solid, but neither Visible nor Movable

Note that multiple inheritance is dangerous if not implemented carefully, as it can lead to the diamond problem. One solution to avoid this is to create classes such as VisibleAndSolid, VisibleAndMovable, VisibleAndSolidAndMovable, etc. for every needed combination, though this leads to a large amount of repetitive code. Keep in mind that C++ solves the diamond problem of multiple inheritance by allowing virtual inheritance.

Composition and interfaces

The following C# example demonstrates the principle of using composition and interfaces to achieve code reuse and polymorphism.

class Program {
    static void Main() {
        var player = new GameObject(new Visible(), new Movable(), new Solid());
        player.Update(); player.Collide(); player.Draw();

        var cloud = new GameObject(new Visible(), new Movable(), new NotSolid());
        cloud.Update(); cloud.Collide(); cloud.Draw();

        var building = new GameObject(new Visible(), new NotMovable(), new Solid());
        building.Update(); building.Collide(); building.Draw();

        var trap = new GameObject(new Invisible(), new NotMovable(), new Solid());
        trap.Update(); trap.Collide(); trap.Draw();
    }
}

interface IVisible {
    void Draw();
}

class Invisible : IVisible {
    public void Draw() {
        Console.Write("I won't appear.");
    }
}

class Visible : IVisible {
    public void Draw() {
        Console.Write("I'm showing myself.");
    }
}

interface ICollidable {
    void Collide();
}

class Solid : ICollidable {
    public void Collide() {
        Console.Write("Bang!");
    }
}

class NotSolid : ICollidable {
    public void Collide() {
        Console.Write("Splash!");
    }
}

interface IUpdatable {
    void Update();
}

class Movable : IUpdatable {
    public void Update() {
        Console.Write("Moving forward.");
    }
}

class NotMovable : IUpdatable {
    public void Update() {
        Console.Write("I'm staying put.");
    }
}

class GameObject : IVisible, IUpdatable, ICollidable {
    private readonly IVisible _visible;
    private readonly IUpdatable _updatable;
    private readonly ICollidable _collidable;

    public GameObject(IVisible visible, IUpdatable updatable, ICollidable collidable) {
        _visible = visible;
        _updatable = updatable;
        _collidable = collidable;
    }

    public void Update() {
        _updatable.Update();
    }

    public void Draw() {
        _visible.Draw();
    }

    public void Collide() {
        _collidable.Collide();
    }
}

Benefits

To favor composition over inheritance is a design principle that gives the design higher flexibility. It is more natural to build business-domain classes out of various components than trying to find commonality between them and creating a family tree. For example, an accelerator pedal and a steering wheel share very few common traits, yet both are vital components in a car. What they can do and how they can be used to benefit the car is easily defined. Composition also provides a more stable business domain in the long term as it is less prone to the quirks of the family members. In other words, it is better to compose what an object can do (HAS-A) than extend what it is (IS-A).[1]

Initial design is simplified by identifying system object behaviors in separate interfaces instead of creating a hierarchical relationship to distribute behaviors among business-domain classes via inheritance. This approach more easily accommodates future requirements changes that would otherwise require a complete restructuring of business-domain classes in the inheritance model. Additionally, it avoids problems often associated with relatively minor changes to an inheritance-based model that includes several generations of classes.

Some languages, notably Go, use type composition exclusively.[4]

Drawbacks

One common drawback of using composition instead of inheritance is that methods being provided by individual components may have to be implemented in the derived type, even if they are only forwarding methods (this is true in most programming languages, but not all; see Avoiding drawbacks.) In contrast, inheritance does not require all of the base class's methods to be re-implemented within the derived class. Rather, the derived class only needs to implement (override) the methods having different behavior than the base class methods. This can require significantly less programming effort if the base class contains many methods providing default behavior and only a few of them need to be overridden within the derived class.

For example, in the C# code below, the variables and methods of the Employee base class are inherited by the HourlyEmployee and SalariedEmployee derived subclasses. Only the Pay() method needs to be implemented (specialized) by each derived subclass. The other methods are implemented by the base class itself, and are shared by all of its derived subclasses; they do not need to be re-implemented (overridden) or even mentioned in the subclass definitions.

// Base class
public abstract class Employee
{
    // Properties
    protected string Name { get; set; }
    protected int ID { get; set; }
    protected decimal PayRate { get; set; }
    protected int HoursWorked { get; }

    // Get pay for the current pay period
    public abstract decimal Pay();
}

// Derived subclass
public class HourlyEmployee : Employee
{
    // Get pay for the current pay period
    public override decimal Pay()
    {
        // Time worked is in hours
        return HoursWorked * PayRate;
    }
}

// Derived subclass
public class SalariedEmployee : Employee
{
    // Get pay for the current pay period
    public override decimal Pay()
    {
        // Pay rate is annual salary instead of hourly rate
        return HoursWorked * PayRate / 2087;
    }
}

Avoiding drawbacks

This drawback can be avoided by using traits, mixins, (type) embedding, or protocol extensions.

Some languages provide specific means to mitigate this:

  • Raku provides a handles keyword to facilitate method forwarding.
  • Java provides Project Lombok[5] which allows delegation to be implemented using a single @Delegate annotation on the field, instead of copying and maintaining the names and types of all the methods from the delegated field.[6]. Java 8 allows for default methods in an interface, similar to C# etc.
  • Julia macros can be used to generate forwarding methods. Several implementations exist such as Lazy.jl and TypedDelegation.jl.
  • Swift extensions can be used to define a default implementation of a protocol on the protocol itself, rather than within an individual type's implementation.[7]
  • Kotlin includes the delegation pattern in the language syntax.[8]
  • Go type embedding avoids the need for forwarding methods.[9]
  • D provides an explicit "alias this" declaration within a type can forward into it every method and member of another contained type. [10]
  • C# provides default interface methods since 8.0 version which allows to define body to interface member. [11]
  • Rust provides traits with default implementations.

Empirical studies

A 2013 study of 93 open source Java programs (of varying size) found that:

While there is not huge [sic] opportunity to replace inheritance with composition (...), the opportunity is significant (median of 2% of uses [of inheritance] are only internal reuse, and a further 22% are only external or internal reuse). Our results suggest there is no need for concern regarding abuse of inheritance (at least in open-source Java software), but they do highlight the question regarding use of composition versus inheritance. If there are significant costs associated with using inheritance when composition could be used, then our results suggest there is some cause for concern.

Tempero et al., "What programmers do with inheritance in Java"[12]

See also

References

  1. Freeman, Eric; Robson, Elisabeth; Sierra, Kathy; Bates, Bert (2004). Head First Design Patterns. O'Reilly. p. 23. ISBN 978-0-596-00712-6.
  2. Knoernschild, Kirk (2002). Java Design - Objects, UML, and Process: 1.1.5 Composite Reuse Principle (CRP). Addison-Wesley Inc. Retrieved 2012-05-29.
  3. Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. p. 20. ISBN 0-201-63361-2. OCLC 31171684.
  4. Pike, Rob (2012-06-25). "Less is exponentially more". Retrieved 2016-10-01.
  5. http://projectlombok.org/
  6. "@Delegate". Project Lombok. Retrieved 2018-07-11.
  7. "Protocols". The Swift Programming Language. Apple Inc. Retrieved 2018-07-11.
  8. "Delegated Properties". Kotlin Reference. JetBrains. Retrieved 2018-07-11.
  9. "(Type) Embedding". The Go Programming Language Documentation. Google. Retrieved 2019-05-10.
  10. "Alias This". D Language Reference. Retrieved 2019-06-15.
  11. "What's new in C# 8.0". Microsoft Docs. Microsoft. Retrieved 2019-02-20.
  12. Tempero, Ewan; Yang, Hong Yul; Noble, James (2013). What programmers do with inheritance in Java (PDF). ECOOP 2013–Object-Oriented Programming. pp. 577–601.
This article is issued from Wikipedia. The text is licensed under Creative Commons - Attribution - Sharealike. Additional terms may apply for the media files.