Анимация
JavaScript
|
Главная Библионтека Рассмотрим простейший пример низкоуровневой разработки - интеллектуальные указатели (глава 7). Интеллектуальные указатели (smart pointers) представляют собой классы, которые могут быть как одно- так и многопоточными. Они могут использовать различные стратегии владения ресурсами, а также разные компромиссы между безопасностью и скоростью. Кроме того, интеллектуальные указатели могут поддерживать или не поддерживать автоматическое преобразование в обычные указатели. Все эти свойства можно свободно комбинировать, причем обычно в конкретной области, для которой предназначено программное обеспечение, наилучшим является лишь одно решение. Разнообразие методов создания программного обеспечения постоянно смушает начинающих разработчиков. Перед ними стоит конкретная задача. Что применить для ее успешного решения? События? Объекты? Наблюдатели? Обратные вызовы? Виртуальные классы? Шаблоны? Если абстрагироваться от конкретных деталей, разные решения окажутся эквивалентными. В отличие от новичка, опытный разработчик программного обеспечения знает, что работает, а что - нет. Для каждой конкретной задачи существует множество методов решения. Они обладают своими преимуществами и недостатками, которые определяют, насколько тот или иной метод подходит для решения поставленной задачи. Решение, которое казалось вполне приемлемым на бумаге, на практике может оказаться ошибкой. Создавать программное обеспечение сложно, поскольку разработчик постоянно должен делать выбор. А в программировании, как и в жизни вообще, трудно сделать правильный выбор. Опытный разработчик знает, какой выбор ведет к поставленной цели, в то время как новичок каждый раз делает шаг в неизвестность. Опытный архитектор программного обеспечения напоминает хорошего шахматиста: он видит на много ходов вперед. Для этого нужно долго учиться. Возможно поэтому гениальные программисты бывают очень молодыми, в то время как гениальные разработчики программного обеспечения обычно находятся в зрелом возрасте. Кроме головоломок, постоянно возникающих перед начинающими разработчиками, комбинаторная природа архитектурных решений доставляет много хлопот создателям библиотек. Для того чтобы реализовать библиотеку, пригодную для создания программного обеспечения, разработчик должен классифицировать и адаптировать множество типичных ситуаций. Библиотеку следует оставить открытой для модификаций, чтобы прикладной программист мог приспособить ее для своих нужд, создав конкретную программу. Действительно, как упаковать гибкие, основательно разработанные компоненты в библиотеку? Как предоставить пользователю возможность настраивать эти компоненты? Как преодолеть "проклятие выбора" с помощью кода, имеющего разумные размеры? Вот каким вопросам посвящена эта глава, да и вся книга в целом. 1.2. Недостатки универсального интерфейса Реализовать все под оболочкой универсального интерфейса - неудачное решение. Это объясняется следующими причинами. К основным негативным последствиям такого выбора относятся интеллектуальные издержки, огромный размер и неэффективность библиотеки. Гигантские классы очень непродуктивны, поскольку на их изучение нужно тратить большие усилия, они слиш- ком велики, а программы, использующие такие классы, работают намного медленнее, чем аналогичные программы, разработанные вручную. Однако едва ли не самой важной проблемой, связанной с использованием универсального интерфейса, является потеря безопасности статических типов (static type safety). Одна из основных целей архитектуры любого профаммного обеспечения - воплощение некоторых аксиом "по определению". Например, нельзя одновременно создавать два объекта класса Singleton (глава 6) или объекты непересекающихся семейств (disjoint families) (глава 9). В идеале разработчик должен накладывать боль-щинство ограничений еще на этапе компиляции. В больщом всеобъемлющем интерфейсе очень трудно учесть все подобные ограничения. Обычно конкретные офаничения семантически соответствуют лишь части интерфейса. Это ведет к увеличению разрыва между синтаксической и семантической правильностью программ, использующих данную библиотеку. Рассмотрим, например, аспект реализации объекта класса Singleton, связанный с безопасностью работы в многопоточной среде. Если библиотека полностью инкапсулирует потоковые свойства, то пользователь конкретной, непереносимой системы не может использовать библиотеку синглтонов. Если эта библиотека предоставляет доступ к основным незащищенным функциям, возникает опасность, что профаммист разрушит библиотеку, написав код, который будет синтаксически правильным, но семантически неверным. А что, если библиотека будет реализовывать разные проектные решения в виде разных классов более скромного размера? Каждый класс мог бы воплощать конкретное проектное решение. При использовании интеллектуальных указателей, например, естественно ожидать появления многочисленных реализаций: singletonThreadedSmartPtr, Multirhread-edsmartPtr, RefCountedSmartPtr, RefLinl<edSmartPtr и т.п. Для этого подхода характерен резкий рост количества разных вариантов проектных решений. Четыре упомянутых класса могут привести к появлению новых комбинаций, например, в виде класса SingleThreadedRefCountedSmartPtr. Преобразование типов приведет к еще большему количеству комбинаций, которые в конце концов выйдут за рамки возможностей как профаммиста, так и пользователя библиотеки. Очевидно, что идти этим путем не следует. Преодолеть экспоненциальный рост вариантов с помощью фубой силы невозможно. Такого рода библиотеки не только требуют для своей разработки и применения огромных умственных усилий, но и являются крайне негибкими. Малейшая непредвиденная настройка - например, попытка инициализировать конкретным значением интеллектуальные указатели, созданные по умолчанию, - приводит все тщательно разработанные классы библиотеки в плачевное состояние. В процессе разработки профамм на них накладываются определенные Офаничения. Следовательно, библиотеки, ориентированные на разработку профамм, должны давать пользователю возможность формулировать свои собственные оцраничения, а не навязывать встроенные. Раз и навсегда законсервированные проектные решения могут оказаться так же непригодными для библиотек, ориентированных на разработку программ, как, например, константы, явно заданные в обычных профаммах. Разумеется, наборы "наиболее популярных" или "рекомендуемых" готовых решений вполне допустимы, если профаммист может изменять их по мере надобности. Эти проблемы иллюстрируют неблагоприятную ситуацию, сложившуюся в области разработки библиотек. Большого количества низкоуровневых универсальных и специализированных библиотек, облегчающих разработку профамм, т.е. структур высо- кого уровня, практически нет. Это парадоксальная ситуация, поскольку любое нетривиальное приложение воплощает некие проектные рещения. Следовательно, библиотеки, ориентированные на создание программ, должны были бы найти применение в больщинстве приложений. Этот пробел попытались заполнить специальные среды разработки (frameworks). Однако они в основном предназначены для того, чтобы связать приложение с конкретным проектным рещением, а вовсе не для того, чтобы помочь пользователю выбрать и настроить свой проект. Если профаммист стремится реализовать свой оригинальный проект, он должен начинать все "с нуля", создавая классы, функции и т.п. 1.3. Опасно ли множественное наследование? Рассмотрим класс TemporarySecretary, производный от классов Secretary и Temporary одновременно. Класс TemporarySecretary обладает свойствами обоих классов, описывая как секретаря, так и временно нанятого служащего. Кроме того, он может иметь и свои особенности. Это наталкивает на мысль, что множественное наследование может помочь нам справиться с экспоненциальным ростом количества проектных решений с помощью малого числа тщательно подобранных базовых классов. В таком случае пользователь смог бы создать класс для многопоточного интеллектуального указателя, подсчитывающего количество ссылок (reference-counted smart ponter), выведя его из некоторого класса BaseSmartPtr и классов MultiThreaded и RefCounted. Любой опытный разработчик классов знает, что этот наивный подход не работает. Анализ причин, по которым множественное наследование не позволяет воплощать гибкие проектные рещения, помогает найти правильный выход. Проблемы заключаются в следующем. 1. Механика. Не существует стандартного кода, позволяющего объединить наследуемые компоненты в одно целое стандартным образом. Единственным инсфу-ментом, дающим возможность объединить классы BaseSmartPtr, миТ-tiThreaded и RefCounted, является языковый механизм, называемый множественным наследованием (multiple inheritance). Для объединения базовых классов применяется простая суперпозиция и устанавливаются правила доступа к их членам. Это соверщенно недопустимо и работает лищь в простейших случаях. Чаще всего для достижения желаемого эффекта профаммист вынужден тщательно насфаивать поведение наследуемых классов. 2. Информация о типах. Базовые классы не имеют достаточно информации о типах, чтобы выполнить свою работу. Представим, например, что мы пытаемся реализовать глубокое копирование интеллектуального показателя с помощью наследования от базового класса Deepcopy. Какой интерфейс должен иметь класс DeepCopy? Очевидно, что он вынужден создавать объекты, имеющие тип, о котором ему ничего не известно. 3. Изменение состояния. Различные аспекты поведения, реализованные в базовых классах, должны влиять на одно и то же состояние. Это означает, что они долж- Этот при.мер создан на основе старого аргумента, который Бьярн Страуструп (Bjam Strous-trup) привел в пользу множественного наследования в первом издании книги "The С++ Programming Language" (см. Страуструп Б. "Язык програ.ммирования С++". - М.: Мир, 1990. - Прим. ред.). В то время множественное наследование в языке С++ еще не было реализовано. 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 |