Docu/ru/Tut-3.odc

3 Приемы проектирования в Блэкбоксе
ENGLISH

В предыдущих главах были описаны различные приемы и подходы к проектированию. Они помогают решить проблемы, которые возникают в составных интерактивных приложениях. В этой главе обсуждаются более общие аспекты, которые помогают создать компонентно-ориентированный каркас, расширяемый за счет динамически загружаемых блэкбоксовых компонентов.
Мы не пытаемся создать полный метод проектирования, который бы давал пошаговые инструкции, как проектировать новый каркас. Это нереально. Но мы хотим продемонстрировать и обосновать приемы, подходы и образцы, которым следовали при проектировании Компонентного Каркаса Блэкбокса. Некоторые из приемов проектирования даже привели к включению специфических "каркасных" функций в язык Компонентный Паскаль. Вначале обсуждается языковая поддержка проектирования каркаса, затем обеспечение компонент Компонентного Паскаля и правил, которые регулируют компонентное сотрудничество в Блэкбоксе.


3.1 Языковая поддержка

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

Каркас представляет архитектуру для решения определенного класса проблем. Хорошая архитектура использует минимальное число типовых (базовых) элементов, где бы они ни применялись. Последовательное применение типовых элементов упрощает документирование каркаса и облегчает его понимание.
Типовые элементы являются абстрактными решениями проблемы. Для построения каркаса они должны быть сформулированы в терминах конкретного языка программирования. Язык программирования оказывает большое влияние на то, как типовой элемент может быть представлен и как могут поддерживаться его логичность и постоянство.

Есть две основных "философии" построения языка программирования. Одна из них представлена языком Си. Си есть сжатый язык системного программирования, который позволяет легко и эффективно манипулировать структурой памяти данных. Другой подход представлен Паскалем. Паскаль - это читабельный язык прикладного программирования, который обеспечивает высокий уровень безопасности.
Подход Си особенно хорош для написания низкоуровневого кода, такого, как драйверы устройств. Когда Си стал все больше применяться для проектирования приложений, возникла идеология  безошибочного программирования (zero errors ideology), которая провозглашает, что, поскольку ошибки программирования не должны случаться, они не будут случаться. "Настоящий программист" не делает ошибок и поэтому нет нужды в средствах защиты, "давящих" на него со стороны языка. Функции безопасности, такие, как контроль переполнения индекса, нужны начинающим программистам, а для профессионалов они только помеха.
 
Однако современные психологические исследования показывают, что люди делают ошибки все время, независимо от того, пишут ли они программы, выполняют математические доказательства, пилотируют самолеты или оперируют больного. Ошибки допускаются независимо от того, признаются они или нет. Важно понимать, что большинство ошибок тем легче исправить, чем раньше их удается выявить. Обнаружение ошибок работает лучше в команде, взаимодействующей открыто, где член команды не боится двойного контроля его работы другими (выявляющими ошибки). Хорошие авиапредприятия отрабатывают коллективные действия своих пилотов в условиях стресса. Некоторые прогрессивно мыслящие клиники используют похожий тренинг для хирургов и их ассистентов. Они отбросили идеологию безошибочной работы в своих областях.

В мире программирования в последние десять лет преобладает обратная тенденция в силу того, что язык Си стал основным для всех видов программ. В терминах техники программирования и осознания безопасности это был огромный шаг назад от современного состояния. Большие компании пытаются ограничить потери навязыванием дорогостоящих инструментов, которые привносят лишь ограниченный уровень безопасности, вместо решения проблем там, где они (решения) стоят намного меньше, а именно, на уровне языка.
Но, в то время как программисту трудно признать, что делает ошибки, ему намного легче признать, что другие программисты делают ошибки. Это стало очевидно с появлением Интернета. Интернет позволяет легко, а иногда даже автоматически загрузить и выполнить маленькие программы (аплеты) с неизвестных сайтов. Этому коду вообще оказалось нельзя доверять. К счастью, это вынудило большие компании пересмотреть языки с точки зрения безопасности. Фактически, требования безопасности явились причиной выдвижения языка Java на первое место. Поверхностно Java похож на C++, но, в отличие от Cи и C++, он является полностью защищенным по типам, как Компонентный Паскаль.

В Компонентном Паскале объекты и их классы (типы записей) могут быть полностью спрятаны в модуле или они могут быть полностью экспортированы, т.е. они и их части (поля записи / отдельные переменные) могут быть полностью видимы снаружи определяющего их модуля. На практике полезно иметь еще больше контроля. По этой причине Компонентный Паскаль позволяет определить для каждого поля записи, является ли оно полностью экспортируемым, экспортируемым только для чтения или скрытым. Следующий пример (листинг 3-1) показывает, как звездочка ("*") применяется для экспорта и минус ("-") для более ограниченного экспорта "только для чтения":

MODULE ObxSample;

	TYPE
		File* = POINTER TO RECORD
			len: INTEGER	(* пример скрытой переменной *)
		END;

		Rider* = POINTER TO RECORD
			(* здесь может быть несколько курьеров на один файл *)
			file-: File;	(* пример переменной "только для чтения" *)
			eof*: BOOLEAN;	(* пример переменной полного экспорта *)
			pos: INTEGER	(* пример скрытой переменной *)
				(* Инвариант: (pos >= 0) & (pos < file.len) *)
		END;

	PROCEDURE (f: File) GetLength* (OUT length: INTEGER), NEW;
	BEGIN
		length := f.len
	END GetLength;

	PROCEDURE (rd: Rider) SetPos* (pos: INTEGER), NEW;
	BEGIN
		(* защита инвариантов, чтобы ошибки не могли проскочить сквозь компоненты *)
		ASSERT(pos >= 0); ASSERT(pos < rd.file.len);
		rd.pos := pos
	END SetPos;
	...

END ObxSample.

Листинг 3-1. Образец модуля на Компонентном Паскале

Java не поддерживает экспорт только для чтения, но он поддерживает защищенные поля, которые являются видимыми только расширениям класса, но не нормальным клиентам. Это свойство уместно, только если наследование выполнения используется с пересечением модульных границ. По причинам, которые лежат за рамками этого текста, наследование выполнения не является хорошей идеей в блэкбоксовых абстракциях, таких, как компоненты, и поэтому не может нормально переноситься через границы компонент или в интерфейсах компонент каркаса. Защищенный экспорт поэтому не поддерживается в Компонентном Паскале.

Безопасность означает разные вещи для разных людей. С одной стороны, C++ может считаться безопаснее Си. С другой стороны, Интернет вызывает озабоченность, которая выходит за пределы просто безопасности; должна также предусматриваться защита (от несанкционированного доступа, криминальных атак и т.д.). Предметами охраны являются способы авторизации и обычно имеют дело на уровне операционной системы или оборудования. Функции защиты более высокого уровня являются предметом библиотечных определений и исполнения. Однако сопутствующих потерь из-за чрезмерного проникновения сквозь аппаратные механизмы защиты можно избежать, если строить защиту на строгих свойствах безопасности используемого языка(ов), что возвращает нас к вопросу о безопасности. Но что точно есть "безопасность"?
Если коротко, безопасность языка программирования означает, что его спецификация позволяет указывать инварианты, и что его выполнение гарантирует, что эти инварианты поддерживаются. (Возможность доверенного выполнения должна предполагаться, эта уверенность должна подтверждаться выживанием после атак.) Коротко: безопасность есть "инварианты, принятые всерьез". 

Какие типы инвариантов мы рассматриваем? Большинство фундаментальных инвариантов есть инварианты памяти: память, занятая переменными, может использоваться только таким путем, который допускает язык (например, описание типа). На практике это означает, что произвольный выбор типа должен быть запрещен и что ручное удаление динамических структур данных (освобождение памяти) не должно допускаться, поскольку освобождение может случиться слишком рано и некоторая часть все еще используемой памяти может быть снова распределена и, таким образом, будет использоваться одновременно двумя независимыми переменными. Такие висячие указатели гибельны, но их можно полностью избежать, если вместо ручного освобождения используется автоматическая сборка мусора. Поэтому Java и Компонентный Паскаль используют автоматическую сборку мусора.

Инварианты памяти фундаментальны. Более специфичные для приложения инварианты обычно устанавливаются поверх нескольких взаимодействующих объектов. Например, объект, который представляет путь к файлу, всегда должен иметь текущую позицию, которая лежит внутри файла, где файл представляется вторым объектом. Этот инвариант связывает два объекта (и таким образом классы). Только это может гарантировать, что если язык программирования позволяет определить взаимодействие между двумя объектами, которые являются частными для него, то внешний код не может на него повлиять.

Заметим, что это отличается от "защищенного" отношения, рассмотренного выше. В отличие от 
установки отношения между классом и его неизвестными еще подклассами, потомок здесь является классом и который был построен совместно и всегда будет использоваться в связи. Модуль или пакет модулей является подходящим структурированным средством, которое позволяет определить такие свойства безопасности наивысшего уровня. Пакеты Java в этом отношении не идеальны. Поскольку пакеты Java являются открытыми модулями, новые классы могут быть добавлены в пакет во время исполнения (at run-time), и могут полностью нарушать инварианты, которые были установлены ранее. Такое дополнение "чужих" классов в пакет необходимо защищать средствами периода исполнения, которые не подчиняются языковым определениям. В Java это связано с концепцией уникального владения: каждый пакет, по идее, принадлежит его источнику и только этот единственный источник имеет право добавлять новые классы в пакет. В любом случае пакет Java является улучшением по сравнению с многими другими объектно-ориентированными языками, например, полностью неструктурированным подходом дружественных классов в С++, или полным отсутствием подходящего конструкта в стандартном Smalltalk. 
В противоположность пакетам Java Компонентный Паскаль поддерживает закрытые модули, или, для краткости, модули. В Компонентном Паскале модуль является отдельной единицей компиляции, загрузки и сокрытия информации.

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

Но преимущество типовой и модульной безопасности простирается дальше. Оно обеспечивает большую гибкость. На первый взгляд это кажется удивительным. Почему ограничения, такие как типы (которые ограничивают операции над переменными) и модули (которые ограничивают видимость) создают что-то еще, кроме уменьшения гибкости? Причина этого парадокса переработки (refactoring paradoxon) двоякая. С одной стороны, всё, что полностью скрыто в модуле, может быть изменено вмешательством в этот единственный модуль. Локальные изменения в скрытой части модуля никак не отдаются за его пределами. Даже не требуется перекомпиляция других модулей-клиентов. С другой стороны, когда строго типизированный интерфейс по какой-то причине меняется, простая перекомпиляция выявит несоответствие в используемом интерфейсе, например, изменение типа или имени метода. Компилятор, таким образом, реально может повысить уверенность в программной системе, которая была модифицирована некоторыми интерфейсными изменениями.
Контролируя типизированный интерфейс на аккуратно выбранном уровне детализации (разбиения на модули), можно достичь баланса между бинарной совместимостью версий и выявлением определенных несоответствий. Компонентный Паскаль поддерживает строгую типизацию, позволяющую выявлять такие несоответствия. Например, вновь введенный метод должен быть помечен как NEW, и VAR параметры могут быть уточнены как IN или OUT параметры. В Java, например, невозможно различить новый метод, попытку перезагрузки или переопределения (overriding). Ошибка в имени метода, изменении базы или подкласса, или комбинация несовместимых версий может приводить к ошибкам, которые трудно локализовать.

Каркас обычно предопределяет интерфейсы для расширений каркаса. Каркас даже может содержать код, который использует эти интерфейсы, хотя собственно исполнение еще не существует. Вызов кода, который resides "higher up" в модульной иерархии, типично для объектно-ориентированных каркасов. Язык может поддерживать этот типичную для каркаса схему потока управления через определенную форму интерфейсов, которые являются абстрактными классами, которые можно применять где угодно. Компонентный Паскаль поддерживает записи абстрактных типов с единственным наследованием таким образом, что возможен широкий спектр от полностью абстрактных до полностью конкретных типов. Java похож на Компонентный Паскаль в этом отношении, за тем исключением, что он дополнительно поддерживает конструкт раздельный интерфейс. Интерфейс Java - это то же, что и полностью абстрактный класс, за исключением того, что он допускает множественное (интерфейсное) наследование.


MODULE TestViews;

	TYPE
		View* = POINTER TO ABSTRACT RECORD
			(* частично абстрактный тип  *)
			context-: Context;
			... some hidden fields ...
		END;

		Context* = POINTER TO ABSTRACT RECORD END;
		(* полностью абстрактный тип *)

		PROCEDURE (v: View) Restore* (l, t, r, b: INTEGER), NEW, ABSTRACT;

		...

END TestViews.

Листинг 3-2. Полуабстрактный и абстрактный типы записей в Компонентном Паскале

Инварианты являются свойствами, которые предположительно остаются неизменными. Тогда возникает вопрос, может ли подтип (подкласс / расширенный тип) произвольно менять поведение своего базового типа. В объектно-ориентированных языках такие модификации могут быть достигнуты поправками [?, overriding] унаследованных методов. Java позволяет делать классы или методы финальными [final], давать проектировщику каркаса возможность предотвратить любой вид нарушения инварианта через "заднее крыльцо" правок [?, overriding].Типы записей Компонентного Паскаля являются финальными по умолчанию, они могут быть явно помечены как расширяемые.

Компонентный Паскаль превосходит Java в нескольких других языковых конструктах. Один является ограниченными [limited] записями. Это записи, которые могут быть расширены и размещены только внутри описывающего их модуля. С точки зрения перспективы импорта модулей ограниченные типы являются финальными и даже не размещаемыми. Это позволяет гарантировать, что все размещения происходят централизованно в определяющем (каркасе) модуле, что дает этому модулю полный контроль над инициализацией. Например, это позволяет предлагать фабричные [разработчиками] функции, которые размещают объект и инициализируют его разными способами, устанавливая инварианты перед тем, как объект попадет в клиентский модуль. Этот механизм гибче и проще, чем конструкторы, которые используются в Java.

Экспорт "только для исполнения" - это главный пример свойства, являющегося причиной типичных шаблонов проектирования каркаса. Методы типа запись могут быть экспортированы "только для исполнения" путем указания дефиса вместо звездочки. Метод "только для исполнения" может вызываться только внутри определяющего модуля. Но он может быть исполнен [implemented] вне определяющего модуля в исполняющем компоненте каркаса. Например, механизм сохранения Компонентного Каркаса Блэкбокса использует это свойство, чтобы защитить методы загрузки и выгрузки [Internalize и Externalize] от вызова их вне корректного контекста. По существу, каркас (в данном случае модуль Stores) использует методы "только для исполнения" всеми возможными законными способами (выполняет все законные способы применения) и экспортирует их только для целей исполнения.

Для методов, которые представляют интерфейсные варианты, поддерживается атрибутный метод EMPTY (пустой). Пустой метод есть полностью абстрактная ловушка, которая может быть использована в расширении, но в отличие от абстрактных методов ее не требуется использовать (?). Например, образ [вид,  view] имеет пустой метод HandleCtrlMsg, который необходимо применять только в образах, которые реагируют на пользовательский ввод, например, через мышь или клавиатуру.

В книге Design Patterns (Проектные шаблоны) ["Design Patterns, Elements of Reusable Object-Oriented Software"; Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides; Addison-Wesley, 1994; ISBN 0-201-63361-2] обозначен список общих проблем проектирования. Эти проблемы часто ведут к необходимости перепроектирования, поскольку ПО не дает достаточной свободы для внесения изменений. Некоторые из этих проблем решены в Компонентном Паскале:

 Создание объекта указанием класса явно
Модули позволяют скрывать классы. Скрытый класс не может быть прямо подтвержден клиентским модулем, поскольку он там невидим. Это позволяет навязать множество непрямых механизмов размещения. Абстрактные типы записей позволяют отделить интерфейсы от исполнения, так что абстрактные типы записей могут быть экспортированы без риска прямого размещения клиентами.
Как и абстрактные типы, ограниченные [limited] типы не могут быть напрямую размещены клиентами.
Экспорт "только для исполнения" ограничить последовательности размещения и инициализации определяющим модулем, что также предохраняет клиентов от обязанности самим уточнять классы прямым подтверждением их.
Эти свойства можно комбинировать для разработки безопасных исполнений всеми создающими шаблонами, описанными в Design Patterns: abstract factories, builders, factory methods, prototypes и singletons.

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

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

 Алгоритмические зависимости
Части алгоритма могут заменяться с использованием абстрактных методов, пустых (hook) методов или простых объектов. Экспорт "только для исполнения" делает возможным безопасное назначение обязанностей: последовательности корректных вызовов методов должны выполняться определением модуля, исполнение методов должно выполняться расширяющим программистом. Это свойство позволяет разрабатывать безопасные исполнения всех поведенческих шаблонов (строитель, внутренний итератор, стратегия, временный метод и так далее).

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


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

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

Все эти проблемы имеют одну общую тему: локальное изменение может вызвать волновой эффект, который отразится на программной системе в целом. Хорошие языки и приемы проектирования помогают проектировать с учетом возможности изменений в будущем:

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

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


3.2 Модули и подсистемы

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

Весь код Компонентного Паскаля живет внутри модулей. Модуль есть единица компиляции Компонентного Паскаля. Модуль имеет интерфейс и скрытое исполнение. Синтаксически это достигается пометкой некоторых элементов в модуле, например, некоторых типов и процедур, как экспортируемые. Всё, что не экспортировано, невидимо, и поэтому напрямую недоступно извне модуля. Если модулю нужен сервис одного или нескольких других модулей, он импортирует их. Поэтому модуль объявляет, что ему требуются описания интерфейсов импортированных модулей в период компиляции, и подходящее исполнение в период выполнения. Листинг 3-3 показывает возможное исполнение модуля, названного ObxPhoneDB. Он экспортирует три процедуры для входа в телефонный справочник: по индексу, по имени и по номеру. Обратите внимание на звездочки, которые помечают элементы как экспортируемые, в нашем случае это тип String и три процедуры входа:

MODULE ObxPhoneDB;

	CONST
		maxLen = 32;	(* максимальная длина строк имя/число *)
		maxEntries = 5;	(* максимальное число входов в базу данных *)

	TYPE
		String* = ARRAY maxLen OF CHAR;

		Entry = RECORD
			name, number: String
		END;

	VAR db: ARRAY maxEntries OF Entry;

	PROCEDURE LookupByIndex* (index: INTEGER; OUT name, number: String);
	BEGIN	(* дан индекс, возвращается соответствующая пара <name, number> *)
		ASSERT(index >= 0);
		IF index < maxEntries THEN
			name := db[index].name; number := db[index].number
		ELSE
			name := ""; number := ""
		END
	END LookupByIndex;

	PROCEDURE LookupByName* (name: String; OUT number: String);
		VAR i: INTEGER;
	BEGIN	(* дано имя, поиск соответствующего номера телефона *)
		i := 0; WHILE (i # maxEntries) & (db[i].name # name) DO INC(i) END;
		IF i # maxEntries THEN	(* имя найдено в db[i] *)
			number := db[i].number
		ELSE	(*имя не найдено в db[0..maxEntries-1] *)
			number := ""
		END
	END LookupByName;

	PROCEDURE LookupByNumber* (number: String; OUT name: String);
		VAR i: INTEGER;
	BEGIN	(* дан номер телефона, поиск соответствующего имени *)
		i := 0; WHILE (i # maxEntries) & (db[i].number # number) DO INC(i) END;
		IF i # maxEntries THEN	(* номер найден в db[i] *)
			name := db[i].name
		ELSE	(* номер не найден в db[0..maxEntries-1] *)
			name := ""
		END
	END LookupByNumber;

BEGIN	(* инициализация содержимого справочника *)
	db[0].name := "Даффи Дьюк"; db[0].number := "310-555-1212";
	db[1].name := "Виль Е. Койот"; db[1].number := "408-555-1212";
	db[2].name := "Скрудж МакДьюк"; db[2].number := "206-555-1212";
	db[3].name := "Хью Льюис"; db[3].number := "415-555-1212";
	db[4].name := "Томас Деви"; db[4].number := "617-555-1212"
END ObxPhoneDB.

    Даффи Дьюк			310-555-1212
    Виль Е. Койот		   408-555-1212
    Скрудж МакДьюк	   206-555-1212
    Хью Льюис				 415-555-1212
    Томас Деви				617-555-1212

Листинг 3-3. Исполнение of ObxPhoneDB

Перед тем, как модуль использовать, его необходимо загрузить с диска в оперативную память. Но перед тем, как он может быть загружен, его необходимо откомпилировать (команда Разработка -> Компилировать). Компилируя модуль, компилятор порождает кодовый файл и символьный файл. Кодовый файл содержит исполняемый код, который может быть загружен в память. Кодовый файл есть разновидность суперлегкой DLL (динамически загружаемой библиотеки). Компилятор также производит символьный файл, который содержит бинарное представление интерфейса модуля. Если модуль импортирует другие модули, компилятор читает символьные файлы этих модулей, чтобы проверить, что их интерфейсы используются корректно. Процесс компиляции можно изобразить так:



Рис. 3-4. Процесс компиляции

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

компилируется "ObxPhoneDB"  
  new symbol file   564   640

Первые два числа показывают, что машинный код в новом кодовом файле имеет длину 564 байта. 
Второе число показывает, что модуль содержит 640 байт глобальных переменных (пять входов в переменной db; каждый вход состоит из двух строк по 32 элемента в каждой; каждый элемент имеет 2-байтный Юникод-символ). Если символьный файл точно для такого интерфейса уже существует, компилятор выдает более короткое сообщение:

компилируется "ObxPhoneDB"  564   640

Если интерфейс изменился, компилятор пишет новый символьный файл и указывает в рабочем журнале, какие изменения появились по сравнению со старой версией. Например, если вы только что изменили процедуру LookupByNumber, компилятор пишет:

компилируется "ObxPhoneDB"
  LookupByNumber is new in symbol file   964   640

Символьный файл используется только во время компиляции, он не используется во время выполнения. Чтобы загрузить модуль, достаточно его кодового файла. Модули загружаются динамически, т.е. нет отдельного шага линкования, как в большинстве статических языков. Чтобы увидеть список загруженных модулей, выполните Инфо->Загруженные модули. Откроется окно с содержимым, похожим на следующее:

имя модуля	байт использовано	клиенты	скомпилирован			загружен		Обновить

DevCompiler	  	6945	 	 			0			29.12.2004  18:47:13	09.07.2005  22:26:56
DevCPV486	 	35884	  				1			29.12.2004  18:47:13	09.07.2005  22:26:56
DevCPC486	 	46245	  				1			29.12.2004  18:47:13	09.07.2005  22:26:56
DevCPL486	 	 17736	  	  		 2	 		29.12.2004  18:47:13	09.07.2005  22:26:56
...
Config	  			2133	  		 		0			26.06.2005  4:39:44	  09.07.2005  22:26:25
Init	   				  411	  		  		0			29.12.2004  18:48:43    09.07.2005  22:26:25
Controls	 		58307	  				 6			26.06.2005  2:32:05	  09.07.2005  22:26:25
...
Files	   			  	426	 				67			29.12.2004  18:47:00	    слинкован
Kernel	 			 27353	 		   	 69			26.06.2005  2:34:23	      слинкован
National	  		   3635	 				14			26.06.2005  1:50:57	      слинкован
...
StdLoader	 		 8104	  			  0	  		29.12.2004  18:47:00	     слинкован

Таблица 3-5. Список загруженных модулей

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

Легко получить список уже загруженных модулей, но что с модулями, которые еще не загружены? Желание иметь доступ к богатству заранее подготовленных компонентов поднимает некоторые организационные проблемы. Как вам точно определить, какие компоненты доступны? Как вам определить, какие из доступных компонентов дают то, что вам нужно?
 
Это предмет соглашений, документации и поддерживающего инструментария. В Блэкбоксе решено, что коллекция связанных компонентов, называемая подсистема, размещается в отдельным папках; все они находятся в корневой папке Блэкбокса. Там есть такие подсистемы, как System, Std, Host, Mac, Win, Text, Form, Dev или Obx. Полная коллекция подсистемы называется депозитарий (хранилище) Блэкбокса. Идея, лежащая в основе структуры депозитария, состоит в том, чтобы всё, что принадлежит компоненту (кодовые файлы, символьные файлы, документация, исходные тексты) сохранялось вместе в систематической и простой структуре папок в соответствии с правилом

Держать все составляющие компонента в одном месте.

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



Рис. 3-6. Стандартные подсистемы Блэбокса 

Каждая папка подсистемы, как Obx, может содержать следующие подпапки: 



Рис. 3-7. Структура типичной папки подсистемы

Исходный модуль сохраняется в папке Mod подсистемы. Имя файла соответствует имени модуля, но без префикса подсистемы; например, модули ObxPhoneDB и ObxPhoneUI сохранены как Obx/Mod/PhoneDB и Obx/Mod/PhoneUI соответственно. Для каждого исходного файла может быть соответствующий символьный файл, например, Obx/Sym/PhoneDB; соответствующий кодовый файл 
Obx/Code/PhoneDB; и соответствующий файл документации, например, Obx/Docu/PhoneDB. Также может быть 0 или более ресурсных документов в подсистеме, например, Obx/Rsrc/PhoneUI. Причем нет необходимости в соотношении 1:1 между модулями и ресурсами, хотя обычно рекомендуется использовать имя модуля как часть имени ресурса для упрощения поддержки.

Модули, чьи имена имеют форму SubMod, такие, как TextModels, FormViews или StdCmds, сохраняются в их соответствующих подсистемах, заданных префиксами имен, например, Text, Form или Std. Имя подсистемы начинается с заглавной буквы и далее могут идти несколько заглавных и затем несколько строчных букв или цифр. Первая заглавная буква после этого обозначает отдельный модуль в подсистеме.
Модули, которые не принадлежат подсистеме, т.е. модули, имена которых не имеют формы SubMod, сохраняются в специальной подсистеме, названной System. Вся библиотека Блэкбокса и ядро каркаса принадлежат к этой категории, это модули Math, Files, Stores, Models и т.д.

Каждая папка подсистемы может содержать следующие вложенные папки:
Code	Папка с исполняемыми кодовыми файлами, т.е. легкими DLL.
	Например, для модуля "FormCmds" там есть файл "Form/Code/Cmds".
	Модуль для связи с настоящими DLL ОС (Windows DLL или кодовые		
	фрагменты Mac OS) не имеют кодового файла.
Docu	Папка с полностью документированными интерфейсами и другими docu.
	Например, для модуля "FormCmds" там есть файл "Form/Docu/Cmds".
	Для модуля, который используется только внутренне (для служебных целей
	Блэбокса), его файл docu не поставляется.
	Часто есть общие файлы документации, которые не относятся к отдельному
	модулю подсистемы. Такие файлы содержат один или более дефисов в их
	именах, например, "Dev/Docu/P-S-I". 
	Типичными файлами являются 
	"Sys-Map" (обзор гиперссылок на другие документы этой подсистемы)
	"User-Man" (руководство пользователя)
	"Dev-Man" (руководство разработчика)
DocuRu	Папка с переводами на русский язык документов из Docu.
Mod	Папка с исходными модулями.
	Например, для модуля "FormCmds" там есть файл "Form/Mod/Cmds".
	Для модуля, который не публикуется в исходном виде ("белый ящик" = 
	"white box"), его исходный текст не поставляется пользователям.
Rsrc	Папка с ресурсными документами подсистемы.
	Например, для модуля "FormCmds" там есть файл "Form/Rsrc/Cmds".
	Может быть ноль, один или более ресурсных файлов для одного документа.
	Если есть несколько файлов, второй получает суффикс "1", третий суффикс 
	"2" и так далее. Например, "Form/Rsrc/Cmds", "Form/Rsrc/Cmds1",
	 "Form/Rsrc/Cmds2" и т.д.
	Часто есть добавочные файлы, которые не относятся к отдельному модулю
	подсистемы. Типичными файлами являются
	"Strings" (строковые ресурсы этой подсистемы)
	"Menus" (меню этой подсистемы)
Sym	Папка с символьными файлами.
	Например, для модуля "FormCmds" там есть файл "Form/Sym/Cmds".
	Для модуля, который используется только внутренне, его символьный файл
	не поставляется.
	

Таблица 3-8. Содержание стандартных папок подсистемы

Если вы что-то хотите найти в депозитарии, его подсистемах или их папках, вы можете выполнить команду Инфо->Обзор подсистем. Если вы хотите найти больше сведений о модуле, вы можете выделить имя модуля в тексте и затем выполнить Инфо->Исходный текст,  или Инфо->Документация. Эти команды открывают модульные файлы Mod, Sym или Docu. Для этой цели Инфо->Интерфейс преобразует бинарное представление символьного файла в читабельное текстовое описание. Например, наберите строку "ObxPhoneDB" в рабочем журнале, выделите строку и затем выполните команду просмотра Инфо->Интерфейс. В результате в новом окне будет открыт следующий текст:

DEFINITION ObxPhoneDB;

	TYPE
		String = ARRAY 32 OF CHAR;

	PROCEDURE LookupByIndex (index: INTEGER; OUT name, number: String);
	PROCEDURE LookupByName (name: String; OUT number: String);
	PROCEDURE LookupByNumber (number: String; OUT name: String);

END ObxPhoneDB.

Листинг 3-9. Определение ObxPhoneDB

Модульное определение, сгенерированное браузером синтаксически, является подмножеством модульного исполнения, за тем исключением, что ключевое слово MODULE заменено на DEFINITION. Этот синтаксис можно назвать языком описания интерфейса (IDL) Компонентного Паскаля. Тексты в этом языке обычно создаются по полным исходным текстам модулей браузером или аналогичным инструментом, так что нет нужды писать их вручную или компилировать. 

Поскольку команды браузера оперируют с символьным файлом модуля, он может быть использован, даже когда еще нет ни настоящей документации, ни кодового файла. Во время прототипирования, когда документация редко имеется, просмотр символьного файла очень удобен для быстрого получения деталей, таких, как сигнатура процедуры, или обзора интерфейса всего модуля. Когда доступна полная документация, может использоваться команда Инфо->Документация. Она открывает файл документации модуля, который начинается с определения модульного интерфейса, точно так, как показано выше,  но затем продолжается назначением модуля и детальным описанием различных элементов, экспортированных модулем, например:

Модуль ObxPhoneDB обеспечивает доступ к телефонному справочнику. Доступ может быть по индексу, по имени или по номеру. Вход состоит из строки имени и телефонного номера. Ничто не может быть пустым. Минимальный индекс есть 0, и все входы являются смежными.

PROCEDURE LookupByIndex (index: INTEGER; OUT name, number: ARRAY OF CHAR)
Возвращает пару <name, number> входа index. Если индекс слишком большой, возвращается <"", "">.
Процедура выполняется за постоянное время.

Pre
index >= 0	20

Post
index правильный
	name # ""  &  number # ""
index неправильный
	name = ""  &  number = ""

PROCEDURE LookupByName (name: ARRAY OF CHAR; OUT number: ARRAY OF CHAR)
Возвращает телефонный номер, связанный с name, или "", если не найден вход для name.
Процедура выполняется за линейное время, в зависимости от размера базы данных (справочника).

Post
name найдено
	number # ""
name не найдено
	number = ""

PROCEDURE LookupByNumber (number: ARRAY OF CHAR; OUT name: ARRAY OF CHAR)
Возвращает имя, связанное с number, или "", если не найден вход для number.
Процедура выполняется за линейное время, в зависимости от размера базы данных (справочника).

Post
number найдено
	name # ""
number не найдено
	name = ""
	
Листинг 3-10. Документация ObxPhoneDB

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

Свободны	0 .. 19
Предусловия	20 .. 59
Постусловия	60 .. 99
Инварианты	100 .. 120
Резервировано	121 .. 125
Еще не использовано	         126
Резервировано	         127

Листинг 3-11. Номера утверждений в Блэкбоксе

Хорошо известно, что определение ошибки тем более затруднено и дороже обходится, чем позже она обнаруживается, т.е. чем дальше разнесены источник и его эффекты. Это позволяет сформулировать правило:

Позволяйте ошибкам заявлять о себе как можно раньше.

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

Никогда не позволяйте ошибкам проникать сквозь границы компонентов.

К счастью, большинство проверок предусловий недорого и поэтому их отключение при исполнении не имеет смысла. Это важно, поскольку в компонентно-ориентированной системе проверки периода исполнения не могут быть выключены в конечной (готовой) системе, поэтому разрабатываемая и готовая системы не различаются. На практике большинство компонентов уже отлажено в режиме черного ящика ("продукция"), а другие отлаживаются в режиме белого ящика. Готовые компоненты должны взаимодействовать, чтобы доказать приверженность сформулированному выше правилу, которое означает никогда не отключать проверки периода исполнения.


3.3 Интерфейсы типа узкое горло

Один частный шаблон, который мы обсудим здесь, есть шаблон Носитель-Курьер-Преобразователь (Carrier-Rider-Mapper). Этот шаблон используется несколькими путями в Компонентном Каркасе Блэкбокса: в текстовой подсистеме, в файловой абстракции, в рамочной абстракции, в абстракции контейнер/контекст и других. Давайте рассмотрим в качестве примера текст.
Пусть текст будет носителем. Носитель есть объект, который несет (содержит) данные, в этом случае текстовые данные. По сути, текст можно рассматривать как линейный поток элементов, где элементы имеют такие атрибуты, как шрифт, цвет, стиль и вертикальный сдвиг (для верхних и нижних индексов). Элементы являются символами или образами (views). Для всех практических целей образ в тексте можно рассматривать просто как специальный символ.
Когда читается текст, удобно не требовать указания позиции для любого и каждого прочитанного символа. Это удобство может быть достигнуто поддержкой текущей позиции, т.е. текст сам знает, где читать следующий символ. Каждая операция чтения автоматически увеличивает текущую позицию. Некоторые клиентские объекты могут использовать текстовый носитель одновременно. Например, текстовому образу нужно читать его текст, когда он перерисовывает текст на экране, и команда меню может нуждаться в чтении текста как своего входа. Эти клиенты независимы друг от друга; им незачем знать друг о друге. По этой причине текущая позиция чтения не может сохраняться в самом текстовом носителе, поскольку это может означать, что клиенты будут взаимодействовать друг с другом и потеряют независимость. 
Чтобы избежать такого взаимодействия, носитель имеет функции размещения, которые возвращают так называемые объекты-курьеры (rider). Курьер - это независимый доступ к данным носителя. Каждый текстовый курьер имеет собственную текущую позицию и может далее оставаться таковым как текущие атрибуты. 



Рис. 3-12. Носитель-Курьер (Carrier-Rider) разделение

Причиной, почему носитель и курьер разделены на разные типы данных, является отношение один-ко-многим между сущностями: для одного носителя может быть произвольное число (включая ноль) курьеров.
Обычно требуется, чтобы носитель и курьер исполнялись одним и тем же компонентом, поскольку курьер должен иметь хорошее знание о внутренностях его носителя. Например, текстовые курьеры и текстовые носители оба используются в модуле TextModels. Текстовый курьер содержит скрытые указатели на внутреннюю структуру данных текстового носителя. Поскольку эта информация полностью скрыта за границами модуля  TextModels, внешний клиент не может сделать курьер несовместимым с его носителем. Этот тип инварианта, гарантированный модульной системой Компонентного Паскаля, является примером того, почему сокрытие информации за пределами отдельных классов является важным по соображениям безопасности.

В терминологии проектирования шаблонов текстовый курьер есть внутренний итератор. Однако не все курьеры с необходимостью итераторы. Например, контекстный объект, управляемый контейнером, не является итератором.

Текстовые курьеры бывают двух видов: "анализаторы" (reader) и "синтезаторы" (writer). Анализаторы поддерживают чтение символов и образов, тогда как синтезаторы поддерживают запись символов и образов. Например, могут быть сделаны следующие вызовы:

	reader.ReadView(view)
	
	или
	
	writer.WriteChar("X")

Часто программисту нужно читать или писать сложные последовательности символов, так что работа на уровне отдельных символов слишком неудобна. Желательны высокоуровневые абстракции для чтения и записи. Это является целью преобразователей (mappers). Преобразователь содержит курьера, которого он использует для обеспечения более проблемно-ориентированных операций для записи или для чтения. Поскольку существуют очень разные проблемы программирования, разные приложения могут использовать разных преобразователей, возможно даже работающих с одним и тем же носителем.

Для текстовых манипуляций Блэкбокс предлагает модуль TextMappers, который определяет и исполняет два текстовых преобразователя: форматер для записи и сканер для чтения.



Рис. 3-13. разделение Carrier-Rider-Mapper

Как сканер, так и форматер работают на уровне символов Компонентного Паскаля: с целыми числами, вещественными числами, строками и т.д. Например, следующие вызовы могут быть:

	scanner.ReadInt(int)
	
	или
	
	formatter.WriteReal(3.14)

Если есть разные преобразователи для одного вида носителя, они все должны исполняться в терминах интерфейса курьера. По этой причине интерфейс курьер/носитель иногда называется интерфейсом узкого горла. Причина, по которой курьеры и преобразователи разделены, не только в отношении один-ко-многим как между носителем и курьером, так в отношении один-ко-многим между преобразователем и курьером, но и в независимой расширяемости: особые обстоятельства могут требовать особых преобразователей. Если носители и курьеры строго придерживаются хорошо спроектированного курьерского интерфейса (типа узкое горло), можно добавлять новых преобразователей в любое время и использовать их с тем же носителем.
Даже лучше: интерфейс носитель/курьер может быть абстрактным интерфейсом, так что могут быть разные исполнения его. Например, файловый носитель может быть исполнен по-разному для файлов на гибком диске, файлов на жестком диске, файлах CD-ROM и сетевых файлов. Но если все они исполняют один и тот же интерфейс, то все файловые преобразователи могут работать со всеми файловыми исполнениями. Сравните это с ситуацией, где вы вынуждены заново исполнить все преобразователи для всех носителей: вместо исполнения n носителей плюс m преобразователей, вынуждены исполнить n носителей плюс n * m преобразователей! Там, где случается такое расширение в двух измерениях (носитель/курьер и преобразователь), интерфейс узкого горла необходим, чтобы избежать так называемого декартова произведения проблем,  т.е. взрыва [количества] исполнений.

Отметим, что интерфейс узкого горла сам не расширяемый, поскольку каждое расширение, которое не может быть выполнено в терминах интерфейса узкого горла, сводит на нет все его существующие исполнения. Например, если вы расширяете драйвера устройства для отображения черно-белой точечной картинки цветным расширением, то все существующие исполнения драйвера устройства должны быть обновлены. Поскольку это может быть сделано разными компаниями, это может стать главной проблемой. 
Проблема может быть снята, если новое расширение интерфейса определено как опция: тогда клиенты должны тестировать, где опция поддерживается. Если нет, то клиент либо должен сигнализировать, что он не может работать с этим оборудованием, либо он должен изящно деградировать, предлагая более ограниченную функциональность. Если опциональный интерфейс поддерживается и используется, он действует как вид конспирации между исполнением интерфейса и его клиентом. Это приемлемо, но, несомненно, уменьшает комбинации клиентов и исполнений, которые полностью функциональны. Это подчеркивает, насколько важны хорошие проекты узкого горла, чтобы исключить необходимость в последующих расширениях.



Рис. 3-14. Расширяемость в двух измерениях

В терминах проектных шаблонов, шаблон Носитель-Курьер-Преобразователь (Carrier-Rider-Mapper) решает проблему гибкости и удобства доступа к носителю данных, однако он является исполняемым. Он может рассматриваться как состоящий из двух простых и наиболее важных шаблонов (схем, приемов, методов) проектирования: разделение одного объекта на несколько объектов, чтобы обеспечить отношение многие-к-одному; и разбиение одного объекта на два объекта для обеспечения независимой расширяемости: 

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

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

Разделение Носитель-Курьер-Преобразователь возвращает к исследовательскому проекту ["Insight ETHOS: On Object-Orientation in Operating Systems"; Clemens Szyperski; vdf, Zьrich, 1992, ISBN 3 7281 1948 2], предшествовавшему Блэкбоксу. Этот проект использовал некоторые шаблоны проектирования и правила проектирования (например, исключение наследования исполнения), которые также описаны в Design Patterns. В терминологии Design Patterns комбинация Курьер-Преобразователь (или Носитель-Преобразователь, если нет курьера) формирует шаблон мост (bridge). 

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

	rider := carrier.NewRider(rider);

Идея в том, что если уже существует старый курьер, который больше не используется, он может быть переработан. Переработка делается фабричным методом NewRider (см.следующий раздел), если возможно. Конечно, переработка законна только тогда, когда старый курьер больше нигде не используется для чего-либо еще (возможно кем-то еще); вот почему так важно, что курьер поддерживается в контролируемом окружении.
Обычно процедура NewRider применяется следующим образом:

	PROCEDURE (o: Obj) NewRider (old: Rider): Rider;
		VAR r: RiderImplementation;
	BEGIN
		IF (old # NIL) & (old IS RiderImplementation) THEN  (* переработать старый курьер *)
			r := old(RiderImplementation)
		ELSE	(* разместить новый курьер *)
			NEW(r)
		END;
		... initialize r ...
		RETURN r
	END NewRider;

Листинг 3-15. Переработка курьера

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


3.4 Создание объекта

Компонентный Каркас Блэкбокса в основном спроектирован как черный ящик. Он строго отделяет интерфейс от исполнения. Клиент может только импортировать и, таким образом, напрямую использовать или манипулировать интерфейсами объектов. Исполнения спрятаны внутри модулей. Это вынуждает модульных клиентов подчиняться следующему правилу проектирования (Gamma et.al.):

Программа для интерфейса, а не для исполнения.

В Компонентном Паскале объектные интерфейсы представлены типами абстрактных записей. Исполнение записи абстрактного типа есть ее конкретное расширение. Обычно исполнения не экспортируются и поэтому не могут быть расширены ("subclassed") в других модулях, что означает невозможность наследования исполнения. Это также позволяет сформулировать правило для программистов, занимающихся расширениями (Gamma et.al.):

Приоритет композиции объекта, а не наследованию классов.

Конкретные типы записей даже не могут быть прямо подтверждены: поскольку клиент не может импортировать объектное использование, чей тип не экспортирован, он не может разместить пример вызовом NEW. Это желательно, поскольку NEW будет привязывать клиентский код навсегда к одному частному исполнению объекта; повторное использование кода для других исполнений станет невозможным где-либо еще. Но как код клиента может решить проблему размещения черного ящика, т.е. получить исполнение объекта без специфических знаний о нем?



Рис. 3-16. Экспортированные интерфейсные записи и скрытые записи исполнения

Вместо вызова NEW может быть возможен вызов функции размещения, так называемой фабричной функции. Например, модуль TextModels может экспортировать функцию New, которая размещает и возвращает текстовую модель. Внутренне она выполняет NEW над неэкспортируемым типом исполнения и, возможно, выполняет некоторую инициализацию. Это лучше, чем прямой вызов клиентами NEW, поскольку модуль размещения может гарантировать корректную инициализацию и новая реализация (версия) модуля может выполнять размещение или инициализацию несколько иным путем, не влияющим на клиента. Однако, фабричные статичные функции являются слишком негибкими. Надлежащее решение проблемы размещения черного ящика требует уровня окольных путей. Есть несколько подходов, чтобы этого достигнуть. Они названы созидательными шаблонами. Блэкбокс использует 4 созидательных шаблона: прототипы, фабричные методы, каталожные объекты и фабричные управляющие. Они объясняются ниже один за другим.

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

Иногда существует объект, который может использоваться для создания объекта другого типа. Для этой цели существующий объект предлагает фабричную функцию, фабричный метод. Возможно, может предлагать разные фабричные методы, которые поддерживают разные стратегии инициализации или которые создают разные типы конкретных объектов.
В  Блэкбоксе текстовая модель предлагает процедуры NewReader и NewWriter, которые генерируют новых анализаторов и синтезаторов. Они являются курьерскими объектами, которые представляют пути доступа к тексту.
Фабричные функции предназначены для объектов, которые исполняются одновременно  ("implementation covariance"). Это всегда случай для курьеров и их носителей.

Но этот подход недостаточен для размещения самих текстовых моделей. Когда появляется первая текстовая модель? Блэкбокс использует специальные объекты с фабричными методами, так называемые фабричные объекты (factory objects). В Блэкбоксе фабричные объекты используются особым образом: они установлены в глобальные переменные и могут быть заменены во время исполнения без влияния на код клиента. По историческим причинам мы называем фабричные объекты, которые используются для конфигурационных целей, директивными объектами (directory objects). 
Например, модуль TextModels экспортирует тип Directory, который содержит функцию New. Более того, он экспортирует две переменные dir и stdDir и процедуру SetDir. По умолчанию TextModels.dir используется кодом клиента для размещения новых пустых текстовых моделей. TextModels.stdDir содержит исполнение по умолчанию текстовой модели, что часто полезно при отладке исполнения новой текстовой модели. Например, вы можете использовать старую папку при разработке исполнения новой текстовой модели. Когда новое исполнение готово, вы устанавливаете его объект-каталог вызовом SetDir, таким образом обновляя на лету текстовую подсистему под новое исполнение. Тексты со старым исполнением этим не затрагиваются. Если новое исполнение содержит ошибки, умалчиваемое исполнение может быть переустановлено вызовом TextModels.SetDir(TextModels.stdDir). Текстовая подсистема использует объекты типа каталог их обычным способом. Тот же шаблон проектирования может быть найден в других контейнерных модельных абстракциях; например, для моделей форм. В Блэкбоксе они используются для гораздо более простых моделей и образов; например, для DevMarkers.

Отметим, что объекты, размещенные через папки, часто являются устойчивыми и их исполнения поэтому долгоживущими. По этой причине некоторые различные исполнения появляются через время, которое должно использоваться одновременно. Обычно по умолчанию нормальные объекты папок подсистемы установлены автоматически, когда подсистема загружена. Если другие объекты папок должны быть установлены, используется модуль Config для установки конфигурации желаемых объектов папок при старте.
Имя "объект-каталог" ("directory object") пришло из другого использования шаблона проектирования каталога: в модуле Files файл может быть создан использованием объекта-каталога Files.dir. Но Files.Directory также предлагает средства для нахождения большей информации о текущей конфигурации файловой системы (например, процедур Files.Directory.FileList). Файловые каталоги также интересны, поскольку они показывают, что заменяемый каталог может иногда может нуждаться в продвижении к замененному объекту-каталогу (?). Например, если новый каталог обеспечивает специальные файлы памяти, которые отличаются именами, начинающимися на "M:\", тогда новый каталог должен контролировать каждый файл, проверяя, начинается ли его имя шаблоном, приведенным выше. Если так, то это файл памяти. Если нет, это нормальный файл и должен поддерживаться старым объектом-каталогом. 
Может существовать одновременно несколько разных объектов-каталогов. Например, если сервис использует файлы особым образом, он может обеспечивать его собственный объект - файловую папку, которая по умолчанию пересылается стандартному объекту-каталогу файловой системы.
Множественные объекты-каталоги и связывающие объекты-каталоги являются частью шаблона проектирования каталога. Эта связующая часть шаблона является каскадируемой, если новый объект выходит на самый последний старый вместо stdDir.
Отметим, что, в отличие от типичных фабричных классов, каталоги редко расширяют.



Рис. 3-17. Переход между двумя объектами-каталогами

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

Registry (реестр) будет хорошим дополнением к объектам-каталогам. Реестр - это устойчивая центральная база данных, которая сохраняет информацию конфигурации, такую, как индивидуальные объекты-каталоги для установки при загрузке. Проблема с реестрами состоит в том, что они противоречат децентрализованной природе программных компонентов, что ведет к проблемам управления, таким как поиск реестровых входов, которые больше не адекватны.

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

Как пример, диалоговая форма передачи файла может позволять задавать протокол передачи файлов вместе с такой информацией, как обратный адрес, скорость передачи и т.д. Протокол передачи файлов, который указывает исполнение коммуникационного объекта, может быть передан из диалоговой формы коммуникационному ПО, где он используется для размещения подходящего коммуникационного объекта (например, потоковый объект TCP). 
Вместо передачи фабричного объекта может быть передано его символическое имя. "Внизу" сервис интерпретирует это имя; если необходимо, загружает модуль, который исполняет соответствующую фабричную функцию; и затем создает объект, используя фабричную функцию. Таким образом, сервис действует как фабричный менеджер. 
Этот подход используется для некоторых сервисов Блэкбокса, в частности, для подсистем Sql и Comm. В обоих случаях имя модуля может передаваться, когда размещается новый объект. Этот модуль, как ожидается, предоставит подходящие фабричные функции. Для Sql модули SqlOdbc и DtfDriver поставляют подходящие исполнения объекта и фабричные функции. Для Comm модуль CommTCP поставляет подходящее исполнение объекта.
Фабричные менеджеры часто больше подходят для создания временных (несохраняемых, non-persistent) объектов, тогда как объекты-каталоги больше подходят для создания постоянных (несохраняемых, persistent) объектов.