Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Программирование на C / C++ / C++ for real programmers.pdf
Скачиваний:
231
Добавлен:
02.05.2014
Размер:
2.04 Mб
Скачать

96

Видите, что здесь происходит? Для клиента класс Foo перестает существовать. Для всех практических целей указатель стал самим объектом. С таким же успехом мы могли все переименовать, убрать Р перед указателем и заменить имя Foo чем-ни6удь более закрытым и загадочным. Единственное, что говорит о существовании второго класса, — предварительное объявление class Foo;.

Цена всего происходящего — вызов не подставляемых (noninline) функций в каждой функции класса указателя. Для некоторых немногочисленных приложений и классов даже эта малая цена может стать неприемлемой. В таких случаях существуют две альтернативы для повышения скорости: использование умных указателей на базе оператора -> и использование интерфейсных указателей с занесением объявления класса указываемого объекта в файл .h и отказом от всех преимуществ инкапсуляции. Как вы убедитесь в оставшейся части этой главы, второй вариант все же имеет некоторые достоинства.

Изменение интерфейса

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

Грани

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

class View { // На практике будет производным от другого класса protected:

//Часть, предназначенная только для производных классов вида

public:

//Функции конструирования и инициализации

//Функции уничтожения и деактивизации

//Общие функции объекта

//Обработка событий

//Функции отображения

//Геометрия («где сработала мышь?»)

//Управление иерархией видов

};

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

— все равно что блуждать в зеркальном лабиринте; куда бы вы ни повернулись, виднеются бесчисленные отражения одного и того же класса. Конечно, такой класс можно было бы организовать и более разумно — например, выделить каждое семейство интерфейсов в собственный базовый класс и затем объединить эти классы с помощью множественного наследования. Или построить комплекс из объектов, совместная работа которых основана на взаимном делегировании. Все эти варианты обладают своими недостатками.

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

Когда фрагменты объединяются в общий класс, от которого затем создаются производные классы, проблема возникает заново на другом уровне иерархии классов.

Нелегко заставить один базовый класс правильно работать с другим, когда эти два класса не имеют ничего общего до их объединения в контексте некоторого составного класса.

Проектирование больших комплексов взаимодействующих объектов — занятие не для слабонервных.

97

Можно ли разбить такой класс на составляющие, не прибегая к сложностям создания производных классов или делегирования? Конечно можно, если воспользоваться технологией мудрых указателей. Достаточно создать для одного объекта несколько указателей, каждый из которых отвечает за некоторый аспект деятельности этого объекта.

class ViewEvents { private:

View* view; public:

// Функции, относящиеся к обработке событий

};

class ViewDrawing { private:

View* view; public:

// Функции, относящиеся к графическому выводу

}; // И т.д.

Каждый из этих мудрых указателей воспроизводит интерфейс к некоторому подмножеству функций класса View и перенаправляет вызовы функциям-прототипам объекта вида. Сам объект вида может быть устроен как угодно: на основе одиночного и множественного наследования, делегирования в комплексе объектов или в форме одного гигантского конгломерата; клиентов это волновать не должно. Я называю такие интерфейсные указатели, ограничивающие клиента подмножеством полного интерфейса, гранями (facets).

Эта базовая идея укоренилась как минимум в одной коммерческой технологии — компонентной модели объекта (СОМ, Component Object Model) компании Microsoft, которая называет эти указатели интерфейсами. Один из мелких разработчиков, компания Quasar Knowledge Systems, предложила аналогичную идею для объектов SmallTalk и назвала такие указатели комплексами (suites). Как бы они ни назывались, этой идее суждено стать одной из важнейших идиом дизайна объектноориентированного программирования будущего, поскольку она обладает повышенной гибкостью и модульностью по сравнению с функциональным делением на основе наследования и делегирования.

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

Преобразование указываемого объекта в грань

Итак, вы хотите получить грань по имеющемуся указываемому объекту. Для этого существует много способов, однако наиболее соответствующий стилю C++ заключается в использовании операторов преобразования.

class View { public:

operator ViewEvents() { return new ViewEvents(this); } operator ViewDrawing() { return new ViewDrawing(this); }

};

Другой вариант — разрешить пользователю напрямую использовать конструкторы класса грани:

ViewEvents ve(aView);

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

98

противном случае. Те из вас, кому приходилось пользоваться технологиями СОМ и OLE компании Microsoft, узнают знакомую функцию QueryInterface, поддерживаемую всеми объектами.

Кристаллы

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

class ViewEvents { private:

View* view; public:

operator ViewDrawing() { return ViewDrawing(*view); } // И т.д. для других граней

};

В этом маленьком С++-изме работа поручается операторной функции operator ViewDrawing() целевого вида. При малом количестве граней такое решение вполне приемлемо. С ростом количества граней число операторов преобразования возрастает в квадратичной зависимости, поскольку каждая грань должна преобразовывать ко всем остальным. Следующая модификация возвращает задачу к порядку n, где n — количество граней. Продолжая свою откровенно слабую метафору, я называю объект, который собирает и выдает грани, кристаллом (gemstone).

class View;

class ViewEvents; class ViewDrawing; class ViewGemstone { private:

View* view; public:

ViewGemstone(View* v) : view(v) {}

bool operator!() { return view == NULL; } operator ViewEvents();

operator ViewDrawing(); // И т.д.

};

class ViewEvents {

friend class ViewGemstone; private:

View* view;

ViewEvents(View* v) : view(v) {} public:

bool operator!() { return view == NULL; } operator ViewGemstone();

};

class ViewDrawing {

friend class ViewGemstone; private:

View* view;

ViewDrawing(View* v) : view(v) {}

99

public:

bool operator!() { return view == NULL; } operator ViewGemstone();

};

У нас есть один объект, кристалл, который умеет генерировать все грани; каждая грань, в свою очередь, знает, как найти кристалл. Кристалл является единственным объектом, который может создавать грани, так как последние имеют закрытые конструкторы и дружат с кристаллом. Концепция кристалла чрезвычайно гибка — он может быть самостоятельным объектом, абстрактным базовым классом объекта и даже одной из граней.

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

Вариации на тему граней

Грани можно реализовать несколькими способами. В совокупности они образуют надмножество тех возможностей, которые в C++ поддерживаются с помощью наследования и переменных класса.

Грани — множества подфункций

Самая простая форма грани — та, которая предоставляет интерфейс к подмножеству функций указываемого объекта.

// В файле Pointee.h class Pointee;

class Facet {

friend class PointeeGemstone; private:

Pointee* pointee;

Facet(Pointee* p) : pointee(p) {} public:

void Fn1(); int Fn2(); void Fn17();

};

class PointeeGemstone { private:

Pointee* pointee; public:

PointeeGemstone(Pointee* p) : pointee(p) {} Operator Facet();

};

// В файле Pointee.cpp class Pointee { public:

void Fn1();

100

int Fn2(); void Fn3(); char Fn4(); // И т.д. void Fn17();

};

Здесь грань просто отбрасывает все функции, которые не входят в ее компетенцию. Клиент имеет дело с «объектом», который намного легче всего указываемого объекта, но за кулисами все равно прячется полный объект.

Грани — переменные класса

Грань может представлять собой интерфейсный указатель на переменную класса. Это позволяет многократно использовать грань в различных кристаллах или для организации интерфейса к отдельному экземпляру. Если указываемый объект имеет переменную класса Bar, грань может представлять собой простой интерфейсный указатель на Bar.

// В файле Pointee.h class BarFacet { private:

Bar* bar; public:

BarFacet(Bar* b) : bar(b) {}

// Интерфейсы к функциям класса Bar

};

class PointeeGemstone { private:

Pointee* p; public:

operator BarFacet(); // И т.д.

};

// В файле Pointee.cpp class Pointee {

friend class PointeeGemstone; private:

Bar bar; // Внедренная переменная класса Pointee public:

// И т.д.

};

PointeeGemstone::operator BarFacet()

{

return BarFacet(&p->Bar); // Грань переменной

}

Все прекрасно работает, если вам хватает относительно простых правил согласованности C++. Вероятно, в более общем случае стоит воспользоваться приемами, описанными далее, в разделе «Обеспечение согласованности». В частности, одна из проблем такого упрощенного подхода

101

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

Грани — базовые классы

Грани также могут использоваться для создания эквивалента встроенного преобразования типа от производного класса к базовому.

// В файле Pointee.h class FooFacet { private:

Foo* foo; public:

FooFacet(Foo* f) : foo(f) {}

// Интерфейсы к функциям класса foo

};

class PointeeGemstone { private:

Pointee* p; public:

operator FooFacet(); // И т.д.

};

// В файле Pointee.cpp

class Pointee : public Foo { friend class PointeeGemstone; public:

// И т.д.

};

PointeeGemstone::operator FooFacet()

{

 

return FooFacet(p);

// Компилятор преобразует p к Foo*

}

 

Как и в случае с гранями-переменными, это может позволить вам многократно использовать одни и те же грани Foo для базовых классов, переменных или отдельных объектов, хотя для обеспечения более строгих правил согласованности, описанных ниже, потребуется более узкая специализация. Например, при таком подходе вы сможете выполнить преобразование от кристалла к грани FooFacet, но не сможете снова вернуться к кристаллу.

Грани — делегаты

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

// В файле Pointee.h class BarFacet { private:

Bar* bar;