Наследование и композиция
дата публикации: 2016-07-04

Сначала приведу несколько полезных понятий, которые взяты мной из книги: Design Patterns: Elements of Reusable Object-Oriented Software

Инстанцирование – это процесс создания экземпляра класса, в процессе чего выделяется память на переменные и методы класса.

Абстрактный класс – класс, единственным назначением которого является определение интерфейса для своих подклассов. Невозможно создать экземпляр абстрактного класса.

Конкретный класс – класс не являющийся абстрактным.

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

Прежде чем говорить о композиции, вспомним про статическое и динамическое связывание и проведем аналогию. Статическое связывание осуществляется на этапе компиляции ровным счетом как и наследование. Динамическое связывание осуществляется на этапе исполнения, как и композиция. Наследование зачастую называют "прозрачным ящиком" из-за того, что дочерний класс получает доступ к внутреннему устройству родительского класса. Композиция лишена такого и называется "черным ящиком". Итак, что же такое композиция.

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

class Base {
public:
  Base() { cout << "I'm Constructor Base" << endl; }
  void getInfo() { cout << "I'm class Base" << endl; }
  virtual ~Base() { cout << "I'm Destructor Base" << endl; }
};

class Derived1 : public Base {
public:
  Derived1() { cout << "I'm Constructor Derived1" << endl; }
  void access() { Base::getInfo(); }
  ~Derived1() { cout << "I'm Destructor Derived1" << endl; }
};

class Derived2 {
public:
  Base b;
  Derived2() { cout << "I'm Constructor Derived2" << endl; }
  void access() { b.getInfo(); }
  ~Derived2() { cout << "I'm Destructor Derived2" << endl; }
};

int main() {
  Derived1* obj1 = new Derived1();
  obj1->access();
  delete obj1;
  cout << "-------------" << endl;
  Derived2* obj2 = new Derived2();
  obj2->access();
  delete obj2;
}

Результатом выполнения кода будет:

I'm Constructor Base
I'm Constructor Derived1
I'm class Base
I'm Destructor Derived1
I'm Destructor Base
-------------
I'm Constructor Base
I'm Constructor Derived2
I'm class Base
I'm Destructor Derived2
I'm Destructor Base

На примере двух классов Derived1 и Derived2 показаны два подхода: наследование и композиция. При этом уполномоченным объектом во втором случае является экземпляр b класса Base, а получателем экземпляр obj2 класса Derived2. Результат выполнения кода одинаковый, что показывает идентичность подходов.

Следует привести еще пару определений, которые тесно связаны с делегированием. Агрегирование одного объекта с другим подразумевает несение ответственности за агрегированный объект со стороны агрегирующего. В нашем примере агрегирующим объектом является obj2, а агрегированным объект b. Понятие агрегирования очень близко к понятию осведомленности, однако связи между объектами в случае осведомленности могут появляться или исчезать по мере исполнения программы. Приведу пример осведомленности на базе класса Derived2.

class Derived3 {
public:
  Base* b;
  Derived3() { cout << "I'm Constructor Derived3" << endl; }
  void startConnect() { b = new Base(); }
  void access() { b->getInfo(); }
  void stopConnect() { delete b; }
  ~Derived3() { cout << "I'm Destructor Derived3" << endl; }
};

Класс Derived3 показывает свою осведомленность о классе Base, но управляет выделением памяти под экземпляр этого класса непосредственно программист с помощью методов startConnect и stopConnect. В случае с агрегированием агрегированный объект "встраивается" в агрегирующий, что обеспечивает совместные операции с памятью.

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

1. Программируйте через интерфейсы, а не через реализации.

2. Предпочитайте композицию наследованию.