понедельник, 3 декабря 2012 г.

Наследование vs Композиция vs Агрегация

Между двумя классами/объектами существует разные типы отношений. Самым базовым типом отношений является ассоциация (association), это означает, что два класса как-то связаны между собой, и мы пока не знаем точно, в чем эта связь выражена и собираемся уточнить ее в будущем. Обычно это отношение используется на ранних этапах дизайна, чтобы показать, что зависимость между классами существует, и двигаться дальше.

image

Рисунок 1. Отношение ассоциации

Более точным типом отношений является отношение открытого наследования (отношение «является», IS A Relationship), которое говорит, что все, что справедливо для базового класса справедливо и для его наследника. Именно с его помощью мы получаем полиморфное поведение, абстрагируемся от конкретной реализации классов, имея дело лишь с абстракциями (интерфейсами или базовыми классами) и не обращаем внимание на детали реализации.

image

Рисунок 2. Отношение наследование

И хотя наследование является отличным инструментом в руках любого ОО-программиста, его явно недостаточно для решения всех типов задач. Во-первых, далеко не все отношения между классами определяются отношением «является», а во-вторых, наследование является самой сильной связью между двумя классами, которую невозможно разорвать во время исполнения (это отношение является статическим и, в строготипизированных языках определяется во время компиляции).

В этом случае нам на помощь приходит другая пара отношений: композиция (composition) и агрегация (aggregation). Оба они моделируют отношение «является частью» (HAS-A Relationship) и обычно выражаются в том, что класс целого содержит поля (или свойства) своих составных частей. Грань между ними достаточно тонкая, но важная, особенно в контексте управления зависимостями.

image

Рисунок 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 у нас один, а иерархия должна быть у репозитариев.

image

Рисунок 4. Наследование vs Агрегация

Каждый вариант приводит к одному и тому же конечному результату, при этом связанность изменяется от очень высокой (при наследовании) к очень слабой (при агрегации).

Заключение

Существует несколько достаточно объективных критериев для определения связности дизайна по диаграмме классов: большие иерархии наследования (глубокие или широкие иерархии), и повсеместное использование композиции, а не агрегации скорее всего говорит о сильно связанном дизайне.

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

Обилие же композиции говорит о нарушении Принципа Инверсии Зависимостей, сформулированном Бобом Мартином, которую сейчас можно выразить в терминах агрегации и композиции: предпочитайте агрегацию вместо композиции, поскольку первая стимулирует использование абстракций, а не конкретных классов.

В следующий раз: перейдем к рассмотрению конкретных DI паттернов и начнем с самого популярного из них – с Constructor Injection.

17 комментариев:

  1. По поводу тезиса что ассоциация - временная связь, пока не определились что именно надо.

    Если некий класс создаёт объект другого класса (та же фабрика), какая между ними связь? Я такое обозначаю именно как ассоциация и не временно, а на постоянной основе.

    Показать что они просто зависимы тоже не годиться, ведь создание объекта требует от "фабрики" знания как его правильно создать и "фабрика" на это завязана.

    ОтветитьУдалить
  2. Достаточно понятная статья, раскладывает все по полочкам.

    ОтветитьУдалить
  3. @Alexander: да, согласен, но в этом случае я добавляю уточнение над стрелкой, типа: <>, <> etc. А вот без такого уточнения - точно бывает только на ранней стадии.

    @Анатолий: спасибо!

    ОтветитьУдалить
  4. Любую проблему можно решить введением дополнительного абстрактного слоя, за исключением проблемы слишком большого количества абстрактных слоев.

    ОтветитьУдалить
  5. Как всегда хорошая статья! Наконец-то запомнил UML нотацию :)

    ОтветитьУдалить
  6. @Владимир: Спасибо! UML не сложное дело, тем более, что нужно запомнить всего-то процентов 20 нотации, не больше. А в деле дизайна помогает здорово.

    ОтветитьУдалить
  7. Нет, ну я все-таки не пойму. Где ты такие картинки берешь...? Про ромбики - круто :).

    ОтветитьУдалить
  8. @eugene: картинки я беру в Visio 2013;) там просто есть темки разные, вот они и смотрятся необычно:)

    ОтветитьУдалить
  9. "В данном случае мы не избавляемся от композиции (CustomService все еще контролирует время жизни ICustomRepository), но делает это не напрямую, а с помощью дополнительной абстракции – абстрактной фабрики." - вот тут не понял откуда взялся ICustomRepository, может AbstractRepository ?
    Смысл вводить интерфейс тут ?

    @Alexander: "Если некий класс создаёт объект другого класса (та же фабрика), какая между ними связь? Я такое обозначаю именно как ассоциация и не временно, а на постоянной основе." А разве это не есть агрегация ? Или если он не использует этот объект, а только возвращает, то это ассоциация ?

    Мораль: меньше наследования, больше агрегации ? )

    ОтветитьУдалить
  10. @Артем: Да, спасибо, речь идет об AbstractRepository.

    По поводу морали: больше агрегации, меньше композиции (о том, что наследование - не айс, это достаточно старый баян).

    ОтветитьУдалить
  11. Наилучшей проверкой на то, стоит ли использовать наследование, является проверка на соответствие принципу Лисков. Принцип непрост для понимания, зато приводит, затем, к просветлению :-)

    ОтветитьУдалить
  12. Вот еще раз насчет фабрики хотелось бы уточнить... а то не совсем понятно. Фабрика создает объект, следовательно связь композиция, так как она управляет временем жизни объекта, я правильно рассуждаю ?


    По поводу статьи - наконец то я понял, то что объясняли в университете :) Подача материала грамотная.

    ОтветитьУдалить
  13. @Артем: когда у нас появляется фабрика, то у нас в рассмотрении участвуют три класса: фабрика, создаваемые фабрикой классы и пользователь фабрики.

    Тот, кто пользуется фабрикой, ее не создает. Отношение между ним и фабрикой - агрегация.

    Но тот, кто использует фабрику, он создает объекты с помощью него, поэтому отношению между ним и создаваемым объектом - композиция.

    ОтветитьУдалить
  14. Все очень доступно и понятно написано, но возник вопрос.

    public class Program()
    {
    static void main()
    {
    A a = new A();
    B b = new B(); a.MakeNotComposite(b);
    a = null;
    /*
    ......some code......
    */
    }
    }


    public class B()
    {
    public C c {get;set;}
    }


    public class C()
    {

    }


    public class A()
    {
    private readonly C c = new C();
    public void MakeNotComposite(B b)
    {
    b.C = c;

    }
    }

    В каком отношении находятся классы А и С. Ведь нельзя сказать что это композиция, потому что на объект класса С теперь есть ссылка из объекта класса B, и при смерти объекта класса А, объект класса С останется жить. То есть получается тип отношения между классами нельзя определить на этапе компиляции?

    ОтветитьУдалить
  15. @toks: логически - это все еще композиция, хотя во время исполнения - уже нет. Обычно подобное поведение относится к code smell-ам, поскольку этот код подрывает инкапсуляцию класса A.

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

    Есть вариант, когда оба класса реализуют интерфейс IDisposable и целое вызывает Dispose своих составных частей делая их "логически" мертнвыми.

    ОтветитьУдалить
  16. Мне эта тема интересна см.также http://www.cyberforum.ru/oop/thread1155828.html
    Способы проектирования не однозначны. Хотелось бы понять, какие свойства информационной модели приводят к однозначному выбору агрегирования по значениям или по ссылкам.
    Особенно по ссылкам: где в жизни востребована ситуация, когда класс собирается из объектов другого класса созданных раньше и где действительно надо чтоы эти объекты "жили" дальше после уничтожения класса-агрегата? На С++ агрегирование по ссылкам я реализую с помощью создания списка объектов- члена класса-агрегата. Важно что этот список L может заполняться как конструктором класса-агрегата, так и методом. Т.е. класс-агрегат должен иметь конструктор, оставляющий список объектов А пустым. А заполнение объектами А происходит с помощью спец.метода вызываемого уже после создания агрегата, который запихивает очередной объект А в список L класса-агрегата.

    ОтветитьУдалить
  17. Этот комментарий был удален автором.

    ОтветитьУдалить