Наследование. Абстрактные базовые классы (ABC) / FAQ C++
В чем особенность отделения интерфейса от реализации?
Интерфейсы – это самый ценный ресурс компании. Разработка интерфейса занимает больше времени, чем создание конкретного класса, который выполняет этот интерфейс. Кроме того, интерфейсы требуют времени более высокооплачиваемых людей.
Поскольку интерфейсы настолько ценны, они должны быть защищены от искажения структурами данных и другими артефактами реализации. Таким образом, вы должны отделить интерфейс от реализации.
Как отделить интерфейс от реализации на C++ (как в Modula-2)?
Используйте ABC.
Что такое ABC?
Абстрактный базовый класс (abstract base class).
На уровне проектирования абстрактный базовый класс (ABC) соответствует абстрактной концепции. Если вы спросите механика, ремонтирует ли он транспортные средства, он, вероятно, поинтересовался бы, какое транспортное средство вы имеете в виду. Скорее всего, он не ремонтирует космические шаттлы, океанские лайнеры, велосипеды или атомные подводные лодки. Проблема в том, что термин «транспортное средство» является абстрактным понятием (например, вы не можете построить «транспортное средство», если не знаете, какое именно транспортное средство строить). В C++ класс Vehicle
(транспортное средство) будет абстрактным базовым классом, а Bicycle
, SpaceShuttle
и т.д. будут производными классами (OceanLiner
– это разновидность Vehicle
). В реальном объектно-ориентированном проектировании абстрактные базовые классы встречаются повсюду.
На уровне языка программирования абстрактный базовый класс – это класс, который имеет одну или несколько чисто виртуальных функций-членов. Вы не можете создать объект (экземпляр) абстрактного базового класса.
Что такое «чисто виртуальная» функция-член?
Объявление функции-члена, которая превращает обычный класс в абстрактный класс (то есть ABC). Обычно вы реализуете ее только в производном классе.
Некоторые функции-члены существуют концептуально; у них нет обоснованного определения. Например, предположим, что я попросил вас нарисовать Shape
(фигуру) в точке (x, y), имеющую размер 7. Вы спросите меня: «Какую фигуру нарисовать?» (круги, квадраты, шестиугольники и т.д. рисуются по-разному). В C++ мы должны указать существование функции-члена draw()
(чтобы пользователи могли вызывать ее, когда у них есть Shape*
или Shape&
), но мы понимаем, что она может (логически) быть определена только в производных классах:
class Shape {
public:
virtual void draw() const = 0; // = 0 означает, что функция "чисто виртуальная"
// ...
};
Эта чисто виртуальная функция превращает Shape
в абстрактный базовый класс. Если хотите, можете думать о синтаксисе "= 0;
", как если бы код был в указателе NULL
. Таким образом, Shape
обещает своим пользователям услугу, но Shape
не может предоставить код для выполнения этого обещания. Это заставляет любой реальный объект, созданный из [конкретного] класса, производного от Shape
, иметь указанную функцию-член, даже если базовый класс еще не имеет достаточно информации для ее фактического определения.
Обратите внимание, что можно дать определение чисто виртуальной функции, но это обычно сбивает с толку новичков, и это лучше отложить на потом.
Как вы определяете конструктор копирования или оператор присваивания для класса, который содержит указатель на (абстрактный) базовый класс?
Если класс «владеет» объектом, на который указывает указатель (абстрактного) базового класса, используйте «идиому виртуального конструктора» в (абстрактном) базовом классе. Как обычно с этой идиомой, мы объявляем в базовом классе чисто виртуальный методclone()
:
class Shape {
public:
// ...
virtual Shape* clone() const = 0; // виртуальный конструктор (копирования)
// ...
};
Затем мы реализуем этот метод clone()
в каждом производном классе. Ниже показан код производного класса Circle
:
class Circle : public Shape {
public:
// ...
virtual Circle* clone() const;
// ...
};
Circle* Circle::clone() const
{
return new Circle(*this);
}
Примечание: тип возвращаемого значения в производном классе намеренно отличается от типа возвращаемого значения в базовом классе.
Ниже показан код производного класса Square
:
class Square : public Shape {
public:
// ...
virtual Square* clone() const;
// ...
};
Square* Square::clone() const
{
return new Square(*this);
}
Теперь предположим, что каждый объект Fred
«имеет» объект Shape
. Естественно, объект Fred
не знает, является ли Shape
объектом Circle
или Square
, или... Конструктор копирования и оператор присваивания Fred
'а для копирования этого объекта вызовут метод clone()
Shape
:
class Fred {
public:
// p должен быть указателем, возвращенным с помощью new; он не должен быть NULL
Fred(Shape* p)
: p_(p) { assert(p != NULL); }
~Fred()
{ delete p_; }
Fred(const Fred& f)
: p_(f.p_->clone()) { }
Fred& operator= (const Fred& f)
{
if (this != &f) { // Проверить на самоприсваивание
Shape* p2 = f.p_->clone(); // СНАЧАЛА создать новый...
delete p_; // ...ЗАТЕМ удалить старый
p_ = p2;
}
return *this;
}
// ...
private:
Shape* p_;
};