Критерии оценки корректности применения наследования. Примеры корректного и некорректного применения наследования

Часто начинающие программисты применяют наследование в мало пригодных ситуациях, где более уместной является композиция. Наследование классов следует применять, если всегда можно однозначно без логического противоречия напрямую использовать объект производного класса в любом месте программы, где ожидается объект базового класса. Этот принцип носит специальное название - принцип подстановки Лисков (Liskov’s Substitution Principle).

Существует простой контрольный вопрос, по которому можно определить корректность применения наследования: является ли в предметной области понятие, описываемое производным классом, разновидностью понятия, описываемого базовым классом?Проговорить ответ на этот вопрос следует на естественном языке. Например:

Автобус - это разновидность транспортного средства (Bus IS A Vehicle).

Грузовик - это разновидность транспортного средства (Truck IS A Vehicle).

Приведенные утверждения истинны и не вызывают никаких противоречий. Так нельзя сказать о фразе, касающейся отношения между классами Engine и Vehicle, кажущейся очень странной:

Двигатель - это разновидность транспортного средства???

Двигатель является частью транспортного средства, но никак не его разновидностью, соответственно здесь должно применяться отношение композиции, а не наследования.

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

class Point3D

{

float m_x, m_y, m_z;

};

class Circle

: public Point3D

{

float m_radius;

public:

Circle ( Point3D _center, float _radius )

: Point3D( _center ), m_radius( _radius )

{}

};

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

class Circle

{

Point3D m_center;

float m_radius;

public:

Circle ( Point3D _center, float _radius )

: m_center( _center ), m_radius( _radius )

{}

};

Ниже приведен еще один типичный неправильный пример использования наследования - эллипс наследуется от окружности для повторного использования элементов реализации (координаты центра и одного из радиусов):

class Ellipse

: public Circle

{

float m_radius2;

public:

Ellipse ( Point3D _center, float _radius1, float_radius2 )

: Circle( _center, _radius1 ), m_radius2( _radius2 )

{}

};

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

class Ellipse

{

Point3D m_center;

float m_radius1, m_radius2;

public:

Ellipse ( Point3D _center, float _radius1, float _radius2 )

: m_center( _center ), m_radius1( _radius1), m_radius2( _radius2 )

{}

};

class Circle

: public Ellipse

{

public:

Circle ( Point3D _center, float _radius )

: Ellipse( _center, _radius, _radius )

{} // ^ используем один и тот же радиус дважды

};

Второе решение лучше первого с точки зрения соблюдения правил проектирования объектных моделей, однако не слишком эффективно в связи с хранением избыточного второго радиуса для окружности. Такое наследование иногда называют “сужающим”. Помимо избыточности, еще один недостаток такого вида наследования состоит в том, что методы базового класса, а также код, использующий объекты-эллипсы, может случайно нарушить невидимый для них инвариант окружности о равенстве радиусов. Окружность не может гарантировать, что одна из копий значения ее радиусов, хранящаяся в определении базового класса, не будет несогласованно изменена без аналогичного изменения второй копии, хранящейся в производном классе.

Наилучший способ моделирования эллипсов и окружностей состоит в выделении более общего базового класса - фигура с центром - объединяющего схожие свойства обоих конкретных классов, при этом не создающего избыточности на уровне данных или неявных инвариантов:

class ShapeWithCenter

{

Point3D m_center;

public:

ShapeWithCenter ( Point3D _center )

: m_center( _center )

{}

};

class Circle

: public ShapeWithCenter

{

float m_radius;

public:

Circle ( Point3D _center, float _radius )

: ShapeWithCenter( _center ), m_radius( _radius )

{}

};

class Ellipse

: public ShapeWithCenter

{

float m_radius1, m_radius2;

public:

Ellipse ( Point3D _center, float _radius1, float _radius2 )

: ShapeWithCenter( _center ), m_radius1( _radius1 ), m_radius2( _radius2 )

{}

};

Наши рекомендации