В мире ООП существует два вида создания одних типов из других: наследование и композиция. Композиция подразумевает хранение объектов других типов в создаваемом.
Преимуществом композиции над наследованием в рамках создания типов является позднее связывание. Если наследование “прибивает” тип к его родителю, делая логику, реализованную в подтипах более трудной к переиспользованию, создание типов через композицию даёт возможность динамически подменять реализацию использованной логики (в зависимости от вида).
Выделяют два вида композиции: агрегация и ассоциация. Различаются они в способе получения композируемых объектов. Агрегация подразумевает получение объектов извне (в качестве параметров конструктора), тогда как при ассоциации объекты создаются непосредственно в конструкторе.
public class Model
{
private readonly IDependency _first;
private readonly IOtherDependency _second;
public Model(IDependency first)
{
_first = first; // Агрегация
_second = new ConcreteOtherDependency(); // Ассоциация
}
}
Композицию наследованию стоит предпочесть в случаях, когда типы связываются для переиспользования логики. В целом, наследование стоит использовать тогда, когда сам интерфейс типа подразумевает полиморфизм, тогда, когда реализация логики меняется от подтипа к подтипу.
Хорошим примером неверного использования наследования является паттерн Фабричный Метод. Сама логика в базовом типе - не полиморфна, мы используем наследование, чтобы переиспользовать бизнес логику реализованную в базовом классе, полиморфна лишь логика создания объектов. Решением такой проблемы является паттерн Абстрактная Фабрика. При её использовании мы выделяем фабричные методы в отдельную абстракцию, и используем её как агрегированный объект в классе, которым при Фабричном Методе был базовым. Таким образом мы избежали сильной связанности типов и поддержали SRP.
Композиция > наследования
Использование композиции поощряет SRP, потому как проектируемые модули будут инкапсулировать связную логику, не имея за собой багажа базовых типов. Соответсвенно код становится более модульным, и в дальнейшем проще проектировать более гибкие реализации, композируя отдельные модули, нужные для выполнения конкретной задачи.