В мире ООП существует два вида создания одних типов из других: наследование и композиция. Композиция подразумевает хранение объектов других типов в создаваемом.

Преимуществом композиции над наследованием в рамках создания типов является позднее связывание. Если наследование “прибивает” тип к его родителю, делая логику, реализованную в подтипах более трудной к переиспользованию, создание типов через композицию даёт возможность динамически подменять реализацию использованной логики (в зависимости от вида).

Выделяют два вида композиции: агрегация и ассоциация. Различаются они в способе получения композируемых объектов. Агрегация подразумевает получение объектов извне (в качестве параметров конструктора), тогда как при ассоциации объекты создаются непосредственно в конструкторе.

public class Model
{
		private readonly IDependency _first;
		private readonly IOtherDependency _second;

		public Model(IDependency first)
		{
				_first = first; // Агрегация 
				_second = new ConcreteOtherDependency(); // Ассоциация 
		}
}

Когда что использовать?

Композицию наследованию стоит предпочесть в случаях, когда типы связываются для переиспользования логики. В целом, наследование стоит использовать тогда, когда сам интерфейс типа подразумевает полиморфизм, тогда, когда реализация логики меняется от подтипа к подтипу.

Хорошим примером неверного использования наследования является паттерн Фабричный Метод. Сама логика в базовом типе - не полиморфна, мы используем наследование, чтобы переиспользовать бизнес логику реализованную в базовом классе, полиморфна лишь логика создания объектов. Решением такой проблемы является паттерн Абстрактная Фабрика. При её использовании мы выделяем фабричные методы в отдельную абстракцию, и используем её как агрегированный объект в классе, которым при Фабричном Методе был базовым. Таким образом мы избежали сильной связанности типов и поддержали SRP.

Композиция > наследования

Использование композиции поощряет SRP, потому как проектируемые модули будут инкапсулировать связную логику, не имея за собой багажа базовых типов. Соответсвенно код становится более модульным, и в дальнейшем проще проектировать более гибкие реализации, композируя отдельные модули, нужные для выполнения конкретной задачи.