Composition over inheritance
Composition over inheritance (or Composite Reuse Principle) in object-oriented programming 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 Design Patterns: "Favor 'object composition' over 'class inheritance'."[3]
Basics
An implementation of composition over inheritance typically begins with the creation of various interfaces representing the behaviors that the system must exhibit. The use of interfaces allows this technique to support the polymorphic behavior that is so valuable in object-oriented programming. 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. Any business domain class that contains a reference to the interface can easily support any implementation of that interface and the choice can even be delayed until run time.
Example
Inheritance
An example in C++11 follows:
class GameObject
{
public:
virtual ~GameObject() {}
virtual void update() {}
virtual void draw() {}
virtual void collide(Object objects[]) {}
};
class Visible : public GameObject
{
public:
virtual void draw() override { /* draw model at position of this object */ };
private:
Model* model;
};
class Solid : public GameObject
{
public:
virtual void collide(GameObject objects[]) override { /* check and react to collisions with objects */ };
};
class Movable : public GameObject
{
public:
virtual void update() override { /* update position */ };
};
Then, we have concrete classes:
- class
Player
- which isSolid
,Movable
andVisible
- class
Cloud
- which isMovable
andVisible
, but notSolid
- class
Building
- which isSolid
andVisible
, but notMovable
- class
Trap
- which isSolid
, but neitherVisible
norMovable
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 Player();
player.Update();
player.Collide();
player.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.");
}
}
abstract class GameObject : IVisible, IUpdatable, ICollidable {
private readonly IVisible _v;
private readonly IUpdatable _u;
private readonly ICollidable _c;
public GameObject(IVisible visible, IUpdatable updatable, ICollidable collidable) {
_v = visible;
_u = updatable;
_c = collidable;
}
public void Update() {
_u.Update();
}
public void Draw() {
_v.Draw();
}
public void Collide() {
_c.Collide();
}
}
class Player : GameObject {
public Player() : base(new Visible(), new Movable(), new Solid()) { }
}
class Cloud : GameObject {
public Cloud() : base(new Visible(), new Movable(), new NotSolid()) { }
}
class Building : GameObject {
public Building() : base(new Visible(), new NotMovable(), new Solid()) { }
}
class Trap : GameObject {
public Trap() : base(new Invisible(), new NotMovable(), new Solid()) { }
}
Benefits
To favor composition over inheritance is a design principle that gives the design higher flexibility, giving business-domain classes and more stable business domain in the long term. In other words, HAS-A can be better than an IS-A relationship.[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.
Drawbacks
One drawback of using composition instead of inheritance is that all of the methods being provided by the composed classes must be implemented in the derived class, even if they are only forwarding methods. 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 need 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 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 class Employee
{
// Member variables
protected string m_name;
protected int m_id;
protected decimal m_payRate;
protected int m_hoursWorked;
// Get/set the employee's name
public string Name
{
get { return m_name; }
set { m_name = value; }
}
// Get/set the employee's ID
public int ID
{
get { return m_id; }
set { m_id = value; }
}
// Get/set the employee's pay rate
public decimal PayRate
{
get { return m_payRate; }
set { m_payRate = value; }
}
// Get hours worked in the current pay period
public int HoursWorked()
{
return m_hoursWorked;
}
// Get pay for the current pay period
abstract public decimal Pay();
}
// Derived subclass
public HourlyEmployee: Employee
{
// Get pay for the current pay period
public decimal Pay()
{
// Time worked is in hours
return m_hoursWorked * m_payRate;
}
}
// Derived subclass
public SalariedEmployee: Employee
{
// Get pay for the current pay period
public decimal Pay()
{
// Pay rate is annual salary instead of hourly rate
return m_hoursWorked * m_payRate/2087;
}
}
This drawback can be avoided by using traits or mixins. Some languages, such as Perl 6, provide a handles
keyword to facilitate method forwarding. In Java, Project Lombok lets you implement delegation using a single @Delegate annotation on the field instead of copying and maintaining names and types of all methods from the delegated field.
Empirical studies of composition and inheritance
A 2013 study of 93 open source Java programs (of varying size) found that
While there is [no] huge 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"[4]
See also
- Delegation pattern
- Liskov substitution principle
- Object-oriented design
- Role-oriented programming
- State pattern
- Strategy pattern
References
- 1 2 Freeman, Eric; Freeman, Elisabeth; Sierra, Kathy; Bates, Bert (2004). Hendrickson, Mike; Loukides, Mike, eds. "Head First Design Patterns" (paperback) 1. O'REILLY: 23. ISBN 978-0-596-00712-6.
- ↑ Kirk Knoernschild (2002). Java Design - Objects, UML, and Process: 1.1.5 Composite Reuse Principle (CRP). Addison-Wesley Inc. Retrieved 2012-05-29.
- ↑ Gamma et al. 1994, p. 20.
- ↑ Tempero, Ewan; Yang, Hong Yul; Noble, James (2013). What programmers do with inheritance in Java (PDF). ECOOP 2013–Object-Oriented Programming. pp. 577–601.
- Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. ISBN 0-201-63361-2. OCLC 31171684.