|
Программирование >> Обобщенные обратные вызовы
> Рекомендация Предпочтительно делать интерфейс невиртуальным. Обратите внимание - я сказал предпочтительно , а не всегда . Интересно, что стандартная библиотека С++- следует этой рекомендации. Не считая деструкторов (которые рассматриваются отдельно в рекомендации №4) и не учитывая дублирования виртуальных функций, которые участвуют в специализациях шаблонов классов, в стандартной библиотеке есть: 6 открытых виртуальных функций; все они представляют собой std::exception::what и ее перекрытия; и 142 неоткрытых виртуальных функций. Недавно мне встретился еще один пример. Когда я писал эту книгу, я работал на Microsoft над С++-аспектами платформы .NET и .NET Frameworks (FX), которые выросли в Win FX, который является объектно-ориентированным наследником Win32 API и программной моделью Longhorn, следующего поколения операционной системы Windows. Win FX даже в текущем состоянии разработки представляет собой огромный API - уже сейчас в нем более 14000 классов и около 100000 функций-членов (включающих в себя текущее состояние .NET Frameworks и многое другое). Это действительно много. Вы не ошибетесь, если спросите - не слишком ли это монстрооб-разно, но сейчас дело не в .этом. Вот почему я упомянул .NET Frameworks и его эволюцию в Win FX. Для такого монстра, как эта библиотека классов, единственная надежда на работоспособность заключается в повсеместном строго соблюдающемся хорошем дизайне классов. Я счастлив сообшить, что так оно и есть. Вот одна из рекомендаций проектирования Win FX, которая кажется удивительно знакомой, и хотя я согласен с ней, я не имею никакого отношения к ее принятию - она принята по не зависящим от меня обстоятельствам. Рекомендуется обеспечивать настройку посредством защищенных методов. Открытый интерфейс базового класса должен обеспечивать богатый набор функциональных возможностей для потребителя этого класса. Однако наст/юйка класса для предоставления богатых функциональных возможностей потребителю должна обеспечиваться реализацией минимально возможного количества методов. Для достижения этой цели следует обеспечить набор невиртуальных или финальных открытых методов, каждый из которых вызывает единственный защищенный метод (член семейства методов) с суффиксом core , который и реализует данный метод. Эта технология известна как метод шаблона (.NET Framework (WinFX) Design Guidelines, январь 2004). Давайте рассмотрим эту практическую рекомендацию подробнее. Традиционно многие программисты используют базовые классы с открытыми виртуальными функциями. Например, мы можем написать следующий код. пример 18-1: традиционный базовый класс class widget { public: каждая из этих функций может (не обязательно) быть чисто виртуальной, и в этом случае она может быть реализована в widget, а может и не быть - см. [Sutter02]. virtual int process( Gadget* ); Имеются в виду функции Java/C#. - Прим. перев. Задача 18. Виртуальность 119 virtual bool isDoneC); ... Эти открытые виртуальные функции, как и все открытые виртуальные функции, одновременно определяют и интерфейс, и настраиваемое поведение. Проблема заключается в слове одновременно , поскольку каждая открытая виртуальная функция вынуждена обслуживать двух потребителей с разными потребностями и разными целями. Одна группа пользователей - внешний вызывающий код, которому для работы с классом требуется его открытый интерфейс. Другая группа - производные классы, которым требуется интерфейс настройки , представляющий собой набор виртуальных функций, посредством которых производные классы расширяют и уточняют функциональные возможности базовых классов. Открытая виртуальная функция вынуждена выполнять две работы. Одна - определять интерфейс, поскольку она открытая и, соответственно, является непосредственной частью интерфейса. Вторая - определить детали реализации, а именно, настроить внутреннее поведение, поскольку эта функция виртуальная и, таким образом, обеспечивает возможность замещения в производном классе реализации этой функции в базовом классе. То, что перед виртуальной функцией стоят две существенно различные задачи и имеется два разных класса пользователей, - признак недостаточного разделения задач и того, что неплохо было бы пересмотреть сам подход к дизайну класса. А что, если мы захотим отделить спецификацию интерфейса от спецификации настраиваемого поведения реализации? Тогда мы будем вынуждены в конечном итоге перейти к чему-то наподобие шаблона проектирования метод шаблона (Template Method pattern [Gamma95]), поскольку то, чего мы хотим добиться, очень напоминает данный шаблон. Однако наша задача существенно уже, и поэтому заслуживает более точного имени. Назовем этот шаблон проектирования шаблоном невиртуального интерфейса (Nonvirtual Interface (NVI) pattern). Вот пример данного шаблона проектирования в действии. пример 18-2: более современный базовый класс, использующий невиртуальный интерфейс (nvi) для отделения интерфейса от внутренней реализации класса class widget { public: Стабильный невиртуальный интерфейс int ProcessC Gadget& ); использует DoProcess...() bool isDoneQ; Использует DolsDone() private: Настройка - деталь реализации, которая может как соответствовать интерфейсу, так и не соответствовать ему. каждая из этих функций может (не обязательно) быть чисто виртуальной и, если это так, иметь (или не иметь) реализацию в классе widget (см. [Sutter02]) virtual int DOProcessPhasel( Gadgets ); vi rtual int DoProcessPhase2( Gadget& ); vi rtual bool DolsDoneO; ... Использование шаблона NVI дает возможность получения устойчивого невиртуального интерфейса при делегировании всей работы по настройке закрытым виртуальным функциям, в конце концов, виртуальные функции разработаны для того, чтобы позволить производным классам настроить свое поведение. Поскольку интерфейс класса предполагается стабильным и непротиворечивым, лучше всего не позволять производным классам каким-либо образом изменять или настраивать его. Подход NVI обладает рядом преимуществ и не имеет существенных недостатков. Во-первых, обратите внимание, что теперь базовый класс полностью контролирует свой интерфейс и стратегию и может диктовать предусловия и постусловия его работы, выполнять добавление функциональности и другие подобные действия в одном удобном для повторного использования месте - функциях невиртуального интерфейса. Это способствует хорошему дизайну класса, поскольку позволяет базовому классу обеспечить согласованность производных классов при подстановке в соответствии с принципом подстановки Л исков (Liskov) [Liskov88]. В случае, когда особое значение приобретают вопросы производительности профаммы, базовый класс может выполнять проверку ряда предусловий и постусловий только в отладочном режиме, отказываясь от таких проверок либо в процессе компиляции окончательной версии профаммы, либо подавляя эти проверки во время выполнения программы в соответствии с настройками ее запуска. Во-вторых, при лучшем разделении интерфейса и реализации, мы можем обеспечить большую свободу в настройке поведения класса, не влияя на его вид для внешних пользователей. Так, в примере 18-2 мы решили, что имеет смысл предоставить пользователю одну функцию Process и в то же время обеспечить более гибкую настройку, разбив реализацию на две части - DoProcessPhasel и DoProcessPhase2. Это оказалось очень просто. Мы бы не смогли добиться этого при использовании версии с открытыми виртуальными функциями без того, чтобы такое разделение стало видимо в интерфейсе, тем самым добавляя сложности для пользователей, которым в этой ситуации пришлось бы вызывать две функции (см. также задачу 19 в [SuUetOO]). В-третьих, теперь базовый класс лучше приспособлен к будущим изменениям. Мы можем позже изменить наши замыслы и добавить проверку выполнения пред- и постусловий или разделить работу на несколько этапов, или, напри.мер, реализовать полное разделение интерфейса и реализации с использованием идиомы указателя на реализацию (Pimpl, см. [SutterOO]), или внести другие изменения в интерфейс для настройки класса, при этом никак не влияя на код, который использует этот класс. Например, существенно труднее начать с открытой виртуальной функции и позже пытаться обернуть ее в другую для проверки пред- и постусловий, чем изначально предоставить невиртуальную функцию-оболочку (даже если никакой дополнительной проверки или иной работы в настоящий момент не требуется) и вставить в нее необходимые проверки позже. (Дополнительная информация о том, как сделать класс более приспособленным для будущих изменений, имеется в [HyslopOO].) Но, - могут возразить некоторые, - все, что делает такая открытая виртуальная функция - это вызов закрытой виртуальной функции. Это - всего лишь одна строка. Насколько нужны такие однострочные функции, если они практически бесполезны, да и к тому же приводят к снижению эффективности (за счет лишнего вызова функции) и повышению сложности (за счет добавления лишней функции)? Сначала пара слов об эффективности: на практике снижения эффективности не будет, так как если такая однострочная передающая вызов функция объявлена как встраиваемая, то все известные мне компиляторы выполняют оптимизацию такого вызова, полностью убирая его, т.е. в результате при вызове нет никаких накладных расходов. Теперь поговорим о сложности. Единсгвсннос, в чем проявляется сложность, - это дополнительное время, необходимое для написания тривиальных односфочных функций-оболочек. Все. На интср- На самом деле некоторые компиляторы всегда делают такую функцию встраиваемой и убирают ее, независимо от того, хотите вы этого или нет, впрочем, это уже другая история - см. задачу 25.
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |