В этой статье поговорим об одной особенности C++, которая не имеет особого практического значения, но иногда может пригодиться. Это абстрактные классы.
Но перед тем, как рассказать про абстрактные классы нужно начать с путаницы, которая часто встречается в книжках по C++.
Путаница между конструкцией и обобщением
Разберем сначала ситуацию в физическом мире. Допустим, у нас есть табуретка.
Мы можем добавить к табуретке спинку и получить стул.
В этом случае мы можем сказать, что табуретка — это часть конструкции стула. Назовем эту точку зрения «конструктивный подход». Важно, что при конструктивном подходе любые два человека легко назовут отличия стула от табуретки. Это наглядно и не вызовет споров.
Есть другой подход. Мы поможем посмотреть абстрактно и назвать табуретку и стул одним словом «мебель». Мебель — это обобщение. Само понятие «мебель» происходит от латинского слова mobilis — подвижный. Так называются предметы для сидения, лежания и размещения вещей. Назовем эту точку зрения «абстрактный подход».
Различие между понятием «стул» и понятием «мебель» в том, что стул — это физический предмет, который можно пощупать, а мебель — это абстрактное понятие для удобства описания группы предметов. Поэтому когда мы видим вывеску «Мебель», мы понимаем, что именно мы можем купить в этом магазине, но мы не можем купить «мебель», мы можем купить только конкретный предмет мебели: стул, стол, кресло.
Проблема с обобщением заключается в том, что обобщение может быть произведено по любому признаку. Например, табуретка и стул могут входить в следующие обобщения:
- предметы мебели,
- предметы, на которых сидят
- предметы из дерева и т.д.
Все эти обобщения правильны и имеют смысл в определенном контексте. Нельзя указать единственно правильное обобщение. Каждый может предложить свой вариант.
Когда мы переходим к программированию, то часто авторы используют такие аналогии для указания наследования «Мебель — Стул», то есть используют абстрактный подход. Но эти понятия не связаны отношением наследования. Правильное наследование нам подсказывает конструктивный подход: «Табуретка — Стул».
В программировании класс — это описание некоторого образования, которое включает свойства и методы. Мы можем описать табуретку как некоторый класс, а стул сделать производным классом. То есть программист в программе реализует не обобщение, а конструкцию.
В этом случае стул унаследует описание табуретки и получит дополнительно методы и свойства спинки. Поэтому производный класс в программировании — это аналог конструктивного подхода в физическом мире.
Мебель — это обобщение, а для обобщения в языке C++ вводится отдельное понятие — абстрактный класс.
Какая же путаница часто бывает в учебниках по C++? Есть популярная аналогия для иллюстрации классов: «Млекопитающие-Волки». И его приводят как пример наследования классов. Это неверно. Нет такого зверя — «млекопитающее». Термин «млекопитающее» — это обобщение. Для его реализации требуется использовать абстрактный класс.
А для обычного наследования правильно использовать аналогию «Волк — Собака». Здесь все нормально, потому что существуют как экземпляры волков, так и экземпляры собак.
Что такое абстрактный класс?
Абстрактный класс — это класс, в котором хотя бы один метод является виртуальным. А виртуальный метод — это метод, который должен быть переопределен в производных классах. Иногда виртуальный метод называют не совсем правильно «виртуальной функцией», так как эта функция являются часть класса, то правильно ее называть методом.
Рассмотрим пример. Мы создаем класс Shape, который будем использовать для рисования фигур. Но нельзя нарисовать «фигуру» вообще, можно нарисовать прямоугольник, круг, квадрат и т.п. Термин «фигура» — это обобщение. Поэтому мы определяем метод Draw как виртуальный. Его нельзя вызвать, а в производных классах будет свое определение этого метода.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#include <iostream> using namespace std; class Shape { public: virtual void Draw() = 0; }; class Rectangle: public Shape { public: void Draw() { cout << "Rectangle" << endl; } }; class Circle: public Shape { public: void Draw() { cout << "Circle" << endl; } }; int main() { Rectangle R; Circle C; R.Draw(); C.Draw(); return 0; } |
В этом примере строка
1 |
virtual void Draw() = 0; |
задает виртуальную функцию, которая потом переопределяется в производных классах.
Использование абстрактных классов
Так как абстрактные классы не могут иметь экземпляров, практического смысла в них нет. В приведенном примере абстрактный класс можно спокойно удалить, на поведение программы это никак не повлияет.
Когда же могут пригодиться абстрактные классы?
Чаще всего абстрактные классы используются в рекламных целях. Если вам нужно представить программу в какой-либо презентации, то добавление абстрактного класса дает возможность сделать красивую картинку типа этой.
Поэтому широко распространено использование абстрактных классов при защите диплома или диссертации. Это позволяет подать проект в более солидном виде.
Абстрактный класс на всякий случай
В некоторых книгах по C++ приводится такой аргумент по использованию абстрактных классов: в начале разработки программы программист не знает, что может понадобится. Поэтому нужно создать абстрактный класс на всякий случай, а потом можно будет легко переопределить его методы под любую задачу. Поэтому абстрактные классы якобы повышают «гибкость» разработки.
Звучит заманчиво, но давайте проверим этот аргумент на реальном примере. Допустим, в программе уже есть рисование нарисовать круга и прямоугольника. Прошло время и ему неожиданно говорят: «Нарисуй еще ромб». А у него, какая досада, нет абстрактного класса «фигура».
Сравним поведение программиста в двух ситуациях:
Ситуация 1. Абстрактный класс есть
Программист напишет класс «Ромб» и функцию Draw:
1 2 3 4 |
class Circle: public Shape { public: void Draw() { ... } }; |
Ситуация 2. Абстрактного класса нет
Программист напишет класс «Ромб» и функцию Draw:
1 2 3 4 |
class Circle { public: void Draw() { ... } }; |
Чем-нибудь упростил ему задачу абстрактный класс? Ничем.
Поэтому писать абстрактный класс «на всякий случай» точно не стоит.
Абстрактные классы хорошо со своими библиотеками поставлять. Скрывают «лишние» методы и переменные реализации класса, но оставляют только те методы, которые необходимы для программеров смежного отдела или аутсорсеров. Например куча классов с бизнес-логикой, а для GUI-шников видны пара-другая виртуальных методов и что бы докапаться до грааля ноу-хау, либо дизассемблер, либо промышленный шпионаж).
Это всё утрировано, но прочитав пост, я подумал, что абстрактные классы это 5-е колесо телеги программирования. Их видимо придумали маркетологи для маркетологов
Скорее не маркетологи, а теоретики от программирования.
Автору не мешало бы вникнуть поглубже в предмет прежде чем о нем писать. Всему что создано есть своя сфера применения, в том числе и абстрактным классам. А приведенные примеры высосаны из пальца.
Жаль, что человек, который так глубоко проник в предмет абстрактных классов, так и не смог назвать сферы их применения.