Анимация
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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105

DocElement

+Accept(visitor; DocElementVisitorS)

Para;

jraph

Accept(vis: DocElement

Visitors)

Raster

Bitmap

Accept(visitor: DocElem

entVlsitor&)

OocElementVlsjtor

VisitParagraphlpar: Pafagfaph&) VlsitRasterBitmap(bmp: RasterBitmap&)

DocStats

IncrementFontSIze

Рис. 10.1. Иерархия классов для игры с двумя уровнями сложности

10.2. Перегрузка и функция-ловушка

Управление перегрузкой функций в языке С++ оказывает очень большое влияние на реализацию проектов на основе шаблона visitor, хотя для самого шаблона перегрузка функций значения не имеет.

В классе DocElementvisitor каждому инспектируемому типу соответствует отдельная функция-член: visit Paragraph (Paragraph*), visitRasterBitmap (RasterBitmap*) и т.д. Эти функции явно избыточны. Кроме того, имя инспектируемого типа является частью самой функции.

Как правило, избыточности лучше избегать. Для этого следует применять перегрузку функций. Мы просто назовем все функции одним именем visit и предоставим компилятору решать, какую перегруженную функцию visit вызывать для передаваемого параметра заданного типа. Альтернативое определение класса DocElementvisitor выглядит следующим образом.

class DocElementvisitor {

public:

virtual void visit(Paragraph*) = 0; virtual void visit(RasterBitmap*) = 0; ... другие подобные функ11ии ...



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

void Paragraph::Accept(DocElementvisitor& v) {

v.visitC*this);

void RasterBitmap::AcceptCDocElementvisitor& v) {

v.visitC*this);

Они выглядят настолько одинаковыми, что может возникнуть соблазн перенести их в базовый класс DocElement. Это было бы ошибкой. На самом деле это соверщенно разные функции. Параметр *this в функции Paragraph::Accept имеет статический тип Paragraphs, а в функции RasterBitmap: :Accept - тип RasterBitmap*. Именно эти статические типы помогают компилятору правильно определять, какую из пере-фуженных функций DocElementvisitor::visit следует вызывать. Если бы функция Accept была реализована в классе DocElement, парамеф *this имел бы статический тип DocElement*, который не может предоставить компилетору всю необходимую информацию. Таким образом, факторизация классов не должна быть произвольной. Неправильная факторизация может привести профамму в негодность.

Перефузка функций порождает интересную идею. Допустим, что все классы, производные от класса DocEl ement, реализуют функцию Accept, просто переадресовывая вызов функции DocElementvisitor: :visit. Тогда в классе DocElement можно определить следующую перефуженную функцию-ловущку (catch-all overload).

class DocElementvisitor {

publi с:

... как и прежде ...

virtual void visit(DocElement*) = 0;

Когда будет вызываться эта перефуженная функция? Если новый класс является непосредственным наследником класса DocElement и в классе DocElementvisitor для него нет подходящей персфуженной функции visit, то вступают в силу правила перефузки и автоматического преобразования производных типов в базовые. Ссылка на объект неизвестного класса автоматически конвертируется в ссылку на объект базового класса DocElement, и вызывается функция-ловущка. Если такой функции нет, возникает ошибка компиляции. Предусматривать функцию-ловушку или нет, зависит от конкретной ситуации.

В перефуженной функции-ловушке можно выполнить много полезной работы. Примеры представлены в работах Влиссидеса (Vlissides, 1998, 1999). В такой функции можно выполнять произвольные действия весьма обобщенного характера или прибегнуть в переключению типов (используя оператор dynamic cast), чтобы распознать фактический тип параметра DocElement.

10.3. Уточнение реализации: шаблон Acyclic Visitor

Итак, вы решили применить шаблон visitor. Вас интересует, как это сделать в реальном проекте?



Анализ зависимостей между классами в предьщущем примере приводит нас к следующим выводам.

• Для того чтобы скомпилировать определение класса DocElement, необходимо знать о существовании класса DocElementvisitor, поскольку он упоминается в сигнатуре функции-члена DocElement::Accept. Для этого достаточно сделать неполное объявление класса (forward declaration).

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

Такой тип зависимости называется циклическим (cyclic dependency). Циклические зависимости создают хорощо известные трудности. Для класса DocElement необходим класс DocElementvisitor, а классу DocElementvisitor нужны все классы из иерархии класса DocElement. Следовательно, класс DocElement зависит от всех своих подклассов. Фактически здесь проявляется циклическая зависимость по имени (cyclic name dependency), т.е. для компиляции определений классов нужны лищь их имена. Таким образом, классы следует разделить по файлам.

Файл DocElementvisitor.h class DocElement; class Paragraph; class RasterBitmap;

... неполные объявления всех подклассов класса DocElement ...

class DocElementvisitor {

virtual void visitParagraph(Paragraph&) = 0; virtual void visitRasterBitmap(RasterBitmap&) = 0; ... другие подобные функции ...

Файл DocElement.h class DocElementvisitor;

class DocElement {

public:

virtual void Accept(DocElementvisitor&) = 0;

};

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

Добавление новых подклассов класса oacElement сшкавтсрс крайне стжиыы. На ведь мы и не собирались добавлять в иерархию класса DocElement никаких новых элементов! Шаблон visitor предназначен прежде всего для устойчивых иерархий, в которые добавляются лищь операции, а не новые классы. Стоит напомнить, что шаблон visitor позволяет это сделать за счет усложнения процедуры добавления новых производных классов в инспектируемую иерархию (в нашем примере - в иерархию класса DocEl ement).

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



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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105