Анимация
JavaScript


Главная  Библионтека 

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 [ 35 ] 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85

virtual bool isDoneC); ...

Эти открытые виртуальные функции, как и все открытые виртуальные функции, одновременно определяют и интерфейс, и настраиваемое поведение. Проблема заключается в слове "одновременно", поскольку каждая открытая виртуальная функция вынуждена обслуживать двух потребителей с разными потребностями и разными целями.

• Одна группа пользователей - внешний вызывающий код, которому для работы с классом требуется его открытый интерфейс.

• Другая группа - производные классы, которым требуется "интерфейс настройки", представляющий собой набор виртуальных функций, посредством которых производные классы расширяют и уточняют функциональные возможности базовых классов.

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

А что, если мы захотим отделить спецификацию интерфейса от спецификации настраиваемого поведения реализации? Тогда мы будем вынуждены в конечном итоге перейти к чему-то наподобие шаблона проектирования "метод шаблона" (Template Method pattern [Gamma95]), поскольку то, чего мы хотим добиться, очень напоминает данный шаблон. Однако наша задача сушественно уже, и поэтому заслуживает более точного имени. Назовем этот шаблон проектирования шаблоном невиртуального интерфейса (Nonvirtual Interface (NVI) pattern). Вот пример данного шаблона проектирования в действии.

пример 18-2: более современный базовый класс,

использующий невиртуальный интерфейс (NVI)

для отделения интерфейса от внутренней

реализации класса

class widget { public:

Стабильный невиртуальный интерфейс

int Process( 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 в [SutterOO]).

В-третьих, теперь базовый класс лучше приспособлен к будущим изменениям. Мы можем позже изменить наши замыслы и добавить проверку выполнения пред- и постусловий или разделить работу на несколько этапов, или, напри.мер. реализовать полное разделение интерфейса и реализации с использованием идиомы указателя на реализацию (Pimpl, см. [SutterOO]), или внести другие изменения в интерфейс для настройки класса, при этом никак не влияя на код, который использует этот класс. Например, существенно труднее начать с открытой виртуальной функции и позже пытаться обернуть ее в другую для проверки пред- и постусловий, чем изначально предоставить невиртуальную функцию-оболочку (даже если никакой дополнительной проверки или иной работы в настоящий момент не требуется) и вставить в нее необходимые проверки позже. (Дополнительная информация о том, как сделать класс более приспособленным для будущих изменений, имеется в [HyslopOO].)

" Но, - могут возразить некоторые, - все, что делает такая открытая виртуальная функция - это вызов закрытой виртуальной функции. Это - всего лишь одна строка. Насколько нужны такие односфочные функции, если они практически бесполезны, да и к тому же приводят к снижению эффективности (за счет лишнего вызова функции) и повышению сложности (за счет добавления лишней функции)?" Сначала пара слов об эффективности: на практике снижения эффективности не будет, так как если такая од-носфочная передающая вызов функция объявлена как встоаивасмая, то все известные мне компиляторы выполняют оптимизацию такого вызова, полностью убирая его, т.е. в результате при вызове нет никаких накладных расходов. Теперь поговорим о сложности. Единсгвсннос, в чем проявляется сложность, - это дополнительное время, необходимое для написания фивиальных односфочных функций-оболочек. Все. На интср-

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



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

Итак, изложенный материал оправдывает невиртуальные интерфейсы и доказывает, что виртуальные функции только выифывают от закрытия. Но мы еше не ответили на вопрос, должны ли виртуальные функции быть закрытыми или защищенными. Ответ:

> Рекомендация

Лучше делать виртуальные функции закрытыми (private).

Это просто. Такой подход позволяет производному классу переопределять функции для необходимой настройки поведения класса, при этом не делая их доступными для непосредственного вызова производным классам (что было бы возможно при обь-явлении функций защищенными). Дело в том, что виртуальные функции существуют для того, чтобы обеспечить возможность настройки поведения класса; если они при этом не должны непосредственно вызываться из кода производных классов, нет никакой необходимости в том, чтобы делать их не закрытыми. Однако иногда нам надо вызывать базовые версии виртуальных функций (см., например, [HyslopOO]), и только в этом случае имеет смысл делать эти виртуальные функции защищенными, т.е. мы можем сформулировать очередное правило.

> Рекомендация

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

Основной вывод заключается в том, что применение шаблона NVI к виртуальным функциям помогает нам отделить интерфейс от реализации. Можно добиться еще более полного разделения, если воспользоваться шаблоном типа Bridge [Gamma95], идиомой наподобие Pimp! (преимущественно для управления зависимостями во время компиляции и гарантий безопасности исключений) [SutterOO, Suttcrt)2] или более общими handle/body или envelope/letter [Coplien92], а также других подходов. Если же только вам не нужна большая степень разделения интерфейса и реализации, то NVI зачастую оказывается достаточно для ваших нужд. С другой стороны, применение NVI - хорошая идея, которую стоит принять по умолчанию в своей практической деятельности при создании нового кода и рассматривать как минимально необходимое разделение. В конце концов, она не приводит к дополнительным расходам (не считая написания дополнительной строки кода на функцию), но зато сушественно уменьшает количество проблем впоследствии.

Дополнительные примеры использования шаблона NVI для "приватизации" вир-туального поведения можно найти в [HyslopOO].

Кстати о [HyslopOO] - вы не обратили внимания на то, что в представленном там коде имеется открытый виртуальный деструктор? Это приводит нас ко второй теме нашей задачи.

Виртуальный вопрос №2: деструкторы базовых классов

Теперь мы готовы заняться вторым классическим вопросом -- должны ли деструкторы базовых классов быть виртуальными?



0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 [ 35 ] 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85