|
Программирование >> Многопоточная библиотека с принципом минимализма
Перейдем теперь к более интересной части. Допустим, нам нужно инициализировать объект класса SmartPtr<widget, EnforceNotNull> объектом класса SmartPtr<Extendedwidget, NoChecking>. Как и раньше, указатель типа Extend-edwidget* без проблем преобразовывается в указатель типа Widget . Затем компилятор попытается сравнить конструкторы классов SmartPtr<Extendedwidget, NoChecking> и EnforceNorNull. Если класс Enf orceNorNull реализует конструктор, получающий объект NoChecking, то компилятор проверяет этот конструктор. Если класс NoChecking реализует оператор преобразования в тип EnforceNull, вызывается функция, реализующая это преобразование. В остальных случаях возникает ошибка компиляции. Очевидно, что при реализации преобразований стратегий друг в друга возникает удвоенная гибкость: преобразование можно реализовать как с помощью конструктора, так и с помощью оператора. По сложности это напоминает оператор присваивания, однако, к счастью, Sutter (2000) описал очень остроумный способ, позволяющий реализовать оператор присваивания с помощью конструктора копирования. (Он настолько остроумен, что о нем обязательно следует прочитать. Этот способ был применен при реализации класса SmartPtr в библиотеке Loki.) Несмотря на то что преобразования объектов классов NoChecking в объекты класса EnforceNotNull и даже наоборот имеют довольно ясный смысл, некоторые преобразования совершенно бессмысленны. Представьте себе преобразование указателя, подсчитывающего ссылки, в указатель, поддерживающий другую стратегию владения (ownership policy), например, уничтожение после копирования (destmctive сору) наподобие std: : auto ptr. Такое преобразование семантически неверно. Определение указателя, подсчитывающего ссылки, состоит в следующем: все указатели на один и тот же объект считаются известными и отслеживаются с помощью единственного счетчика. При попытке применить другую стратегию происходит нарушение инварианта, гарантирующего правильную работу указателя, подсчитывающего ссылки. В заключение заметим, что неявные преобразования, изменяющие стратегию владения, применять не следует и обращаться с ними нужно с максимальной осторожностью. Лучше всего изменять стратегию владения указателем, подсчитывающим ссылки, явно вызывая соответствующую функцию. Эта функция будет успешно работать, только если счетчик ссылок исходного указателя равен 1. 1.12. Разложение классов на стратегии При проектировании, основанном на применении стратегий, самое трудное -правильно разложить функциональные возможности класса на стратегии. Основное правило заключается в следующем: нужно идентифицировать и назвать проектные решения, определяющие режим работы класса. Все, что можно сделать несколькими способами, должно идентифицироваться и передаваться от класса к стратегии. Не забудьте: проектные решения, замурованные в класс, подобны константам, зашитым в код. Рассмотрим, например, класс WidgetManager. Если внутри класса WidgetManager создается новый объект класса widget, процесс создания объекта нужно передать стратегиям. Если класс WidgetManager хранит коллекцию объектов класса widget, имеет смысл выразить этот способ хранения в виде стратегии, если при этом какой-то специфический механизм хранения не имеет особого приоритета. в идеале главный класс полностью описывается с помощью отдельных внутренних стратегий. Он делегирует все проектные рещения и ограничения стратегиям. Такой класс представляет собой оболочку коллекции стратегий и только дирижирует стратегиями, добиваясь нужного режима работы. Недостаток обобщенного главного класса, чрезмерно перегруженного стратегиями, заключается в слишком большом количестве шаблонных параметров. На практике, если количество параметров больше четырех, с классом становится трудно работать. Тем не менее они оправдывают свое существование, если главный класс обеспечивает сложные и полезные функциональные возможности. Определение класса с помощью оператора typedef представляет собой важный инструмент, позволяющий использовать классы, основанные на стратегиях. Применение этого оператора - не просто вопрос удобства. Он гарантирует упорядоченное использование класса и его легкое сопровождение. Например, рассмотрим следующее определение типа, typedef SmartPtr < widget, RefCounted, Nochecked > widgetPtr; В программе было бы довольно затруднительно использовать длинную специализацию класса SmartPtr вместо класса WidgetPtr. Однако утомительность программирования ничего не значит по сравнению с главными проблемами, заключающимися в понимании и сопровождении программы. В процессе проектирования определение класса widget может измениться - например, может применяться стратегия проверки, отличающаяся от стратегии NoChecked. Очень важно, что класс widgetPtr использует вся программа, а не только жестко закодированная конкретизация класса SmartPtr. Это напоминает различие между вызываемыми и подставляемыми (inline) функциями. С технической точки зрения подставляемая функция решает ту же задачу, однако создать абстракцию на ее основе невозможно. Раскладывая класс на стратегии, важно найти ортогональное разложение (orthogonal decomposition). Возникающие при этом стратегии совершенно не зависят друг от друга. Распознать неортогональное разложение очень легко - в нем разные стратегии нуждаются в информации друг о друге. Например, представим себе стратегию Array в интеллектуальном указателе. Эта стратегия очень проста - она диктует, ссылается интеллектуальный указатель на массив или нет. Можно определить стратегию Array так, чтобы она содержала функцию-член т& ElementAtCr* ptr, unsigned int index), a также аналогичную версию для типа const т. В стратегиях, не работающих с массивами, функция ElementAt просто не определяется, поэтому попытка ее использовать приведет к ошибке во время компиляции. Функция ElementAt, в соответствии с определением, данным в разделе 1.6, представляет собой факультативную расширенную функциональную возможность. Реализации двух классов, основанных на стратегии Array, приведены ниже. template <class т> struct IsArray { т& ElementAtCr* ptr, unsigned int index) 42 Часть I. Методы return ptr[index]; const T& ElementAtCT* ptr, unsigned int index) const { return ptr[index]; template <c1ass т> struct isNotArray {}; Проблема заключается в том, что предназначение стратегаи Array (указать, ссылается интеллектуальный указатель на массив ти нет) плохо согласованно с другой стратегией - разрушением. Уничтожать указатели на объекты можно с помошью оператора delete, а указатели на массивы, состояшке из объектов, - оператором delete[]. Две независимые друг от друга стратегии являются ортогональными. На основании данного определения можно утверждать, что стратегии Array и Destroy не являются ортогональными. Для того чтобы описать способ работы с величинами, хранящимися в массиве, и способ уничтожения объектов в виде отдельных стратегий, необходимо описать их взаимодействие. Например, в стратегии Array, кроме функций, можно предусмотреть булевскую константу и передать ее в стратегию Destroy. Это осложняет и несколько офаничивает процесс проектирования обеих стратегий. Неортогональные стратегии несовершенны, поэтому нужно стараться их избегать. Они снижают уровень типовой безопасности во время компиляции и усложняют процесс разработки как главного класса, так и классов стратегий. Если приходится прибегать к неортогональным стратегиям, нужно хотя бы минимизировать их взаимную зависимость, передавая класс стратегии в качестве аргумента шаблонной функции другого класса стратегии. Этот способ компенсирует недостатки, связанные с неортогональностью, за счет гибкости, присущей интерфейсам, основанным на шаблонах. Теневой стороной этого метода остается тот факт, что одна стратегия должна явно задавать некоторые детали реализации других стратегий. Это снижает степень инкапсуляции. 1.13. Резюме Проектирование - это выбор. Чаще всего проблема заключается не в том, что задачу в принципе невозможно решить, а в том, что у нее существует множество способов решения. Необходимо знать, какие из возможных решений удовлетворительно решают поставленную задачу. Это вынуждает нас переходить от высших архитектурных уровней к низшим. Более того, выбранные варианты можно комбинировать, что приводит к появлению проклятия выбора из большого количества вариантов. Для того чтобы преодолеть проклятие выбора с помощью кода, имеющего разумные размеры, разработчики библиотек, предназначенных для проектирования профаммного обеспечения, должны изобретать и применять специальные способы. Изначально эти методы были предназначены для обеспечения гибкости при автоматической генерации кода в сочетании с небольшим количеством элементарных механизмов (primitive devices). Сами по себе библиотеки предоставляют офомное количество таких механизмов. Более того, они содержат спецификации (specifications), на основе которых создаются новые механизмы, так что пользователь может создавать их самостоятельно. Это существенно влияет на открытость (open-ended) проектирования.
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |