Компонентное программное обеспечение, ч. 4
© 1997, Cuno Pfister (Oberon Microsystems, Inc.)
© 2005, русский перевод: Ермаков И.Е.
Для публикации текста требуется письменное разрешение автора и переводчика.
4 Средства разработки
Компонентное ПО — это хорошая идея, но без компонентов она не будет работать. Компоненты сначала нужно создать, а это требует подходящих средств разработки. И даже если компоненты уже есть, должны быть инструмент для их сборки. Некоторые инструменты могут предназначаться для сборки компонентов, другие — для их конструирования, а некоторые подходят для того и другого. Небольшое число продуктов хорошо подходит для разработки и развития целых компонентных каркасов.
Рынок для средств сборки будет по меньшей мере в десять раз шире рынка средств конструирования. В этой главе мы рассмотрим те вопросы, которые поднимаются компонентным ПО касательно языков программирования, каркасов и сред разработки. И сейчас мы обзорно рассмотрим BlackBox Component Builder.
4.1 Языки
Какие языки программирования могут использоваться для разработки компонентов. Короткий ответ: почти любой. Однако, мы попытаемся дать более подробный и менее поверхностный ответ в следующем тексте.
В основном большинство языков сегодня выглядят похоже на разновидность C или Pascal, например, С++ и Java подобны C, в то время как Ada, Component Pascal и даже Visual Basic и Eiffel подобны Pascal. Lisp, Smalltalk и некоторые другие «экзотические» языки программирования составляют сравнительно небольшое меньшинство. Например, в 1995 году опрос более 400 университетов показал, что 65% из них используют Pascal-подобный язык, около 16% спользуют C-подобный язык, а остальные распределились по другим языкам.
Пока вы используете C- или Pascal-подобный язык, вы можете быть уверены, что найдется необходимое число способных разработчиков. Стоимость изучения другого языка того же стиля, например, Component Pascal, если вы знаете Pascal, незначительны по сравнению со стоимостью изучения огромных интерфейсов современных библиотек и операционных систем.
Это значит, что есть достаточная свобода в выборе языка для конкретной задачи.
На практике, языково-независимые объектные модели значительно увеличили эту свободу в последнее время. Большинство языков сегодня дают доступ к языково-независимым объектным моделям, например, COM. Объектные модели добавляют динамические возможности, которых может недоставать языку. Языково-независимые объектные модели делают языковые решения менее стратегическими, чем они обычно были: выбор одного языка для одного компонента не мешает использовать второй язык для другого компонента. Использование конкретного языка больше не создает острова. Следовательно, больше нет причин не использовать лучший язык для данной задачи.
Создает ли язык различия вообще, если кодирование является только малой частью общей стоимости проекта? Фактически, мелкие синтаксические нюансы, например, как выглядят конструкции циклов, не дают никаких заметных отличий в стоимости проекта (предполагая, что запретный GOTO сейчас более или менее умер). Следовательно, сравнение языков касательно «программирования в малом» не имеет большого значения. Но современный язык программирования — это нечто большее, чем только нотация для реализации мелких алгоритмов. Хорошо спроектированный язык программирования также поддерживает программирование в большом. Чтобы оставаться управляемыми, большие программы должны разбиваться на компоненты, которые взаимодействуют только через определенные интерфейсы. Хороший язык программирования может использоваться не только как язык реализации, но также и как язык описания и спецификации интерфейса. Интерфейсы определяют архитектуру системы: те части, которым разработчик может доверять; статические свойства системы, которые не могут быть нарушены; структурную суть компонентов. Язык, который позволяет выражать явно («статически») большую часть архитектуры системы в своей нотации для интерфейсов, делает возможным написание средств, которые помогут обеспечить согласованность реализации и спецификации. Компилятор может дать сигнал о нарушениях интерфейса еще во время компиляции, когда исправление ошибки обходится недорого. Проверки во время выполнения дают возможность выявить другие нарушения интерфейсов как можно раньше, во время тестирования.
Статическая выразительность языка и инструментальная поддержка, задействованная им, становятся еще более важными, когда интерфейсы изменяются, что часто случается на этапе проектирования и создания прототипа, или позже, на этапе эксплуатации ПО (который отнимает около 80% от общих затрат разработки!). Фактически, архитектура большой системы неизменно ухудшается со временам, когда делаются изменения и расширения. Улучшение архитектуры системы, при котором отбрасывается старый багаж и придается гибкость некоторому подмножеству интерфейсов и компонентов, называется рефакторингом. Сегодня этой стороной разработки пренебрегают более всего. Но компонентно-ориентированные языки являются мощными инструментами рефакторинга, которые уменьшают время, стоимость и риск, связанные с изменением частей существующей системы.
Это значит, что состоятельный язык программирования может привнести отличия в большинство фаз жизненного цикла программных компонентов, и в жизненный цикл компонентной программной системы (который может быть намного длиннее жизненного цикла любого из ее компонентов)! Следовательно, распространенное мнение, что выбор языка программирования не имеет значения, основан на слишком плоской аргументации, которая опускает измерение «программирование в большом».
Языки программирования, которые поддерживают явно формулируемые интерфейсные конструкции, например, типы, объявления, называются «статическими» или «третьего поколения» языками. Примеры таких языков — Pascal, C и C++. Их главное преимущество в том, что они могут быть применены к очень большому числу очень больших задач, и они эффективны. Языки, которые которые избегают статических конструкций, чтобы получать предельную гибкость с наименьшими усилиями, называются «динамическими» или «четвертого поколения» языками. Динамические языки поддерживают инкрементную загрузку кода, сборку мусора и тесно связаны с поддержкой среды разработки. Их превосходство — в быстрой разработке маленьких частей низкотехнологичного кода, например, сценариев для сборки компонентов. Их основное достоинство — что «все проходит», то есть, в них нет жесткой системы типов, лишних секций объявлений или других подобных ограничивающих статических конструкций. Даже с самыми агрессивными технологиями оптимизации они обычно медленнее, чем статические языки. Тесная интеграция со средой означает, что их использование удобно, и что объектная модель может быть вставлена непосредственно в язык, так что взаимодействие между компонентами становится намного более легким, чем при использовании языково-независимой объектной модели.
Относительно компонентного ПО интересно отметить, что различия между статическими и динамическими языками — это причина того, что OLE и OpenDoc пришли к двум уровням программируемости: на уровне объектной модели (статический) и на уровне автоматизации (динамический).
Более современные языки, такие как Component Pascal и Java, доказали, что различия между статическими и динамическими языками могут быть преодолены, то есть, что язык может собрать в себе большинство преимуществ обоих сторон. Такой гибридный язык позволяет гибкую разработку или модификацию реализаций компонентов. С другой стороны, он позволяет жестко определять интерфейсы, так что соответствие этим интерфейсам может быть проверено автоматически. Мы называем такие языки, подобны Component Pascal и Java, компонентно-ориентированными». Пока что мы не говорили об объектной ориентированности, которая появилась и в тех, и в других языках. Давайте взглянем на ООП и на то, как компонентная ориентированность перерастает ООП.
Нет всеобщего согласия относительно того, что есть ООП и чем оно должно быть, но большинство потребует от ООП-языка следующих характеристик: объекты, классы, полиморфизм, позднее связывание и наследование. Объект инкапсулирует состояние и поведение. Поведение объекта доступно через процедуры, связанные с типом объекта, так называемые методы. Класс является планом, чертежом для реализации объектов данного типа. Во время выполнения может быть создано произвольное число экземпляров класса, то есть, объектов.
Полиморфизм означает, что достаточно похожие объекты могут подменять друг друга, то есть, во время выполнения переменной могут быть присвоены объекты различных типов. Объекты являются «достаточно похожими», если они реализуют одинаковый интерфейс, то есть, следуют одному и тому же контракту. Например, механизм хранилищ должен принимать любой объект, который является хранимым, то есть, который реализует интерфейс хранилища. Интерфейс хранилища может содержать две процедуры Externalize и Internalize, где Externalize записывает содержимое объекта в файл, а Internalize читает содержимое объекта из файла.
Позднее связывание значит, что поведение объекта может быть различным, в зависимости от того, какой динамический тип он имеет. Например, некоторые хранимые объекты имеют различное содержимое, и следовательно, различные реализации своих процедур Externalize. Объект «треугольник» сохраняет координаты своих трех вершин, а текстовый объект сохраняет последовательность символов, которую он содержит. Название «позднее связывание» происходит из факта, что из-за полиморфизма во время компиляции неизвестно, будет ли сохраняемый объект треугольником, текстом или чем-то еще. Следовательно, решение о том, какого типа объект сохраняется и где находится правильный код его Externalize, должно приниматься позже, а именно — во время выполнения.
Сокрытие информации означает, что интерфейс и реализация объекта различаются между собой. Внешние взаимодействия происходят только через интерфейс, а реализация остается скрытой. Это позволяет позднее изменить скрытые детали реализации, без нарушений в работе клиентов.
Полиморфизм, позднее связывание и сокрытие информации работают совместно, чтобы сделать возможным четкое разделение интерфейса и реализации, и следовательно — обеспечить поддержку для компонентного ПО. Однако, наследование реализации дает нечто другое. Это значит, что объект может «наследовать» некоторое поведение от другого объекта, «перегрузить» часть его и добавить к нему свое собственное новое поведение. Это удобная форма повторного использования кода. Она хорошо работает, если наследующий объект жестко придерживается контракта того объекта, от которого он наследует, поскольку тогда он может использоваться вместо него без нарушения контракта с клиентами.
К сожалению, если используется наследование, очень тяжело предупредить повторный вход в объект, то есть, само-рекурсия может вести к непредсказуемым изменениям состояния унаследованного объекта. Например, поток управления в пределах объекта может перескакивать вверх и вниз между базовым классом и подклассом. Если некоторая деталь реализации базового класса изменяется в новой версии, подкласс может перестать работать, поскольку его предположения больше неверны.
Сокрытие информации делает возможным задать контракт между подклассом и базовым классом однозначно (так называемый интерфейс специализации). Наследование реализации — это такая тесная связь между объектам, которая на практике требует, чтобы исходный код наследуемого объекта был открыт, то есть, сокрытие информации отбрасывается, и реализация должна рассматриваться как интерфейс («наследование разрушает инкапсуляцию»). Мы ранее уже встречались с этой проблемой под названием «семантическая хрупкость базового класса». В будущем кто-нибудь предложит практические правила для ограниченного вида наследования реализации, который не ведет к проблеме семантической хрупкости. Но сегодня это все еще тема для исследований. В книге [Shypersky97] эта проблема рассматривается в деталях.
Хорошим высказыванием является то, что наследование реализации — это GOTO девяностых. Подобно GOTO шестидесятых, наследование очень удобно, программисты его используют, не всегда очевидно, как без него можно обойтись, решение без него может сделать программу длиннее, и сам вопрос может вызвать горячие дискуссии. Но с фундаментальной точки зрения, наследование имеет сходство с GOTO в том, что приводит к неконтролируемым передачам управления, которые затрудняют понимание программы и делают рискованным ее изменение.
Наследование вредно, если оно используется через границы компонентов. Пока наследование используется внутри компонента, оно не опасно. Так как компонент является черным ящиком, он может быть реализован с использованием наследования реализации, функционального программирования, ассемблера и чего угодно, лишь бы это годилось для конкретного компонента. Единственное, что имеет значение, — это верная реализация интерфейса, то есть, соблюдение контракта с окружающим миром. Внутри компонента вы имеете полный контроль над всеми своими исходными кодами и можете свободно менять внутренние интерфейсы, как вам будет угодно.
Компонентно-ориентированные языки помогают создавать более надежное компонентные программные системы быстрее, поскольку такие языки предоставляют «компонентно-ориентированные» возможности в дополнение к ООП-возможностям (полиморфизм, позднее связывание и сокрытие информации).
Безопасность — одна из таких возможностей. Безопасность означает, что язык гарантирует некоторые базовые правила для компонента, которые не приходится помещать заново в контракт каждого компонента. В частности, безопасный язык программирования гарантирует целостность памяти, то есть, один компонент не может разрушить память других компонентов. Это упрощает договорные обязательства каждого объекта, так как правильное управление памятью — а его отсутствие является причиной более половины всех ошибок программирования — может просто считаться само собой разумеющимся. Это достигается предоставлением службы сборки мусора, то есть, память возвращается автоматически, когда она больше не используется. Сборка мусора невидима и освобождает программиста от ручной работы.
Безопасные языки дают тот тип защиты, который желателен в программной среде, состоящей и тесно взаимодействующих компонентов, особенно если иметь в виду, что здесь традиционные механизмы аппаратной защиты неприменимы.
В будущем все больше и больше заказчиков будут требовать использования безопасных языков для создания компонентов, поскольку это может значительно уменьшить количество загадочных сбоев, и следовательно, недоверие к компоненту. Как только компоненты станут широко распространенными, вопросы качества обязательно окажутся на главном месте.
Компонентно-ориентированный язык также помогает достичь безопасности на более высоком уровне, нежели просто целостность памяти. Сокрытие информации — часть ответа, поскольку оно позволяет прятать, а значит защищать, детали реализации. Большинство ООП-языков ограничивают сокрытие информации отдельными классами. Это очень ограничено, так как обычно несколько классов могут кооперироваться для предоставления некоторой услуги. Такие классы должны иметь возможность тесно работать вместе, при этом их кооперация должна быть защищена от внешних воздействий. Это значит, что такие классы должны иметь их собственный закрытый интерфейс, который не будет делиться больше ни с кем. Чтобы гарантировать согласованность закрытых контрактов такого рода, язык программирования должен поддерживать сокрытие информации вокруг нескольких классов. Чтобы сделать это, язык должен предоставлять конструкцию «модуль» или «пакет», подобно языкам Component Pascal или Java.
Сокрытие информации над отдельными классами является необходимым требованием для компонентно-ориентированного языка, поскольку это позволяет программному архитектору создавать заказные безопасные свойства (то есть, инварианты) в компонентной программной системе.
Компонентно-ориентированный язык подразумевает, что реализация предоставляет объектную модель, которая поддерживает динамическую загрузку новых компонентов. Обычно это библиотечная служба, которая позволяет явно загружать компонент по его имени или иному подходящему идентификатору. Такое средство называется поддержкой метапрограммирования, которое позволяет одной программе манипулировать (в данном случае загружать) другую программу. Это требует обширной информации о типах во время выполнения (RTTI), которая идет гораздо дальше минимальной информации, поддерживаемой ООП-языками, такими как C++.
Следующая таблица дает историческую перспективу эволюции императивного программирования:
Десятилетие | Технология программирования | Ключевое нововведение |
---|---|---|
1940-е | машинные коды | программируемые машины |
1950-е | ассемблерные языки | символы |
1960-е | языки высокого уровня | выражения и независимость от машины |
1970-е | структурное программирование | структурные типы и управляющие конструкции |
1980-е | модульное прогаммирование | отделение интерфейса от реализации |
1990-е | объектно-орентированное пр-е | полиморфизм |
2000-е | компонентно-ориентированное пр-е | динамическая и безопасная композиция |
Таблица 4-1. Эволюция императивного программирования
Каждый шаг является эволюцией своего предшественника и характеризуется одним или двумя ключевыми нововведениями. Шаг от машинных кодов к ассемблеру опирался на введение символов, такой шаг часто был центральным в других областях человеческой деятельности (например, алгебра). Языки высокого уровня ввели независимость от машины и выражения, что видно на примере Fortran, название которого является сокращением от «formula translation». Структурное программирование сделало два шага: определяемые типы были применены к данным; и код, на каждом уровне детализации был ограничен формой «трубки» с одним входом и одним выходом. Модульное программирование, примерами которого являются Modula-2 и Ada, ввели формальность интерфейса для каждого программного компонента. Объектно-ориентированное программирование добавило полиморфизм, то есть, расширяемые типы. В конце концов, шагом, который сделал реальностью компонентное ПО как сотрудничество независимо развиваемых компонентов, стала динамическая загрузка и безопасная интеграция компонентов.
Рис. 4-2. Компонентно-ориентированные языки программирования
Теперь мы можем сделать самые главные выводы относительно компонентного ПО и языков программирования: выбор языка может влиять на стоимость проекта в течение всего жизненного цикла компонента; некоторые языки поддерживают компонентное ПО решительно лучше, чем другие; а стандартизированные языково-независимые объектные модели делают выбор языка менее стратегическим решением, чем это было раньше.
Коротко: ничто не мешает использовать для работы лучший язык.
Вероятно, выбор языков будет всегда. С одной стороны, существующие языки никогда не умрут; это Fortran, Cobol, Basic или Pascal. С другой стороны, одноязыковые подходы не будут слишком успешными. Уже несколько раз думали, что некоторый язык будет последним; как в случае с PL/1, Ada, C++ и теперь — Java.
Фактически, стандартизированные объектные модели даже делают разработку новых языков более привлекательной, если (и только если) разработчики этих языков заботятся о хорошей поддержке основных объектных моделей.
Таблица 4-3 сравнивает несколько языков программирования в отношении их поддержки компонентного ПО. Мы не имели намерения сравнить все существующие языки; здесь собраны только самые основные языки общего назначения.
Аспект | Pascal | Modula-2 | C | C++ | Smalltalk | Java | Component Pascal |
---|---|---|---|---|---|---|---|
структурир. синтаксис | да | да | нет | нет | нет | нет | да |
простота и регулярность | да | да | нет | нет | да | нет (1) | да |
статические объекты | да | да | да | да | нет | нет | да |
статические типы | да | да | да | да | нет | да | да |
динамические типы | нет | нет | нет | да | да | да | да |
эффектив. трансляция | да | да | да | да | нет | да (2) | да |
динамич. связывание | нет | да (3) | да (3) | да | да | да | да |
сокрытие информации | нет | да (4) | нет | да (5) | да (5) | да (4) | да (4) |
полиморфизм | нет | нет | нет | да | да | да | да |
наследование | нет | нет | нет | да | да | да | да |
множ. наследование | нет | нет | нет | да | нет | да (6) | нет |
полная безопас. типов | нет (7) | нет (7) | нет (7, 8) | нет (9) | да | да | да |
сборка мусора (10) | нет | нет | нет | нет | да | да | да (11) |
динамическая загрузка | нет | нет | нет | нет | да | да | да |
раздел. интерф./реализ. | нет | нет | нет | нет | нет | да (6) | нет (12) |
(1) Определение языка включает 34 класса и сотни методов; существует множество исключений из правил; возможности языка взаимоблокируются так, что невозможно объяснить одно без знания остального
(2) Отсутствие статических типов и параметров, передаваемых по значению, накладывает ограничения на быстродействие
(3) Процедурные типы / указатели на процедуры
(4) Модули / пакеты
(5) Только в отдельных классах, никакие инварианты между классами не могут гарантироваться
(6) Интерфейсы для (множественного) наследования интерфейса; классы для (одиночного) наследования реализации
(7) Небезопасные вариантные записи; явное освобождение памяти
(8) Небезопасные указатели
(9) Унаследованные небезопасные возможности C
(10) Сборка мусора необходима для достижения полной безопасности типов
(11) Образуется меньше мусора, чем в языках без статических типов, поэтому — более эффективен
(12) По соглашению, наследование реализации редко используется, поэтому такое разделение не слишком важно
Таблица 4-3. Сравнение языков программирования
Заметим, что даже настоящие компонентно-ориентированные языки, такие как Java или Component Pascal, не превосходны. Например, они не поддерживают пред- и пост- условия в интерфейсе, как Eiffel [Meyer89]. (Eiffel не является компонентно-ориентированным. Его система типов требует глобального анализа, что противоречит идее компонентного ПО; в нем не хватает конструкции пакета; и его библиотеки сильно связаны наследованием реализации.) Также в Java и Component Pascal желательна совместимость с отдельно откомпилированными и загруженными компонентами, чтобы увеличить возможности повторного использования кода. (Шаблоны С++ не совместимы с компонентным ПО в том смысле, что шаблон не может быть скомпилирован отдельно от своих клиентов; каждое инстанциирование генерирует новый код, и дублирование кода нарастает.)
Java сверхпереносима из-за своей виртуальной машины. В этом ее огромная сила. Но это — также и слабость, поскольку делает трудным или невозможным взаимодействие с традиционным ПО и оборудованием без отката на другие языки.
Component Pascal — это современный компонентно-ориентированный язык, который дополняет Java, поскольку он меньше и проще, и не привязан к виртуальной машине. Эти факторы, в частности, важны для встроенных систем, таких как автоматизированные системы управления. По причинам стоимости, даже быстродействующие встроенные системы имеют ограниченное количество памяти, и часто для таких приложений требуется писать новые драйверы устройств. Операционная система Portos [Portos] разработана для встроенных приложений реального времени, которые имеют жесткие ограничения по времени, но должны быть расширяемы компонентно-ориентированным образом. Portos реализован полностью на Component Pascal, и поддерживает приложения, написанные на Java.
Два языка достаточно похожи, что могут даже разделять один и тот же сборщик мусора реального времени. Конечно, из-за своей относительной простоты и регулярности, Component Pascal также хорошо подходит и для обучения программированию, хотя он и не разрабатывался конкретно для преподавания. Он был разработан для поддержки разработки и развития сложных компонентно-ориентированных программных систем.
Предыдущие обсуждения дали нам глубокое понимание того, что есть компонент, через рассмотрение аспектов программирования и желательной языковой поддержки. Чтобы дополнить этот раздел некоторым дополнениями о природе компонентов, давайте вернемся к определению компонента, данному ранее:
Компонент — это конструктивная единица с контрактно определенным интерфейсом и только явными контекстными зависимостями. Компоненты могут развертываться независимо друг от друга и собираться третьей стороной.
Теперь мы можем добавить несколько дальнейших наблюдений и описаний в эту краткую формулировку. Компонент — это черный ящик, который предоставляет некоторые услуги через так называемый интерфейс экспорта, и требует некоторых услуг от других компонентов, через так называемый интерфейс импорта. Интерфейс импорта включает в себя контекстные зависимости компонента.
Компонент является единицей упаковки, развертывания и загрузки. Компонент может быть загружен, в памяти может быть в одно и то же время не более одного экземпляра компонента, то есть, он не существует во многих экземплярах, как это обычно бывает с объектами. Компонент редко копируется; обычно только для развертывания и распространения.
Компонент обычно состоит из нескольких классов и, возможно, некоторых объектов, например, фабрик класса. Компонент имеет конфигурацию, то есть, определенное состояние при загрузке. Компонент определяет конфигурацию по умолчанию. Это часто достигается упаковкой некоторых файлов данных, так называемых ресурсов, вместе с кодом компонента. Этими ресурсами могут быть формы, строки или другие параметры; они являются неотъемлемой частью компонента. Некоторые части конфигурации компонента могут изменяться во время выполнения, например, путем замены конфигурационных объектов, таких как фабрики класса. Компонент должен считаться неизменным, то есть, перманентные изменения его конфигурации записываются вне его, например, в файлах настройки или системном реестре.
Коротко говоря, компонент является неизменяемой сущностью, которая внутренне состоит из классов, конфигурационных объектов и ресурсов. В Component Pascal наименьшим компонентом является модуль.
4.2 Каркасы (Frameworks)
Компонент имеет интерфейс экспорта. Этот интерфейс может содержать как минимум одну процедуру, через которую можно получить доступ к фабрике класса. Фабрика класса, в свою очередь, может быть использована для размещения новых объектов тех классов, которые реализует компонент. Но интерфейс экспорта может также определять интерфейсы для других компонентов. Такой интерфейс создает стандарт, которому следуют эти другие компоненты. Компоненты, которые следуют одному или нескольким общим стандартам, могут взаимодействовать, компоненты, которые не следуют общему стандарту, взаимодействовать не могут.
Например, OLE определяет множество интерфейсов COM. Компоненты COM, которые реализуют объекты с этими интерфейсами, могут использоваться в составных документах. Как общее определение прикладного каркаса в [Frameworks97] дается следующее определение:
Компонентный каркас — это собрание компонентных интерфейсов, которые образуют абстрактную схему решений для семейства связанных проблем.
Компонентный каркас — это собрание контрактов, то есть, правил, которые определяют, как объекты могут работать вместе. Некоторые из этих правил могут быть просто соглашениями. Другие правила может быть легко использовать, поскольку каркас предоставляет вместе с интерфейсами подходящий код. Например, каркасы GUI предоставляют стандартное поведение для приложений, окон, меню и т.п. Если стандартное поведение не заменено, оно может быть расширено соответственно правилам, например, для реализации основных принципов пользовательского интерфейса для базовой платформы. Другие правила могут действительно исполняться каркасом, например, средства рисования могут отсекать рисование за границами окон. Каркас предоставляет некоторые службы только через безопасный код, который гарантирует необходимые инварианты (например, инвариант «рисование всегда происходит внутри окон приложения»). Ключом к такому контролю, который обычно затрагивает несколько объектов, является информация, скрываемая между несколькими классами, что обсуждалось в предыдущем разделе.
В противоположность каркасам старых приложений, компонентный каркас определяет правила для независимо разрабатываемых и динамически загружаемых компонентов, а не для классов, которые компонуются вместе в монолитное приложение. Компонентный каркас может предоставлять интерфейсы, возможно вместе с некоторыми процедурами. В противоположность большинству прикладных каркасов, компонентные каркасы являются черными ящиками, то есть, их можно использовать без доступа к их исходному коду. Подобно отличному конракту, чистый интерфейс-«черный ящик» — это идеал, к которому на практике можно приближаться.
Прикладные каркасы, которые сильно опираются на наследование реализации, являются «белыми ящиками», которые публикуются вместе с их исходными кодами. В противоположность прикладным каркасам, компонентный каркас не обязан быть библиотекой классов. Фактически, компонентный каркас может вообще не содержать кода; он может быть только коллекцией интерфейсов. В этом отличие от старого понимания термина «каркас», в котором наличие реального кода играло очень важную роль. В компонентных каркасах фактический код существует большей частью для закрепления правил.
Много лет назад Microsoft опробовала стратегию «Windows повсюду», то есть, одна операционная система для всех возможных нужд. Сегодня Microsoft двигает стратегию «ActivePlatform». Одна из интерпретаций этого такова, что предоставление реального кода (т.е. Windows) не является обязательной, что лишь собрание интерфейсов (то есть, COM-интерфейсов, составляющих ActiveX Platform) имеет значение, если только существует некоторая реализация этих интерфейсов. Конструкторы аппаратуры поняли необходимость архитектуры против реализации во времена IBM 360 — шестидесятые; разработчики программного обеспечения только теперь достигают этого понимания.
Каркас содержит архитектуру, то есть, схему (design) расширяемых компонентов и их взаимодействий. Реализация расширенного компонента в соответствии со стандартом, определенным интерфейсами каркаса, является формой повторного использования: использования схемы. Поскольку разработка новой схемы гораздо более тяжела, чем реализация существующей, повторное использование схемы важнее, чем повторное использование кода. Создание новой схемы требует знания прикладной области, опыта конструирования, способности отличать существенные вопросы от незначительных и способности осознавать и изобретать подходы (patterns). Поскольку плохие схемы могут стать слишком дорогими, только самые опытные разработчики могут создавать каркасы. Другие программисты должны фокусироваться на хорошей реализации существующих схем, то есть, разработке компонентов, которые реализуют данные интерфейсы. Еще больше программистов будут концентрироваться на сборке компонентов.
Включив в себя схемы, каркасы до некоторой степени стали конкурентами инструментам автоматизированного программирования (Computer-Aided-Software-Engineering, CASE). Преимущество каркасов в сравнении с объектно-ориентированными методами проектирования (OODM) заключается в том, что схема непосредственно определена на настоящем языке программирования, и поэтому соответствие расширенных компонентов может быть проверено компилятором. Однако, при условии, что они станут более гибкими, CASE-инструменты могут все играть важную роль в документировании каркасов, и, возможно, в автоматической генерации некоторого рода компонентов.
Хорошо описано, что проектирование каркасов является итеративным процессом [Frameworks97]. На каждом витке разрабатывается новое решение, основанное на каркасе. Обычно этот опыт приводит к некоторым изменениям каркаса и других уже существующих компонентов расширения. Есть надежда, что после нескольких итераций каркас станет достаточно стабильным, чтобы новые компоненты расширения могли разрабатываться без дальнейших изменений каркаса. Для нетривиальных каркасов это процесс может легко растянуться на несколько лет. Идущее сейчас бурное производство новых каркасов для CORBA, ActiveX и Java опасно из-за того, что часто их определение разрабатывается быстрее, чем накапливается необходимый опыт. Более того, вопросы стандартизации и сертификации еще больше усложняют и без того непростой процесс разработки.
Время, необходимое для получения хороших проектов каркасов, имеет глубокий финансовый смысл. В частности, разработчики в больших корпорациях должны быть уверены, что разработка повторно используемых программных компонентов во всем подобна производству продуктов для мирового рынка: она требует потенциально больших начальных инвестиций, профессиональной документации, фазы анализа рынка, (внутреннего) маркетинга и продаж, и дальнейшего сопровождения. Такой подход кажется слишком дорогим, если отдача от инвестиций вычисляется от каждого проекта вместо рассмотрения более длинных сроков, учета сроков выхода на рынок (time-to-market reduction), низких рисков провала и гораздо лучшей развиваемости компонентных программных систем.
4.3 Среды разработки
Большинство пользователей ПО сегодня хотят, чтобы оно решало проблемы, а не было проблемой само. Следовательно, нас интересуют решения, а не программирование. В этом смысле программирование является нежелательной деятельностью, которой надо по возможности избегать. К сожалению, любой вид настройки, включая сборку компонентов, в некоторой степени требует программирования. Многие производители инструментария обещают, что вы сможете настроить существующее ПО «без написания ни единой строчки кода».
Не верьте этому. Компьютеры настолько немые, что вы должны точно сообщить им, что вы от них хотите. Называть это программированием или нет, это все же требует необходимых навыков, таких как точность и ясность.
Например, есть некоторые инструменты для сборки компонентов, которые позволяют задавать поведение графически, или хуже, смешивая графику и текст. Картинка стоит из тысячи слов. Это верно для описания специфических ситуаций, но редко подходит для описания поведения или общих правил. Инструменты графического программирования могут оказаться полезными лишь в некоторых обстоятельствах, когда проблема очень ограничена в размерах и тематике. Небольшие конечные автоматы (finite state machines) — хороший пример. Для более сложных проблем, разработка становится более громоздкой — и эксплуатация более громоздкой — чем в случае с полноценным языком программирования.
В идеале, средство разработки компонентного ПО должно строиться на компонентно-ориентированном языке. Инструментарий не может полностью компенсировать слабости языка программирования. Например, безопасный язык предотвращает целые классы ошибок, что проще и дешевле, чем заставлять программистов потом их устранять, используя причудливые и дорогие инструменты для отладки. Касательно отладки, старое правило гласит: чем раньше отловлена ошибка, тем менее дорого ее исправление. Если язык имеет выразительную систему типов, его компилятор помогает обнаруживать ошибки прямо во время компиляции, перед тем, как вы передадите компонент заказчику. Даже если ошибка может быть обнаружена лишь во время выполнения, ее раннее обнаружение становится гораздо более вероятным. Проверка возможных условий ошибок, таких как нарушение предусловий процедур, называется защитным (defensive) стилем программирования. Такой стиль желателен для любого промышленного ПО.
Для сборки компонентов от средства разработки требуются базовые функции RAD (Rapid Application Development), известные по таким средам, как Borland Delphi или Microsoft Visual Basic. В основном это коллекция элементов управления — кнопок, текстовых полей, списков и т.п. — и графический редактор для элементов управления. Обычно редактор позволяет интерактивно помещать элементы управления на форму и сохранять их расположение в файл. Это простая форма составного пользовательского интерфейса, с формами и элементами управления как перманентными объектами. Инструмент не должен генерировать из формы исходный код, так как это делает последующее редактирование невозможным без доступа к среде разработки, или даже без исходных кодов. «Мастера» генерирования исходного кода — модная часть многих RAD-сред. Однако это должно лишь освободить вас от очевидной и повторяюзщейся работы. При этом не должно происходить никакой «магии», которую вы не понимаете; в противном случае инструмент становится вашим мастером, в то время как должно быть наоборот. В общем, можно сказать, что чем лучше язык программирования, тем меньше надобности в инструментах генерации кода, поскольку язык позволяет поместить повторяющийся код в библиотеки, вместо того, чтобы повторять его снова и снова с незначительными изменениями.
Генерация из формы исходного кода — одна из крайностей. Но берегитесь также и другой. Другая крайность — это исходный код, внедренный в элементы пользовательского интерфейса. Должна быть возможность рассматривать полный исходный код вашего компонента или компонентной сборки без доступа к элементам пользовательского интерфейса, то есть, код должен быть изолирован от кнопок и других «крысиных нор» («rat holes»). Если части вашего кода разнесены по сотням крысиных нор, эксплуатация становится неряшливым занятием, что лишает программу потенциала развития. А такой потенциал необходим, поскольку успешные компонентные сборки обычно приспосабливаются к новым нуждам и потому растут со временем. Поэтому хорошо подумайте, прежде чем выбрать простой сценарный инструмент, который может только собирать компоненты. Вы сможете решить очень быстро 80% ваших сиюминутных проблем, но оставшиеся 20% могут отнять больше, чем первые 80%. В больших компаниях могут быть специальные группы для конструирования компонентов и для их сборки, но разделение должно происходить по организационному признаку, а не по инструментарию.
Гибкие обозреватели интерфейсов и исходных кодов и средства поиска полезны для быстрого доступа к информации о компонентах библиотек и ваших собственных компонентов. Символьный отладчик должен поддерживать защитный стиль программирования, в частности потому, что вы можете отладить только ваши собственные компоненты. Компоненты, которые вы купили, обычно будут черными ящиками без исходных кодов, поэтому вы должны находить ошибки в ваших собственных кодах как можно раньше, прежде чем окажется затронуто поведение других компонентов, которые опираются на ваши.
Компонентно-ориентированный язык программирования уже поддерживает понятие компонента-черного ящика, например, через конструкции модуля и пакета. Без таких конструкций упаковка компонентов становится сложной. Чистые объектно-ориентированные среды, такие как Smalltalk, часто страдают от этой проблемы. В таких системах становится крайне сложно найти все объекты, которые вам нужно собрать вместе в один компонент, поскольку у вас нет способа узнать, что вы потеряли несколько объектов. Упаковка компонентов — это одна из проблем таких недоструктурированных сред, эксплуатация — другая проблема. Учитывая, что около 80% всех инвестиций в ПО сегодня затрачиваются на эксплуатацию, есть веские причины не делать так в будущем.
Быстрая разработка приложений необходима, чтобы идти в ногу с быстро меняющейся деловой средой, что сегодня характерно. Иногда это означает, что самым подходящим подходом будет создать «одноразовый» код как можно быстрее, так что он может быть использован, списан и быстро заменен. Это лучше, чем создание прекрасного решения для бизнеса, который в такой форме уже не существует. Например, черновые расширения с наследованием реализации вполне подойдут, если ПО будет выброшено раньше, чем его понадобится пересматривать или приспосабливать к новым нуждам. Но быстрая разработка и управляемое развитие могут быть совмещены. С подходящими средствами документирования и рефакторинга компонентно-ориентированные RAD могут поддерживать как быструю разработку, так и «быструю эксплуатацию».
В идеале инструментарий должен поддерживать полный жизненный цикл ПО. Сегодня пока нет средств, которые могли бы делать это для компонентного ПО, а традиционные объектно-ориентированные методы проектирования и CASE опираются на допущения «закрытого» типа для монолитного ПО. Инструменты, подходящие для компонентного ПО, должны поддерживать компоненты-черные ящики, исходный код которых недоступен, управление интерфейсом компонентов, конфигурирование во время выполнения и продолжительное инкрементное развитие.
Последний пункт необходим: жизненный цикл компонентной программной системы может быть значительно длиннее, чем любого из ее компонентов. Успешная компонентная система никогда не умрет, она просто будет постепенно расширяться, или обновляться заменой устаревших компонентов. В этом смысле компонентное ПО можно рассматривать как систематический способ работать с унаследованными частями.
Сборка компонентов и даже их конструирование легки по сравнению с проектированием новых компонентных каркасов. Поскольку управляемое развитие компонентного каркаса так сильно зависит от языковой поддержки гарантированной целостности и расширяемости, использовать не компонентно-ориентированный язык окажется слишком дорогим.
Очень простая быстрая проверка качества инструмента компонентной разработки заключается в том, построен ли сам инструмент из компонентов, то есть, «принимает ли собственные лекарства». В противном случае разработчики инструмента не уверены в его мощности.
4.4 BlackBox Component Builder
FINALIST
Инструмент разработки BlackBox Component Builder базируется на языке Component Pascal, компонентно-ориентированном развитии Pascal и Oberon. Если вы знаете Pascal или Modula-2, вы будете чувствовать себя в Component Pascal как дома и полюбите его повышенное удобство и безопасность, например, сборщик мусора.
BlackBox Component Builder изначально проектировался как компонентное средство разработки. Он предоставляет обычные RAD-возможности, такие как элементы управления и редактор форм пользовательского интерфейса. Однако, он не ограничен только дизайном форм. Элементы управления являются отображениями, а формы — всего лишь один из примеров контейнера отображений. В качестве основы для составного пользовательского интерфейса может быть использован любой другой контейнер, например, гипертекстовый контейнер. BlackBox Component Framework (BCF) определяет общую абстракцию для контейнеров, разработанную таким образом, что детали пользовательского интерфейса скрыты. В Windows контейнерные отображения выглядят и ведут себя подобно контейнерам ActiveX; на MacOS — подобно контейнерам OpenDoc. Версия Windows прозрачно поддерживает OLE, то есть, пользователь не заметит никаких отличий между объектом ActiveX и отображением BCF. BCF платформонезависим, то есть, исходные коды могут быть перенесены на все поддерживаемые платформы простой перекомпиляцией. В настоящий момент есть версии для Windows 3.1/95/NT и для Mac OS 7.
Рис. 4-4. BlackBox Component Builder под Windows
BCF дает обширную поддержку для составных документов и составных пользовательских интерфейсов. Он спроектирован как каркас из черных ящиков, то есть, наследование реализации используется редко и только там, где оно не делает интерфейсы неоднозначными. Особое внимание было уделено гарантиям высокой степени безопасности, например, эффект неверно функционирования отображения будет как можно более локальным.
Одной из самых важных целях для BCF было предоставить великолепную развиваемость (maintainability). Для достижения этого для повторного использования кода чаще используется композиция, чем наследование, логика программы полностью отделена от пользовательского интерфейса, архитектура по возможности явная, то есть, выражена непосредственно в мощной и простой системе типов Component Pascal.
Хотя Component Pascal — динамический язык со сборкой мусора, он компилируется прямо в эффективный машинный код; никакой интерпретации не требуется. Это дает хороший контроль над поведением во время выполнения и позволяет реализовывать даже вычислительно дорогие алгоритмы, например, численные. Можно использовать специальные псевдобиблиотеки, поддерживаемые компилятором, которые дают возможность низкоуровневого программирования, например, написания драйверов устройств.
BlackBox Component Builder на 100% написан на Component Pascal, и компилятор, визуальный конструктор, подсистема гипертекста, и т.п. все реализовано как модули Component Pascal, то есть, компоненты. Даже система времени выполнения с ее сборщиком мусора написана полностью на Component Pascal. Компонент состоит как минимум из одного модуля, компоненты, состоящие из нескольких модулей называются подсистемами. Модуль в Component Pascal — это единица компиляции и загрузки, то есть, большая система может быть инкрементно откомпилирована компонент за компонентом, и точно также инкрементно загружена. Компоненты, которые не используются, вообще не загружаются. Программист может явно загрузить новые компоненты во время выполнения, чтобы расширить возможности системы, когда она уже запущена. Хотя Component Pascal компилируется, компилятор настолько быстр и хорошо интегрирован, что воспринимается как интерпретируемая среда.
BlackBox Component Builder позволяет напрямую работать с не-Component Pascal программным обеспечением. Например, Windows-версия полностью поддерживает Windows NT API, доступ к произвольным DLL и создание DLL. Специальная версия компилятора даже напрямую поддерживает COM и DCOM («Direct-To-COM Compiler with Safer OLE»).
Рис. 4-6 дает архитектурный обзор компонентов, составляющих BCF. Прямоугольники с тонкими рамками — это отдельные модули, прямоугольники с толстыми рамками — это целые подсистемы, то есть, коллекции связанных модулей:
Рис. 4-6. Архитектурный обзор BlackBox Component Framework
На нижнем уровне расположена библиотека самых независимых модулей, предоставляющих много служб низкого уровня, например, математические функции, поддержку метапрограммирования и файлов. На верху базовой библиотеки находится множество модулей, которые поддерживают гибкую и платформенно-независимую архитектуру составных документов / пользовательского интерфейса. На их основе построены компоненты обработки текстов, редактирования форм и среды разработки. Благодаря интенсивному повторному использованию эта исчерпывающая система так мала, что может быть помещена на четыре дискеты. Например, символьный отладчик, который является частью подсистемы Dev, использует подсистему Text для вывода удобного гипертекстового интерфейса.