Наследование и композиция
дата публикации: 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. Предпочитайте композицию наследованию.