Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Шрифт:
Оказывается, иногда может быть опасно использовать обычные виртуальные функции, которые обеспечивают как интерфейс функции, так и ее реализацию по умолчанию. Для того чтобы понять, почему имеется такая вероятность, рассмотрим иерархию самолетов в компании XYZ Airlines. XYZ располагает самолетами только двух типов: модель A и модель B, и оба летают одинаково. В связи с этим разработчики XYZ проектирует такую иерархию:
Чтобы выразить тот факт, что все самолеты должны поддерживать функцию fly, и для того чтобы засвидетельствовать, что для разных моделей, в принципе, могут потребоваться различные реализации fly, функция Airplane::fly объявлена виртуальной. При этом во избежание написания идентичного кода в классах ModelA и ModelB в качестве стандартного поведения используется тело функции Airplane::fly, которую наследуют как ModelA, так и ModelB.
Это классический пример объектно-ориентированного проектирования. Два класса имеют общее свойство (способ реализации fly), поэтому оно реализуется в базовом классе и наследуется обоими подклассами. Благодаря этому проект явным образом выделяет общие свойства, что позволяет избежать дублирования, благоприятствует проведению будущих модернизаций и упрощает долгосрочную эксплуатацию – иными словами, обеспечивает все, за что так ценится объектно-ориентированная технология. Программисты компании XYZ Airlines могут собой гордиться.
А теперь предположим, что дела XYZ идут в гору, и компания решает приобрести новый самолет модели C. Эта модель отличается от моделей A и B, в частности, тем, что летает по-другому.
Программисты компании XYZ добавляют в иерархию класс ModelC, но в спешке забывают переопределить функцию fly:
В своем коде потом они пишут что-то вроде этого:
Назревает катастрофа: делается попытка отправить в полет объект ModelC, как если бы он принадлежал одному из классов ModelA или ModelB. Такой образ действия
Проблема здесь заключается не в том, что Airplane::fly ведет себя определенным образом по умолчанию, а в том, что такое наследование допускает неявное применение этой функции для ModelC. К счастью, легко можно предложить подклассам поведение по умолчанию, но не предоставлять его, если они сами об этом не попросят. Трюк состоит в том, чтобы разделить интерфейс виртуальной функции и ее реализацию по умолчанию. Вот один из способов добиться этого:
Обратите внимание, что функция Airplane::fly преобразовна в чисто виртуальную. Она предоставляет интерфейс для полета. В классе Airplane присутствует и реализация по умолчанию, но теперь она представлена в форме независимой функции defaultFly. Классы, подобные ModelA и ModelB, которые хотят использовать поведение по умолчанию, просто выполняют встроенный вызов defaultFly внутри fly (см. также правило 30 о взаимодействии встраивания и виртуальных функций):
Теперь для класса ModelC возможность случайно унаследовать некорректную реализацию fly исключена, поскольку чисто виртуальная функция в Airplane вынуждает ModelC создавать свою собственную версию fly.