Автор: Sergey Teplyakov
Между двумя классами/объектами существует разные типы отношений. Самым базовым типом отношений является ассоциация (association), это означает, что два класса как-то связаны между собой, и мы пока не знаем точно, в чем эта связь выражена и собираемся уточнить ее в будущем. Обычно это отношение используется на ранних этапах дизайна, чтобы показать, что зависимость между классами существует, и двигаться дальше.
Рисунок 1. Отношение ассоциации
Более точным типом отношений является отношение открытого наследования (отношение является, IS A Relationship), которое говорит, что все, что справедливо для базового класса справедливо и для его наследника. Именно с его помощью мы получаем полиморфное поведение, абстрагируемся от конкретной реализации классов, имея дело лишь с абстракциями (интерфейсами или базовыми классами) и не обращаем внимание на детали реализации.
Рисунок 2. Отношение наследование
И хотя наследование является отличным инструментом в руках любого ОО-программиста, его явно недостаточно для решения всех типов задач. Во-первых, далеко не все отношения между классами определяются отношением является, а во-вторых, наследование является самой сильной связью между двумя классами, которую невозможно разорвать во время исполнения (это отношение является статическим и, в строготипизированных языках определяется во время компиляции).
В этом случае нам на помощь приходит другая пара отношений: композиция (composition) и агрегация (aggregation). Оба они моделируют отношение является частью (HAS-A Relationship) и обычно выражаются в том, что класс целого содержит поля (или свойства) своих составных частей. Грань между ними достаточно тонкая, но важная, особенно в контексте управления зависимостями.
Рисунок 3. Отношение композиции и агрегации
HINTПара моментов, чтобы легче запомнить визуальную нотацию: (1) ромбик всегда находится со стороны целого, а простая линия со стороны составной части, (2) закрашенный ромб означает более сильную связь композицию, незакрашенный ромб показывает более слабую связь агрегацию.
Разница между композицией и агрегацией заключается в том, что в случае композиции целое явно контролирует время жизни своей составной части (часть не существует без целого), а в случае агрегации целое хоть и содержит свою составную часть, время их жизни не связано (например, составная часть передается через параметры конструктора). class CompositeCustomService {// Композиция private readonly CustomRepository _repository = new CustomRepository(),public void DoSomething() { // Используем _repository } }class AggregatedCustomService { // Агрегация private readonly AbstractRepository _repository, public AggregatedCustomService(AbstractRepository repository) { _repository = repository, } public void DoSomething() { // Используем _repository } }
CompositeCustomService для управления своими составными частями использует композицию, а AggregatedCustomService агрегацию. При этом явный контроль времени жизни обычно приводит к более высокой связанности между целым и частью, поскольку используется конкретный тип, тесно связывающий участников между собой.
С одной стороны, такая жесткая связь может не являться чем-то плохим, особенно когда зависимость является стабильной (см. раздел Стабильные и изменчивые зависимости в прошлой заметке). С другой стороны мы можем использовать композицию и контролировать время жизни объекта, не завязываясь на конкретные типы. Например, с помощью абстрактной фабрики: internal interface IRepositoryFactory { AbstractRepository Create(), } class CustomService { // Композиция private readonly IRepositoryFactory _repositoryFactory, public CustomService(IRepositoryFactory repositoryFactory) { _repositoryFactory = repositoryFactory, } public void DoSomething() { var repository = _repositoryFactory.Create(), // Используем созданный AbstractRepository } }
В данном случае мы не избавляемся от композиции (CustomService все еще контролирует время жизни AbstractRepository), но делает это не напрямую, а с помощью дополнительной абстракции абстрактной фабрики. Поскольку такой подход требует удвоения количества классов наших зависимостей, то его стоит использовать, когда явный контроль времени жизни является необходимым условием.
Интересной особенностью разных отношений между классами является то, что логичность их использования может зависеть от точки зрения проектировщика, от того, с какой стороны он смотрит на задачу и какие вопросы он задает себе при ее анализе. Именно поэтому одну и ту же задачу можно решить десятком разных способов, при этом в одном случае мы получим сильно связанный дизайн с большим количеством наследования и композиции, а в другом случае эта же задача будет разбита на более автономные строительные блоки, объединяемые между собой с помощью агрегации.
Например, нашу задачу с сервисами и репозитариями можно решить множеством разных способов. Кто-то скажет, что здесь подойдет наследование и сделает SqlCustomService наследником от AbstractCustomService, другой скажет, что этот подход неверен, поскольку CustomService у нас один, а иерархия должна быть у репозитариев.
Рисунок 4. Наследование vs Агрегация
Каждый вариант приводит к одному и тому же конечному результату, при этом связанность изменяется от очень высокой (при наследовании) к очень слабой (при агрегации).
Заключение
Существует несколько достаточно объективных критериев для определения связности дизайна по диаграмме классов: большие иерархии наследования (глубокие или широкие иерархии), и повсеместное использование композиции, а не агрегации скорее всего говорит о сильно связанном дизайне.
Большое количество наследования говорит о том, что проектировщики забыли о старом добром совете Банды Четырех, который сводится к тому, что следует предпочесть агрегацию наследованию, поскольку первая дает большую гибкость и динамичность во время исполнения.
Обилие же композиции говорит о нарушении Принципа Инверсии Зависимостей, сформулированном Бобом Мартином, которую сейчас можно выразить в терминах агрегации и композиции: предпочитайте агрегацию вместо композиции, поскольку первая стимулирует использование абстракций, а не конкретных классов.
В следующий раз: перейдем к рассмотрению конкретных DI паттернов и начнем с самого популярного из них с Constructor Injection.