Composition over inheritance

Last updated
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. UML diagram of composition over inheritance.svg
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.

Composition over inheritance (or composite reuse principle) in object-oriented programming (OOP) is the principle that classes should favor polymorphic behavior and code reuse by their composition (by containing instances of other classes that implement the desired functionality) over inheritance from a base or parent class. [2] Ideally all reuse can be achieved by assembling existing components, but in practice inheritance is often needed to make new ones. Therefore inheritance and object composition typically work hand-in-hand, as discussed in the book Design Patterns (1994). [3]

Contents

Basics

An implementation of composition over inheritance typically begins with the creation of various interfaces representing the behaviors that the system must exhibit. Interfaces can facilitate 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 runtime.

Example

Inheritance

An example in C++ follows:

classObject{public:virtualvoidupdate(){// no-op}virtualvoiddraw(){// no-op}virtualvoidcollide(Objectobjects[]){// no-op}};classVisible:publicObject{Model*model;public:virtualvoiddraw()override{// code to draw a model at the position of this object}};classSolid:publicObject{public:virtualvoidcollide(Objectobjects[])override{// code to check for and react to collisions with other objects}};classMovable:publicObject{public:virtualvoidupdate()override{// code to update the position of this object}};

Then, suppose we also have these concrete classes:

Note that multiple inheritance is dangerous if not implemented carefully because it can lead to the diamond problem. One solution to this is to create classes such as VisibleAndSolid, VisibleAndMovable, VisibleAndSolidAndMovable, etc. for every needed combination; however, this leads to a large amount of repetitive code. C++ uses virtual inheritance to solve the diamond problem of multiple inheritance.

Composition and interfaces

The C++ examples in this section demonstrate the principle of using composition and interfaces to achieve code reuse and polymorphism. Due to the C++ language not having a dedicated keyword to declare interfaces, the following C++ example uses inheritance from a pure abstract base class. For most purposes, this is functionally equivalent to the interfaces provided in other languages, such as Java [4] :87 and C#. [5] :144

Introduce an abstract class named VisibilityDelegate, with the subclasses NotVisible and Visible, which provides a means of drawing an object:

classVisibilityDelegate{public:virtualvoiddraw()=0;};classNotVisible:publicVisibilityDelegate{public:virtualvoiddraw()override{// no-op}};classVisible:publicVisibilityDelegate{public:virtualvoiddraw()override{// code to draw a model at the position of this object}};

Introduce an abstract class named UpdateDelegate, with the subclasses NotMovable and Movable, which provides a means of moving an object:

classUpdateDelegate{public:virtualvoidupdate()=0;};classNotMovable:publicUpdateDelegate{public:virtualvoidupdate()override{// no-op}};classMovable:publicUpdateDelegate{public:virtualvoidupdate()override{// code to update the position of this object}};

Introduce an abstract class named CollisionDelegate, with the subclasses NotSolid and Solid, which provides a means of colliding with an object:

classCollisionDelegate{public:virtualvoidcollide(Objectobjects[])=0;};classNotSolid:publicCollisionDelegate{public:virtualvoidcollide(Objectobjects[])override{// no-op}};classSolid:publicCollisionDelegate{public:virtualvoidcollide(Objectobjects[])override{// code to check for and react to collisions with other objects}};

Finally, introduce a class named Object with members to control its visibility (using a VisibilityDelegate), movability (using an UpdateDelegate), and solidity (using a CollisionDelegate). This class has methods which delegate to its members, e.g. update() simply calls a method on the UpdateDelegate:

classObject{VisibilityDelegate*_v;UpdateDelegate*_u;CollisionDelegate*_c;public:Object(VisibilityDelegate*v,UpdateDelegate*u,CollisionDelegate*c):_v(v),_u(u),_c(c){}voidupdate(){_u->update();}voiddraw(){_v->draw();}voidcollide(Objectobjects[]){_c->collide(objects);}};

Then, concrete classes would look like:

classPlayer:publicObject{public:Player():Object(newVisible(),newMovable(),newSolid()){}// ...};classSmoke:publicObject{public:Smoke():Object(newVisible(),newMovable(),newNotSolid()){}// ...};

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 are 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. Composition relation is more flexible as it may be changed on runtime, while sub-typing relations are static and need recompilation in many languages.

Some languages, notably Go [6] and Rust, [7] use type composition exclusively.

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.

UML class Employee.svg

// Base classpublicabstractclassEmployee{// PropertiesprotectedstringName{get;set;}protectedintID{get;set;}protecteddecimalPayRate{get;set;}protectedintHoursWorked{get;}// Get pay for the current pay periodpublicabstractdecimalPay();}// Derived subclasspublicclassHourlyEmployee:Employee{// Get pay for the current pay periodpublicoverridedecimalPay(){// Time worked is in hoursreturnHoursWorked*PayRate;}}// Derived subclasspublicclassSalariedEmployee:Employee{// Get pay for the current pay periodpublicoverridedecimalPay(){// Pay rate is annual salary instead of hourly ratereturnHoursWorked*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:

Empirical studies

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

While there is not 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" [23]

See also

Related Research Articles

In object-oriented programming, a class is an extensible program-code-template for creating objects, providing initial values for state and implementations of behavior. In many languages, the class name is used as the name for the class, the name for the default constructor of the class, and as the type of objects generated by instantiating the class; these distinct concepts are easily conflated. Although, to the point of conflation, one could argue that is a feature inherent in a language because of its polymorphic nature and why these languages are so powerful, dynamic and adaptable for use compared to languages without polymorphism present. Thus they can model dynamic systems more easily.

Multiple inheritance is a feature of some object-oriented computer programming languages in which an object or class can inherit features from more than one parent object or parent class. It is distinct from single inheritance, where an object or class may only inherit from one particular object or class.

A visitor pattern is a software design pattern and separates the algorithm from the object structure. Because of this separation new operations can be added to existing object structures without modifying the structures. It is one way to follow the open/closed principle in object-oriented programming and software engineering.

<i>Design Patterns</i> 1994 software engineering book

Design Patterns: Elements of Reusable Object-Oriented Software (1994) is a software engineering book describing software design patterns. The book was written by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, with a foreword by Grady Booch. The book is divided into two parts, with the first two chapters exploring the capabilities and pitfalls of object-oriented programming, and the remaining chapters describing 23 classic software design patterns. The book includes examples in C++ and Smalltalk.

The bridge pattern is a design pattern used in software engineering that is meant to "decouple an abstraction from its implementation so that the two can vary independently", introduced by the Gang of Four. The bridge uses encapsulation, aggregation, and can use inheritance to separate responsibilities into different classes.

In class-based programming, the factory method pattern is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created. This is done by creating objects by calling a factory method—either specified in an interface and implemented by child classes, or implemented in a base class and optionally overridden by derived classes—rather than by calling a constructor.

In software engineering, the composite pattern is a partitioning design pattern. The composite pattern describes a group of objects that are treated the same way as a single instance of the same type of object. The intent of a composite is to "compose" objects into tree structures to represent part-whole hierarchies. Implementing the composite pattern lets clients treat individual objects and compositions uniformly.

In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. The decorator pattern is often useful for adhering to the Single Responsibility Principle, as it allows functionality to be divided between classes with unique areas of concern as well as to the Open-Closed Principle, by allowing the functionality of a class to be extended without being modified. Decorator use can be more efficient than subclassing, because an object's behavior can be augmented without defining an entirely new object.

In computer programming, the strategy pattern is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.

Multiple dispatch or multimethods is a feature of some programming languages in which a function or method can be dynamically dispatched based on the run-time (dynamic) type or, in the more general case, some other attribute of more than one of its arguments. This is a generalization of single-dispatch polymorphism where a function or method call is dynamically dispatched based on the derived type of the object on which the method has been called. Multiple dispatch routes the dynamic dispatch to the implementing function or method using the combined characteristics of one or more arguments.

A method in object-oriented programming (OOP) is a procedure associated with an object, and generally also a message. An object consists of state data and behavior; these compose an interface, which specifies how the object may be used. A method is a behavior of an object parametrized by a user.

In object-oriented programming languages, a mixin is a class that contains methods for use by other classes without having to be the parent class of those other classes. How those other classes gain access to the mixin's methods depends on the language. Mixins are sometimes described as being "included" rather than "inherited".

The fragile base class problem is a fundamental architectural problem of object-oriented programming systems where base classes (superclasses) are considered "fragile" because seemingly safe modifications to a base class, when inherited by the derived classes, may cause the derived classes to malfunction. The programmer cannot determine whether a base class change is safe simply by examining in isolation the methods of the base class.

In object-oriented programming, in languages such as C++, and Object Pascal, a virtual function or virtual method is an inheritable and overridable function or method for which dynamic dispatch is facilitated. This concept is an important part of the (runtime) polymorphism portion of object-oriented programming (OOP). In short, a virtual function defines a target function to be executed, but the target might not be known at compile time.

In object-oriented programming, delegation refers to evaluating a member of one object in the context of another original object. Delegation can be done explicitly, by passing the sending object to the receiving object, which can be done in any object-oriented language; or implicitly, by the member lookup rules of the language, which requires language support for the feature. Implicit delegation is the fundamental method for behavior reuse in prototype-based programming, corresponding to inheritance in class-based programming. The best-known languages that support delegation at the language level are Self, which incorporates the notion of delegation through its notion of mutable parent slots that are used upon method lookup on self calls, and JavaScript; see JavaScript delegation.

<span class="mw-page-title-main">Method overriding</span> Language feature in object-oriented programming

Method overriding, in object-oriented programming, is a language feature that allows a subclass or child class to provide a specific implementation of a method that is already provided by one of its superclasses or parent classes. In addition to providing data-driven algorithm-determined parameters across virtual network interfaces, it also allows for a specific type of polymorphism (subtyping). The implementation in the subclass overrides (replaces) the implementation in the superclass by providing a method that has same name, same parameters or signature, and same return type as the method in the parent class. The version of a method that is executed will be determined by the object that is used to invoke it. If an object of a parent class is used to invoke the method, then the version in the parent class will be executed, but if an object of the subclass is used to invoke the method, then the version in the child class will be executed. This helps in preventing problems associated with differential relay analytics which would otherwise rely on a framework in which method overriding might be obviated. Some languages allow a programmer to prevent a method from being overridden.

In object-oriented programming, inheritance is the mechanism of basing an object or class upon another object or class, retaining similar implementation. Also defined as deriving new classes from existing ones such as super class or base class and then forming them into a hierarchy of classes. In most class-based object-oriented languages like C++, an object created through inheritance, a "child object", acquires all the properties and behaviors of the "parent object", with the exception of: constructors, destructors, overloaded operators and friend functions of the base class. Inheritance allows programmers to create classes that are built upon existing classes, to specify a new implementation while maintaining the same behaviors, to reuse code and to independently extend original software via public classes and interfaces. The relationships of objects or classes through inheritance give rise to a directed acyclic graph.

In computer programming, a trait is a concept used in programming languages which represents a set of methods that can be used to extend the functionality of a class.

An interface in the Java programming language is an abstract type that is used to declare a behavior that classes must implement. They are similar to protocols. Interfaces are declared using the interface keyword, and may only contain method signature and constant declarations. All methods of an Interface do not contain implementation as of all versions below Java 8. Starting with Java 8, default and static methods may have implementation in the interface definition. Then, in Java 9, private and private static methods were added. At present, a Java interface can have up to six different types.

In object-oriented programming, forwarding means that using a member of an object results in actually using the corresponding member of a different object: the use is forwarded to another object. Forwarding is used in a number of design patterns, where some members are forwarded to another object, while others are handled by the directly used object. The forwarding object is frequently called a wrapper object, and explicit forwarding members are called wrapper functions.

References

  1. 1 2 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. ISBN   9780201750447 . 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. 1 2 Bloch, Joshua (2018). "Effective Java: Programming Language Guide" (third ed.). Addison-Wesley. ISBN   978-0134685991.
  5. 1 2 Price, Mark J. C# 8.0 and .NET Core 3.0 – Modern Cross-Platform Development: Build Applications with C#, .NET Core, Entity Framework Core, ASP.NET Core, and ML.NET Using Visual Studio Code. Packt. ISBN   978-1-098-12195-2.
  6. Pike, Rob (2012-06-25). "Less is exponentially more" . Retrieved 2016-10-01.
  7. "Characteristics of Object-Oriented Languages - The Rust Programming Language". doc.rust-lang.org. Retrieved 2022-10-10.
  8. "What's new in C# 8.0". Microsoft Docs. Microsoft. Retrieved 2019-02-20.
  9. Skeet, Jon. C# in Depth. Manning. ISBN   978-1617294532.
  10. Albahari, Joseph. C# 10 in a Nutshell. O'Reilly. ISBN   978-1-098-12195-2.
  11. "Alias This". D Language Reference. Retrieved 2019-06-15.
  12. "(Type) Embedding". The Go Programming Language Documentation. Retrieved 2019-05-10.
  13. https://projectlombok.org
  14. "@Delegate". Project Lombok. Retrieved 2018-07-11.
  15. https://github.com/MikeInnes/Lazy.jl
  16. https://github.com/JeffreySarnoff/TypedDelegation.jl
  17. "Method forwarding macro". JuliaLang. 20 April 2019. Retrieved 18 August 2022.
  18. "Delegated Properties". Kotlin Reference. JetBrains. Retrieved 2018-07-11.
  19. "PHP: Traits". www.php.net. Retrieved 23 February 2023.
  20. "Type system". docs.raku.org. Retrieved 18 August 2022.
  21. "Export Clauses". Scala Documentation. Retrieved 2021-10-06.
  22. "Protocols". The Swift Programming Language. Apple Inc. Retrieved 2018-07-11.
  23. Tempero, Ewan; Yang, Hong Yul; Noble, James (2013). "What programmers do with inheritance in Java" (PDF). ECOOP 2013 – Object-Oriented Programming. ECOOP 2013–Object-Oriented Programming. Lecture Notes in Computer Science. Vol. 7920. pp. 577–601. doi:10.1007/978-3-642-39038-8_24. ISBN   978-3-642-39038-8.