С++: Ассоциация, агрегирование и композиция

Я начинаю изучать OOAD, и мне трудно найти пример кода C++, который иллюстрирует, как Association, Aggregation и Composition реализованы программно. (Есть несколько сообщений повсюду, но они относятся к С# или java). Я нашел пример или два, но все они противоречат инструкциям моего инструктора, и я смущен.

Я понимаю, что в:

  • Ассоциация: Foo имеет указатель на объект Bar в качестве члена данных
  • Агрегация: Foo имеет указатель на объект Bar, и данные Bar глубоко копируются в этом указателе.
  • Состав: Foo имеет объект Bar в качестве элемента данных.

И вот как я его реализовал:

//ASSOCIATION
class Bar
{
    Baz baz;
};
class Foo
{
    Bar* bar;
    void setBar(Bar* _bar)
    {
        bar=_bar;
    }
};

//AGGREGATION
class Bar
{
    Baz baz;
};
class Foo
{
    Bar* bar;
    void setBar(Bar* _bar)
    {
        bar = new Bar;
        bar->baz=_bar->baz;
    }
};


//COMPOSTION
class Bar
{
    Baz baz;
};
class Foo
{
    Bar bar;
    Foo(Baz baz)
    {
        bar.baz=baz;
    }
};

Это правильно? Если нет, то как это сделать? Было бы также полезно, если бы вы также дали мне ссылку на код из книги (чтобы я мог обсудить с моим инструктором)

Ответы

Ответ 1

Я собираюсь игнорировать агрегацию. Это не очень четко определенная концепция, и, на мой взгляд, это вызывает больше путаницы, чем того стоит. Композиции и ассоциации вполне достаточно, Крейг Ларман сказал мне об этом. Возможно, это не тот ответ, которого искал ваш инструктор, но вряд ли он будет реализован на С++ в любом случае.

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

Состав

Самый простой способ реализации композиции - использовать переменную типа Bar, как и вы. Единственное изменение, которое я сделал бы, это инициализировать Bar в списке инициализации конструктора:

// COMPOSITION - with simple member variable
class Foo {
 private:
  Bar bar;
 public:   
  Foo(int baz) : bar(baz) {}
};

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

Также может возникнуть причина для реализации композиции с использованием указателя. Например, Bar может быть полиморфным, и вы не знаете конкретного типа во время компиляции. Или, возможно, вы хотите переслать declare Bar, чтобы минимизировать зависимости компиляции (см. ИММОМ PIMPL). Или, возможно, множественность этого отношения составляет от 1 до 0..1, и вам нужно иметь нуль Bar. Конечно, потому что это композиция Foo должна принадлежать Bar, а в современном мире С++ 11/С++ 14 мы предпочитаем использовать интеллектуальные указатели вместо владения исходными указателями:

 // COMPOSITION - with unique_ptr
class Foo {
 private:
  std::unique_ptr<Bar> bar;
 public:   
  Foo(int baz) : bar(barFactory(baz)) {}
};

Я использовал std::unique_ptr здесь, потому что Foo является единственным владельцем Bar, но вы можете использовать std::shared_ptr, если какой-либо другой объект нуждается в std::weak_ptr to Bar.

Ассоциация

Ассоциация обычно реализуется с использованием указателя, как вы это делали:

// Association - with non-owning raw pointer
class Foo {
 private:
  Bar* bar;
 public:   
  void setBar(Bar* b) { bar = b; }
};

Конечно, вам нужно быть уверенным, что Bar будет жив, пока Foo использует его, иначе у вас есть обвисший указатель. Если время жизни Bar менее ясное, тогда a std::weak_ptr может быть более подходящим:

// Association - with weak pointer
class Foo {
 private:
  std::weak_ptr<Bar> bar;
 public:   
  void setBar(std::weak_ptr<Bar> b) { bar = std::move(b); }
  void useBar() {
    auto b = bar.lock();
    if (b)
      std::cout << b->baz << "\n";
  }
};

Теперь Foo может использовать Bar, не опасаясь остаться с висящим указателем:

 Foo foo;
 {
   auto bar = std::make_shared<Bar>(3);
   foo.setBar(bar);
   foo.useBar(); // ok
 }  
 foo.useBar(); // bar has gone but it is ok

В некоторых случаях, когда право собственности на Bar действительно неясно, ассоциация может быть реализована с использованием std::shared_ptr, но я думаю, что это должно быть последним средством.

Что касается вашей реализации Агрегации с глубокой копией указателя Bar. Я бы не сказал, что это типичная реализация, но, как я уже сказал, это зависит от ваших требований.  Вам нужно убедиться, что вы вызываете delete в указателе участника Bar в деструкторе Foo, хотя в противном случае у вас есть утечка памяти (или используйте умный указатель).