Английская Википедия:Composition over inheritance
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]
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:
class Object
{
public:
virtual void update() {
// no-op
}
virtual void draw() {
// no-op
}
virtual void collide(Object objects[]) {
// no-op
}
};
class Visible : public Object
{
Model* model;
public:
virtual void draw() override {
// code to draw a model at the position of this object
}
};
class Solid : public Object
{
public:
virtual void collide(Object objects[]) override {
// code to check for and react to collisions with other objects
}
};
class Movable : public Object
{
public:
virtual void update() override {
// code to update the position of this object
}
};
Then, suppose we also have these concrete classes:
- class Шаблон:Code - which is Шаблон:Code, Шаблон:Code and Шаблон:Code
- class Шаблон:Code - which is Шаблон:Code and Шаблон:Code, but not Шаблон:Code
- class Шаблон:Code - which is Шаблон:Code and Шаблон:Code, but not Шаблон:Code
- class Шаблон:Code - which is Шаблон:Code, but neither Шаблон:Code nor Шаблон:Code
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 Шаблон:Code, Шаблон:Code, Шаблон:Code, 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]Шаблон:Rp and C#.[5]Шаблон:Rp
Introduce an abstract class named Шаблон:Code, with the subclasses Шаблон:Code and Шаблон:Code, which provides a means of drawing an object:
class VisibilityDelegate
{
public:
virtual void draw() = 0;
};
class NotVisible : public VisibilityDelegate
{
public:
virtual void draw() override {
// no-op
}
};
class Visible : public VisibilityDelegate
{
public:
virtual void draw() override {
// code to draw a model at the position of this object
}
};
Introduce an abstract class named Шаблон:Code, with the subclasses Шаблон:Code and Шаблон:Code, which provides a means of moving an object:
class UpdateDelegate
{
public:
virtual void update() = 0;
};
class NotMovable : public UpdateDelegate
{
public:
virtual void update() override {
// no-op
}
};
class Movable : public UpdateDelegate
{
public:
virtual void update() override {
// code to update the position of this object
}
};
Introduce an abstract class named Шаблон:Code, with the subclasses Шаблон:Code and Шаблон:Code, which provides a means of colliding with an object:
class CollisionDelegate
{
public:
virtual void collide(Object objects[]) = 0;
};
class NotSolid : public CollisionDelegate
{
public:
virtual void collide(Object objects[]) override {
// no-op
}
};
class Solid : public CollisionDelegate
{
public:
virtual void collide(Object objects[]) override {
// code to check for and react to collisions with other objects
}
};
Finally, introduce a class named Шаблон:Code with members to control its visibility (using a Шаблон:Code), movability (using an Шаблон:Code), and solidity (using a Шаблон:Code). This class has methods which delegate to its members, e.g. Шаблон:Code simply calls a method on the Шаблон:Code:
class Object
{
VisibilityDelegate* _v;
UpdateDelegate* _u;
CollisionDelegate* _c;
public:
Object(VisibilityDelegate* v, UpdateDelegate* u, CollisionDelegate* c)
: _v(v)
, _u(u)
, _c(c)
{}
void update() {
_u->update();
}
void draw() {
_v->draw();
}
void collide(Object objects[]) {
_c->collide(objects);
}
};
Then, concrete classes would look like:
class Player : public Object
{
public:
Player()
: Object(new Visible(), new Movable(), new Solid())
{}
// ...
};
class Smoke : public Object
{
public:
Smoke()
: Object(new Visible(), new Movable(), new NotSolid())
{}
// ...
};
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 Шаблон:Section link). 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 Шаблон:Code base class are inherited by the Шаблон:Code and Шаблон:Code derived subclasses. Only the Шаблон:Code 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:
- C# provides default interface methods since version 8.0 which allows to define body to interface member.[8][5]Шаблон:Rp[9]Шаблон:Rp[10]Шаблон:Rp
- D provides an explicit "alias this" declaration within a type can forward into it every method and member of another contained type.[11]
- Dart provides mixins with default implementations that can be shared.
- Go type embedding avoids the need for forwarding methods.[12]
- Java provides default interface methods since version 8.[4]Шаблон:Rp Project Lombok[13] supports delegation using the Шаблон:Code annotation on the field, instead of copying and maintaining the names and types of all the methods from the delegated field.[14]
- Julia macros can be used to generate forwarding methods. Several implementations exist such as Lazy.jl[15] and TypedDelegation.jl.[16][17]
- Kotlin includes the delegation pattern in the language syntax.[18]
- PHP supports traits, since PHP 5.4.[19]
- Raku provides a Шаблон:Code trait to facilitate method forwarding.[20]
- Rust provides traits with default implementations.
- Scala (since version 3) provides an "export" clause to define aliases for selected members of an object.[21]
- 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.[22]
Empirical studies
A 2013 study of 93 open source Java programs (of varying size) found that:
See also
- Delegation pattern
- Liskov substitution principle
- Object-oriented design
- Object composition
- Role-oriented programming
- State pattern
- Strategy pattern
References
- ↑ 1,0 1,1 Шаблон:Cite book
- ↑ Шаблон:Cite book
- ↑ Шаблон:Cite book
- ↑ 4,0 4,1 Шаблон:Cite book
- ↑ 5,0 5,1 Шаблон:Cite book
- ↑ Шаблон:Cite web
- ↑ Шаблон:Cite web
- ↑ Шаблон:Cite web
- ↑ Шаблон:Cite book
- ↑ Шаблон:Cite book
- ↑ Шаблон:Cite web
- ↑ Шаблон:Cite web
- ↑ https://projectlombok.org
- ↑ Шаблон:Cite web
- ↑ https://github.com/MikeInnes/Lazy.jl
- ↑ https://github.com/JeffreySarnoff/TypedDelegation.jl
- ↑ Шаблон:Cite web
- ↑ Шаблон:Cite web
- ↑ Шаблон:Cite web
- ↑ Шаблон:Cite web
- ↑ Шаблон:Cite web
- ↑ Шаблон:Cite web
- Английская Википедия
- Страницы с неработающими файловыми ссылками
- Component-based software engineering
- Software architecture
- Programming principles
- Articles with example C Sharp code
- Страницы, где используется шаблон "Навигационная таблица/Телепорт"
- Страницы с телепортом
- Википедия
- Статья из Википедии
- Статья из Английской Википедии