39. Виртуальные функции стоит делать неоткрытыми, а открытые — невиртуальными

39. Виртуальные функции стоит делать неоткрытыми, а открытые — невиртуальными

Резюме

В базовых классах с высокой стоимостью изменений (в частности, в библиотеках) лучше делать открытые функции невиртуальными. Виртуальные функции лучше делать закрытыми, или защищенными — если производный класс должен иметь возможность вызывать их базовые версии (этот совет не относится к деструкторам; см. рекомендацию 50).

Обсуждение

Большинство из нас на собственном горьком опыте выучило правило, что члены класса должны быть закрытыми, если только мы не хотим специально обеспечить доступ к ним со стороны внешнего кода. Это просто правило хорошего тона обычной инкапсуляции. В основном это правило применимо к членам-данным (см. рекомендацию 41), но его можно с тем же успехом использовать для любых членов класса, включая виртуальные функции.

В частности, в объектно-ориентированных иерархиях, внесение изменений в которые обходится достаточно дорого, предпочтительна полная абстракция: лучше делать открытые функции невиртуальными, а виртуальные функции — закрытыми (или защищенными, если производные классы должны иметь возможность вызывать базовые версии). Это — шаблон проектирования Nonvirtual Interface (NVI). (Этот шаблон похож на другие, в особенности на Template Method [Gamma95], но имеет другую мотивацию и предназначение.)

Открытая виртуальная функция по своей природе решает две различные параллельные задачи.

• Она определяет интерфейс. Будучи открытой, такая функция является непосредственной частью интерфейса класса, предоставленного внешнему миру.

• Она определяет детали реализации. Будучи виртуальной, функция предоставляет производному классу возможность заменить базовую реализацию этой функции (если таковая имеется), в чем и состоит цель настройки.

В связи с существенным различием целей этих двух задач, они могут конфликтовать друг с другом (и зачастую так и происходит), так что одна функция не в состоянии в полной мере решить одновременно две задачи. То, что перед открытой виртуальной функцией ставятся две существенно различные задачи, является признаком недостаточно хорошего разделения зон ответственности и по сути нарушения рекомендаций 5 и 11, так что нам следует рассмотреть иной подход к проектированию.

Путем разделения открытых функций от виртуальных мы достигаем следующих значительных преимуществ.

• Каждый интерфейс может приобрести свой естественный вид. Когда мы разделяем открытый интерфейс от интерфейса настройки, каждый из них может легко приобрести тот вид, который для него наиболее естественен, не пытаясь найти компромисс, который заставит их выглядеть идентично. Зачастую эти два интерфейса требуют различного количества функций и/или различных параметров; например, внешняя вызывающая функция может выполнить вызов одной открытой функции Process, которая выполняет логическую единицу работы, в то время как разработчик данного класса может предпочесть перекрыть только некоторые части этой работы, что естественным образом моделируется путем независимо перекрываемых виртуальных функций (например, DoProcessPhase1, DoProcessPhase2), так что производному классу нет необходимости перекрывать их все (точнее говоря, данный пример можно рассматривать как применение шаблона проектирования Template Method).

• Управление базовым классом. Теперь базовый класс находится под полным контролем своего интерфейса и стратегии и может обеспечить пост- и предусловия интерфейса (см. рекомендации 14 и 68), причем выполнить всю эту работу в одном удобном повторно используемом месте — невиртуальной функции интерфейса. Такое "предварительное разложение" обеспечивает лучший дизайн класса.

• Базовый класс более устойчив к изменениям. Мы можем позже изменить наше мнение и добавить некоторую проверку пост- или предусловий, или разделить выполнение работы на большее количество шагов или переделать ее, реализовать более полное разделение интерфейса и реализации с использованием идиомы Pimpl (см. рекомендацию 43), или внести иные изменения в базовый класс, и все это никак не повлияет на код, использующий данный класс или наследующий его. Заметим, что гораздо проще начать работу с использования NVI (даже если открытые функции представляют собой однострочные вызовы соответствующих виртуальных функций), а уже позже добавлять все проверки и инструментальные средства, поскольку эта работа никак не повлияет на код, использующий или наследующий данный класс. Ситуация окажется существенно сложнее, если начать с открытых виртуальных функций и позже изменять их, что неизбежно приведет к изменениям либо в коде, который использует данный класс, либо в наследующем его.

См. также рекомендацию 54.

Исключения

NVI не применим к деструкторам в связи со специальным порядком их выполнения (см. рекомендацию 50).

NVI непосредственно не поддерживает ковариантные возвращаемые типы. Если вам требуется ковариантность, видимая вызывающему коду без использования dynamic_cast (см. также рекомендацию 93), проще сделать виртуальную функцию открытой.

Ссылки

[Allison98] §10 • [Dewhurst03] §72 • [Gamma95] • [Keffer95 pp. 6-7] • [Koenig97] §11 • [Sutter00] §19, §23 • [Sutter04] §18

Поделитесь на страничке

Следующая глава >

Похожие главы из других книг:

Открытые чаты

Из книги автора

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


R.10.2 Виртуальные функции

Из книги автора

R.10.2 Виртуальные функции Если класс base содержит виртуальную (§R.7.1.2) функцию vf, а производный от него класс derived также содержит функцию vf того же типа, тогда вызов vf для объекта класса derived является обращением к derived::vf, даже если доступ к этой функции происходит через


50. Делайте деструкторы базовых классов открытыми и виртуальными либо защищенными и невиртуальными

Из книги автора

50. Делайте деструкторы базовых классов открытыми и виртуальными либо защищенными и невиртуальными РезюмеУдалять или не удалять — вот в чем вопрос! Если следует обеспечить возможность удаления посредством указателя на базовый класс, то деструктор базового класса


Правило 9: Никогда не вызывайте виртуальные функции в конструкторе или деструкторе

Из книги автора

Правило 9: Никогда не вызывайте виртуальные функции в конструкторе или деструкторе Начну с повторения: вы не должны вызывать виртуальные функции во время работы конструкторов или деструкторов, потому что эти вызовы будут делать не то, что вы думаете, и результатами их


Открытые поля, приватные поля и открытые свойства

Из книги автора

Открытые поля, приватные поля и открытые свойства Заметим, что в указанных выше классах поля данных были определены открытыми только для того, чтобы упростить пример. Конечно, с точки зрения объектно-ориентированного подхода предпочтительнее использовать приватные


17.5. Виртуальные функции в базовом и производном классах

Из книги автора

17.5. Виртуальные функции в базовом и производном классах По умолчанию функции-члены класса не являются виртуальными. В подобных случаях при обращении вызывается функция, определенная в статическом типе объекта класса (или указателя, или ссылки на объект), для которого она


17.5.4. Виртуальные функции и аргументы по умолчанию

Из книги автора

17.5.4. Виртуальные функции и аргументы по умолчанию Рассмотрим следующую простую иерархию классов:#include iostreamclass base {public:virtual int foo( int ival = 1024 ) {cout " base::foo() -- ival: " ival endl;return ival;}// ...};class derived : public base {public:virtual int foo( int ival = 2048 ) {cout " derived::foo() -- ival: " ival endl;return ival;}// ...};Проектировщик


17.5.8. Виртуальные функции, конструкторы и деструкторы

Из книги автора

17.5.8. Виртуальные функции, конструкторы и деструкторы Как мы видели в разделе 17.4, для объекта производного класса сначала вызывается конструктор базового, а затем производного класса. Например, при таком определении объекта NameQueryNameQuery poet( "Orlen" );сначала будет вызван


19.2.4. Объекты-исключения и виртуальные функции

Из книги автора

19.2.4. Объекты-исключения и виртуальные функции Если сгенерированный объект-исключение имеет тип производного класса, а обрабатывается catch-обработчиком для базового, то этот обработчик не может использовать особенности производного класса. Например, к функции-члену value(),


Открытые двери

Из книги автора

Открытые двери Выпустите данные в мир через RSS, API и т.п.Не пытайтесь запереть ваших клиентов. Позвольте им получить принадлежащую им информацию когда они хотят и как они хотят. Для этого вам придется отказаться от идеи запечатать данные. Выпустите их. Дайте людям доступ к


1.18 Виртуальные Функции

Из книги автора

1.18 Виртуальные Функции Предположим, что мы пишем программу для изображения фигур на экране. Общие атрибуты фигуры представлены классом shape, а специальные атрибуты – специальными классами:class shape (* point center; color col; //... public: void move(point to) (* center=to; draw(); *) point where() (* return center; *) virtual void


7.2.8 Виртуальные Функции

Из книги автора

7.2.8 Виртуальные Функции Виртуальные функции преодолевают сложности решения с пмощью полей типа, позволяя программисту описывать в базовом классе функции, которые можно переопределять в любом проиводном классе. Компилятор и загрузчик обеспечивают правильное


8.5.4 Виртуальные Функции

Из книги автора

8.5.4 Виртуальные Функции Если базовый класс base содержит virtual (виртуальную) (#8.1) функцию vf, а производный класс derived также содержит функцию vf, то обе функции должны иметь один и тот же тип, и вызов vf для объекта класса derived вызывает derived::vf. Например:struct base (* virtual void vf (); void f (); *);class


ОКНО ДИАЛОГА: Знать, что делать, а не как делать

Из книги автора

ОКНО ДИАЛОГА: Знать, что делать, а не как делать 19 сентября 2005 года исполнилось пятнадцать лет с момента появления Интернета в России. 19 сентября 1990 года был зарегистрирован домен su. Отечественный сегмент всемирной сети был создан во многом благодаря сотрудникам


Стоит ли делать приёмочное тестирование частью спринта?

Из книги автора

Стоит ли делать приёмочное тестирование частью спринта? Тут у нас полный «разброд и шатание». Некоторые наши команды включают приёмочное тестирование в спринт, однако, большинство - нет. И вот почему:Спринт ограничен во времени. Приёмочное тестирование (которое, если