Чистая архитектура. Искусство разработки программного обеспечения Мартин Роберт
Рис. 33.2. Предварительная компонентная архитектура
Двойными линиями, как обычно, изображены архитектурные границы. Как видите, это типичное деление на представления, презентаторы, интеракторы и контроллеры. Также отметьте, что каждую из этих категорий я разбил на подкатегории по соответствующим им действующим лицам.
Каждый компонент на рис. 33.2 потенциально представляет файл .jar или .dll и каждый будет содержать соответствующие представления, презентаторы, интеракторы и контроллеры.
Обратите внимание на особые компоненты Представление каталога и Презентатор каталога. Именно так предполагается реализовать абстрактный вариант использования Просмотр каталога. Я предполагаю, что такие представления и презентаторы будут иметь вид абстрактных классов, которые будут наследовать конкретные классы представлений и презентаторов в производных компонентах.
Стал бы я действительно разбивать систему на все эти компоненты и организовывать их в файлы .jar или .dll? И да и нет. Конечно, я мог бы помудрить над компиляцией и настроить окружение так, чтобы действительно можно было создавать компоненты, развертываемые независимо. Также я мог бы оставить за собой право объединить компоненты, чтобы при необходимости получить меньшее количество единиц развертывания. Например, компоненты, изображенные на рис. 33.2, легко можно объединить в пять файлов .jar — по одному для представлений, презентаторов, интеракторов, контроллеров и утилит соответственно. В этом случае я мог развертывать компоненты, которые, скорее всего, будут меняться независимо друг от друга.
Еще один возможный вариант группировки: объединить представления и презентаторы в один файл .jar, а интеракторы, контроллеры и утилиты поместить в свои, отдельные файлы. Другой, еще более простой вариант группировки: создать два файла .jar — один с представлениями и презентаторами и другой со всем остальным.
Оставив эти возможности открытыми, позже мы сможем изменить способ развертывания системы, исходя из особенностей ее развития.
Управление зависимостями
Поток управления на рис. 33.2 движется справа налево. Входные данные поступают в контроллеры и затем обрабатываются интеракторами. Презентаторы форматируют результаты обработки и передают их представлениям для отображения.
Обратите внимание, что не все стрелки направлены справа налево. Фактически большинство из них направлено слева направо. Это объясняется тем, что архитектура следует правилу зависимости. Все зависимости пересекают архитектурные границы в одном направлении и всегда направлены в сторону компонентов, содержащих политики более высокого уровня.
Также отметьте, что отношения использования (открытые стрелки) совпадают с направлением потока управления, а отношения наследования (закрытые стрелки) направлены против потока управления. Это отражает использование принципа открытости/закрытости, который требует, чтобы зависимости были направлены в правильном направлении и изменения в низкоуровневых деталях не затрагивали высокоуровневые политики.
Заключение
Диаграмма архитектуры на рис. 33.2 включает разделение по двум измерениям. Первое — разделение на основе действующих лиц согласно принципу единственной ответственности; второе соответствует правилу зависимости. Цель обоих — разделить компоненты, изменяющиеся по разным причинам и с разной скоростью. Причины в данном случае соответствуют действующим лицам, а скорости — разным уровням политик.
После структуризации кода таким способом появляется возможность смешивать и распределять его по единицам развертывания как угодно. Вы сможете группировать компоненты в любые единицы развертывания, имеющие смысл, и менять правила группировки с изменением условий.
Глава 34. Недостающая глава
Автор: Симон Браун (Simon Brown), 2 марта 2017
Все советы, которые вы прочитали к настоящему моменту, безусловно, помогут вам проектировать замечательные приложения, состоящие из классов и компонентов с четко определенными границами, понятными обязанностями и управляемыми зависимостями. Но как всегда, дьявол кроется в деталях реализации, и действительно, очень легко споткнуться о последнее препятствие, если не уделить ему должного внимания.
Представим, что мы строим книжный онлайн-магазин и один из вариантов использования, который нам предлагается внедрить, — возможность просмотра клиентами состояния своих заказов. Пример, следующий ниже, описывается с позиции языка Java, однако принципы в равной степени применимы к другим языкам программирования. Отложим пока чистую архитектуру в сторону и рассмотрим несколько подходов к проектированию и организации кода.
Упаковка по уровням
Первый и самый простой, пожалуй, подход — организация традиционной многоуровневой архитектуры, в которой код разделяется по функциональному признаку. Этот подход часто называют «упаковкой по уровням». На рис. 34.1 показано, как могла бы выглядеть соответствующая UML-диаграмма классов.
Рис. 34.1. Упаковка по уровням
В такой типичной многоуровневой архитектуре один уровень выделяется для веб-кода, один уровень — для «бизнес-логики» и один уровень — для работы с хранилищем данных. Иными словами, горизонтальные уровни используются как способ группировки по подобию. В «строгой многоуровневой архитектуре» уровни должны зависеть только от следующего смежного уровня. В Java уровни обычно реализуются в виде пакетов. Как показано на рис. 34.1, все зависимости между уровнями (пакетами) направлены вниз. В этом примере у нас имеются следующие Java-типы:
• OrdersController: веб-контроллер, иногда что-то вроде MVC-контроллера из Spring, обрабатывающего запросы из Веб.
• OrdersService: интерфейс, определяющий «бизнес-логику», связанную с заказами.
• OrdersServiceImpl: реализация службы заказов[63].
• OrdersRepository: интерфейс, определяющий порядок доступа к информации о заказах в хранилище.
• JdbcOrdersRepository: реализация интерфейса хранилища.
В своей статье Presentation Domain Data Layering[64] («Многоуровневая организация: представление, бизнес-логика, данные») Мартин Фаулер написал, что такая трехуровневая организация отлично подходит для начального этапа. И он не одинок. Во многих книгах, руководствах, курсах и примерах кода демонстрируются способы создания многоуровневой архитектуры. Это очень быстрый способ без особых затруднений создать и запустить что-то. Проблема, как указывает Мартин, в том, что с ростом масштаба и сложности программного обеспечения трех больших слоев кода оказывается недостаточно и приходится задумываться о более дробной организации.
Другая проблема, как уже сказал «дядюшка Боб»[65], — многоуровневая архитектура не кричит о своем практическом назначении. Поместите рядом код двух многоуровневых архитектур из разных предметных областей, и они почти наверняка будут выглядеть пугающе похожими: веб-интерфейсы, службы и хранилища. Многоуровневые архитектуры страдают еще от одного большого недостатка, но мы поговорим о нем позже.
Упаковка по особенностям
Другой вариант организации кода — «упаковка по особенностям». Это разделение по вертикали, основанное на объединении связанных особенностей, предметных понятий и общих корней (если использовать терминологию предметно-ориентированного проектирования). В типичных реализациях, которые мне доводилось видеть, все типы помещаются в один Java-пакет, имя которого отражает идею группировки.
При такой организации, изображенной на рис. 34.2, имеются те же интерфейсы и классы, но они помещаются в единый Java-пакет. Ее легко получить простым рефакторингом из «упаковки по уровням», но теперь верхнеуровневая структура кричит о предметной области. Теперь видно, что кодовая база имеет какое-то отношение к заказам, а не к Веб, не к службам и не к хранилищам. Другим преимуществом является относительная простота поиска кода для изменения, например, когда потребуется изменить вариант использования «просмотр заказов». Весь код сосредоточен вместе, а не разбросан по разным Java-пакетам[66].
Рис. 34.2. Упаковка по особенностям
Мне часто встречаются команды разработчиков, испытывающие трудности с разделением на горизонтальные уровни («упаковка по уровням») и переключающиеся на разделение по вертикали («упаковка по особенностям»). По моему мнению, оба подхода не оптимальны. Дочитав книгу до этого места, многие из вас подумают, что можно сделать лучше, и они будут правы.
Порты и адаптеры
Как говорил «дядюшка Боб», подходы, такие как «порты и адаптеры», «гексагональная архитектура», «граница, управление, сущность» и др., придуманы с целью создания архитектур, в которых прикладной/предметный код независим и отделен от технических деталей реализации, таких как фреймворки и базы данных. Такие базы кода часто состоят из двух областей: «внутренняя» (предметная) и «внешняя» (инфраструктура), как показано на рис. 34.3.
Рис. 34.3. Кодовая база с внутренней и внешней областями
«Внутренняя» область включает все предметные понятия, а «внешняя» отвечает за взаимодействия с внешним миром (то есть содержит пользовательские интерфейсы, базы данных, механизмы интеграции со сторонними продуктами). Главное правило при такой организации: «внешняя» область зависит от «внутренней», но никогда наоборот. На рис. 34.4 изображена версия реализации случая использования «просмотр заказов».
Рис. 34.4. Случай использования «просмотр заказов»
Пакет com.mycompany.myapp.domain в этой версии — «внутренний», а другие пакеты — «внешние». Обратите внимание, что зависимости указывают в сторону «внутренней» области. Внимательный читатель наверняка заметит, что класс OrdersRepository переименован в Orders. Это объясняется влиянием правил предметно-ориентированного проектирования, которые требуют всему, что находится «внутри», давать простые имена из словаря «универсального предметного языка». Так, в предметных дискуссиях мы говорим «заказы», а не «хранилище заказов».
Также стоит отметить, что это существенно упрощенная версия UML-диаграммы классов, потому что в ней отсутствуют такие элементы, как интеракторы и объекты для передачи данных через границы зависимостей.
Упаковка по компонентам
В целом я согласен с рассуждениями о принципах SOLID, эквивалентности повторного использования (REP), согласованного изменения (CCP) и совместного повторного использования (CRP) и большинством советов в этой книге, но я пришел к немного другим выводам, касающимся организации кода. Поэтому я представлю еще один вариант, который я называю «упаковка по компонентам». Прежде всего, в своей карьере я много лет посвятил созданию корпоративного программного обеспечения, в основном на Java, во многих прикладных областях. Эти программные системы сильно отличались. Подавляющее их число были основаны на Веб, но имелись также клиент-серверные[67], распределенные, основанные на обмене сообщениями и некоторые другие системы. Хотя технологии отличались, всех их объединяла традиционная многоуровневая архитектура.
Я уже упоминал пару причин, почему многоуровневую архитектуру следует считать неудовлетворительной, но это еще не все. Цель многоуровневой архитектуры — отделить код, выполняющий схожие функции. Веб-интерфейс отделяется от бизнес-логики, которая, в свою очередь, отделяется от механизмов доступа к данным. Как было показано на UML-диаграмме классов, с точки зрения реализации уровень примерно соответствует пакету. С точки зрения доступности кода, чтобы OrdersController мог зависеть от интерфейса OrdersService, последний должен быть объявлен общедоступным, потому что класс и интерфейс находятся в разных пакетах. Аналогично общедоступным должен быть объявлен интерфейс OrdersRepository, так как он используется классом OrdersServiceImpl, находящимся за пределами пакета, определяющего функции доступа к хранилищу.
В настоящей многоуровневой архитектуре стрелки зависимостей всегда должны быть направлены вниз. Уровни должны зависеть только от соседнего, нижележащего уровня. В результате введения некоторых правил, определяющих, как должны зависеть элементы в базе кода, получается красивый, чистый, ациклический граф зависимостей. Большая проблема в том, что мы можем хитрить, вводя некоторые нежелательные зависимости, и при этом получать замечательный ациклический граф зависимостей.
Предположим, что на работу был нанят новый специалист, он присоединился к вашей команде и вы поручаете ему реализовать другой вариант использования, связанный с заказами. Как всякий новичок, этот человек хочет произвести впечатление и реализовать порученное ему задание максимально быстро. Посидев несколько минут с чашкой кофе, новичок замечает существующий класс OrdersController и решает, что это именно тот код, который должен использоваться новой веб-страницей, порученной ему. Но ему нужны некоторые данные о заказах из базы данных. Новичка озаряет: «О, здесь уже есть интерфейс OrdersRepository. Я могу просто внедрить реализацию в мой контроллер. Отлично!» Спустя несколько минут он создает действующую веб-страницу. Но получившаяся UML-диаграмма выглядит, как показано на рис. 34.5.
Стрелки зависимостей все еще направлены вниз, но теперь для некоторых вариантов использования OrdersController минует OrdersService. Такую организацию часто называют нестрогой многоуровневой архитектурой, так как уровням позволено перепрыгивать через смежные, соседние уровни. В некоторых ситуациях это делается намеренно, например, если вы пытаетесь следовать шаблону CQRS[68]. Во многих других случаях нежелательно перепрыгивать через уровень бизнес-логики, особенно если эта бизнес-логика отвечает за авторизацию доступа к отдельным записям, например.
Рис. 34.5. Нестрогая многоуровневая архитектура
Новый вариант использования работает, но наверняка реализован не так, как вам хотелось бы. Нечто подобное я видел во многих командах, которые посещал как консультант, и обычно это проявляется, когда команды начинают выяснять, как выглядит база кода, часто в первый раз.
В такой ситуации необходимо установить правило — архитектурный принцип, — которое гласит, например: «Веб-контроллеры никогда не должны обращаться к хранилищу непосредственно». Проблема, конечно, заключается в исполнении правила. Многие команды, с которыми я встречался, заявляли: «Мы обеспечиваем соблюдение этого принципа строгой дисциплиной и обзорами кода, потому что доверяем нашим разработчикам». Это хорошо, что есть такая уверенность, но все мы знаем, что происходит, когда бюджет и сроки начинают приближаться к концу.
Намного меньше команд сообщали мне, что они используют инструменты статического анализа (например, NDepend, Structure101, Checkstyle) для автоматической проверки и выявления архитектурных нарушений во время сборки. Возможно, вы уже видели такие правила; обычно они имеют форму регулярных выражений или строк с шаблонными символами, которые указывают: «типы в пакете **/web не должны использоваться типами в **/data» и проверяются после этапа компиляции.
Это немного грубоватый подход, но он может помочь, сообщая о нарушениях архитектурных принципов, которые (по вашему мнению) должны помешать вашей команде разработчиков выполнить сборку. Проблема обоих подходов в том, что они чреваты ошибками и цикл обратной связи дольше, чем хотелось бы. Если отключить проверку, в результате код может превратиться в «большой ком грязи»[69]. Лично я хотел бы, чтобы за соблюдением архитектурных принципов следил компилятор.
Это ведет нас к варианту «упаковка по компонентам». Цель этого гибридного подхода, обсуждавшегося до сих пор, — упаковать все обязанности, связанные с одним крупным компонентом, в единый Java-пакет. Речь идет о сервис-ориентированном представлении программной системы, что, собственно, мы наблюдаем в архитектурах микрослужб. Подобно портам и адаптерам, интерпретирующим Веб как всего лишь еще один механизм доставки, методика «упаковка по компонентам» помогает отделить пользовательский интерфейс от этих крупных компонентов. На рис. 34.6 показано, как мог бы выглядеть вариант «просмотр заказов».
По сути, этот подход связывает «бизнес-логику» и код для работы с хранилищем в единое нечто, что мы называем «компонентом». Выше в книге дядюшка Боб дал такое определение компонента:
Компоненты — это единицы развертывания. Они представляют наименьшие сущности, которые можно развертывать в составе системы. В Java — это jar-файлы.
Рис. 34.6. Вариант использования «просмотр заказов»
Мое определение компонента немного отличается: «Группа функциональных возможностей, находящихся за общим чистым интерфейсом, которые постоянно находятся внутри среды выполнения, такой как приложение». Это определение взято из описания модели C4 программной архитектуры («C4 software architecture model»)[70], которая определяет простую иерархическую организацию статических структур программной системы в терминах контейнеров, компонентов и классов (или кода). В этом описании говорится, что программная система состоит из одного или нескольких контейнеров (например, веб-приложений, мобильных приложений, обычных приложений, баз данных, файловых систем), каждый из которых содержит один или несколько компонентов, которые, в свою очередь, реализуются одним или несколькими классами (или кодом). Находится ли каждый компонент в отдельном jar-файле — это уже второстепенный вопрос.
Ключевое преимущество подхода «упаковки по компонентам» заключается в размещении всего кода, например, имеющего отношение к обработке заказов, в одном месте — в компоненте OrdersComponent. Задачи внутри компонента все еще разделены, то есть бизнес-логика отделена от функций доступа к хранилищу, но это разделение является уже деталью реализации компонентов, о которой потребителям знать не обязательно. Это напоминает организацию микрослужб или сервис-ориентированную архитектуру, когда имеется отдельная служба OrdersService, инкапсулирующая все, что связано с обработкой заказов, отличаясь лишь режимом разделения.
Организацию монолитного приложения в виде набора тщательно проработанных компонентов можно рассматривать как шаг в направлении архитектуры микрослужб.
Дьявол в деталях реализации
На первый взгляд кажется, что все четыре подхода представляют собой разные способы организации кода и поэтому могут считаться разными архитектурными стилями. Это ощущение начинает быстро укрепляться, если нет понимания деталей реализации.
Я постоянно наблюдаю чересчур свободное использование модификатора доступа public в таких языках, как Java. Похоже, что разработчики используют ключевое слово public инстинктивно, не думая. Этот инстинкт хранится в нашей мышечной памяти. Если не верите, взгляните на примеры кода в книгах, руководствах и открытых фреймворках на сайте GitHub. Эта тенденция, похоже, никак не связана с архитектурным стилем, используемым для организации кода, будь то горизонтальные уровни, порты и адаптеры или что-то еще.
Объявление всех типов общедоступными означает отказ от возможностей инкапсуляции, предлагаемых языком программирования. Как результат, это открывает возможность любому написать код с реализацией конкретного класса, нарушающий используемый архитектурный стиль.
Организация и инкапсуляция
Взглянем на эту проблему с другой стороны. Если все типы в Java-приложении объявить общедоступными, пакеты превратятся в простой механизм организации (в группировки, как папки), утратив свойства инкапсуляции. Так как общедоступные типы могут беспрепятственно использоваться в любой точке приложения, вы фактически можете игнорировать пакеты, потому что они практически не несут никакой ценности. В результате, если вы игнорируете пакеты (потому что они не имеют никаких средств инкапсуляции и сокрытия), становится совершенно неважно, какой архитектурный стиль вы пытаетесь воплотить. Если все типы объявить общедоступными, пакеты Java на UML-диаграммах, представленных выше, превращаются в ненужную деталь. По сути, при злоупотреблении подобными объявлениями все четыре архитектурных подхода, представленные выше в этой главе, становятся практически неотличимыми друг от друга (рис. 34.7).
Рис. 34.7. Все четыре архитектурных стиля идентичны
Обратите внимание на стрелки между типами на рис. 34.7: они не зависят от архитектурного стиля, который вы пытаетесь воплотить. Концептуально стили очень разные, но синтаксически они идентичны. Кроме того, можно даже утверждать, что после объявления всех типов общедоступными эти четыре стиля превращаются лишь в четыре способа описания традиционной архитектуры с горизонтальными уровнями. Это ловкий фокус, и, конечно же, никто не будет объявлять все свои Java-типы общедоступными. За исключением случаев, когда это действительно делается. И я видел их.
Модификаторы доступа в Java не идеальны[71], но их игнорирование может вызывать проблемы. Порядок распределения типов Java по пакетам фактически может иметь большое значение для доступности (или недоступности) этих типов, когда модификаторы доступа применяются соответственно. Если вернуть пакеты и отметить те типы (сделав их бледнее на диаграмме), которым можно дать более ограничивающий модификатор, картина станет интереснее (рис. 34.8).
Двигаясь слева направо в подходе «упаковка по уровням», интерфейсы OrdersService и OrdersRepository должны быть объявлены общедоступными, потому что имеют входящие зависимости от классов, находящихся за пределами пакета, в котором объявлены эти интерфейсы. Классам с реализациями (OrdersServiceImpl и JdbcOrdersRepository), напротив, можно придать более ограниченную видимость (на уровне пакета). Никто не должен знать об их существовании; они являются деталями реализации.
В подходе «упаковка по особенностям» единственной точкой входа в пакет является OrdersController, поэтому доступ ко всему остальному можно ограничить рамками пакета. Важно отметить, что в такой ситуации никакой другой код, находящийся за пределами этого пакета, не сможет получить никакой информации о заказах в обход контроллера. Иногда это может быть нежелательно.
В подходе с портами и адаптерами интерфейсы OrdersService и Orders имеют входящие зависимости из других пакетов, поэтому они должны быть объявлены общедоступными. И снова, доступность классов реализации
Рис. 34.8. Бледно-серым цветом выделены типы с более ограничивающим модификатором доступа
можно ограничить рамками пакета и внедрять зависимости во время выполнения.
Наконец, в подходе «упаковка по компонентам» интерфейс OrdersComponent имеет входящую зависимость от контроллера, но доступ ко всему остальному можно ограничить рамками пакета. Чем меньше общедоступных типов, тем меньше число потенциальных зависимостей. В данном случае код за пределами этого пакета не имеет возможности[72] напрямую использовать интерфейс OrdersRepository или его реализацию, поэтому соблюдение архитектурного принципа можно переложить на компилятор. То же самое можно проделать в .NET с помощью ключевого слова internal, но при этом придется создать отдельные сборки для всех компонентов.
Для большей ясности отмечу, что все, о чем рассказывалось здесь, относится к монолитному приложению, когда весь код находится в едином дереве исходных текстов. Если вы создаете такое приложение, я рекомендовал бы проводить в жизнь архитектурные принципы, опираясь на компилятор, а не полагаться на самодисциплину и инструменты, выполняющиеся после компиляции.
Другие режимы разделения
Кроме средств, поддерживаемых языком программирования, часто существуют другие способы разделения зависимостей в исходном коде. Для Java имеются свои инфраструктуры, такие как OSGi, и новейшая система модулей в Java 9. При правильном использовании системы модулей позволяют разделить типы, объявленные как public, и публикуемые (published) типы. Например, можно создать модуль Orders, в котором все типы объявлены как public, но опубликовать только ограниченное число этих типов для внешних потребителей. Это долгожданное нововведение, и я с энтузиазмом воспринимаю появление новой системы модулей Java 9, которая даст нам еще один инструмент для создания хорошего программного обеспечения и вновь разбудит в людях интерес к дизайнерскому мышлению.
Другая возможность разделить зависимости на уровне исходного кода — создать несколько разных деревьев с исходным кодом. Для портов и адаптеров, например, можно было бы создать три таких дерева:
• Исходный код с предметной и бизнес-логикой (то есть все, что не зависит от выбора технологии и фреймворков): OrdersService, OrdersServiceImpl и Orders.
• Исходный код веб-интерфейса: OrdersController.
• Исходный код, реализующий хранение данных: JdbcOrdersRepository.
Последние два дерева имеют зависимости времени компиляции от кода с предметной и бизнес-логикой, который сам по себе ничего не знает о веб-интерфейсе и особенностях хранения данных. С точки зрения реализации это можно сделать, настроив отдельные модули или проекты в конфигурации инструмента сборки (например, Maven, Gradle, MSBuild). В идеале этот шаблон можно применить для отделения деревьев с исходным кодом всех компонентов приложения.
Однако это слишком идеалистическое решение, потому что такое разбиение исходного кода влечет за собой проблемы производительности, сложности и сопровождения.
Более простой подход, используемый теми, кто реализует архитектуру портов и адаптеров, заключается в создании двух деревьев с исходным кодом:
• Предметный код («внутренняя» область).
• Инфраструктурный код («внешняя» область).
Его хорошо иллюстрирует диаграмма (рис. 34.9), которую многие используют для обобщения архитектуры портов и адаптеров с ее зависимостью времени компиляции инфраструктурного кода от предметного.
Рис. 34.9. Предметный и инфраструктурный код
Такой подход к организации исходного кода тоже дает положительные результаты, но вы должны помнить о потенциальных компромиссах. Это то, что я называю «окружным антишаблоном портов и адаптеров». В Париже (Франция) имеется окружная автодорога с названием Boulevard Priphrique (бульвар Периферик), позволяющая обогнуть Париж, не утруждая себя сложностями движения внутри города. Включение всего инфраструктурного кода в единое дерево подразумевает потенциальную возможность для кода из любой части приложения (например, веб-контроллера) напрямую вызывать код из друой части приложения (например, функции для работы с базой данных), без пересечения предметной области. Это особенно верно, если забыть применить в том коде соответствующие модификаторы доступа.
Заключение: недостающий совет
Основная цель этой главы — подчеркнуть, что любые, самые лучшие дизайнерские намерения можно уничтожить в мгновение ока, если не учитывать тонкости стратегии реализации. Подумайте, как ваш дизайн должен отображаться в структуру кода, как организовать этот код и какие режимы разделения применять во время выполнения и компиляции. Старайтесь оставлять открытыми любые возможности, но будьте прагматичными и учитывайте численность вашей команды, навыки ее членов, сложность решения и ограничения по времени и бюджету. Также подумайте, как использовать компилятор для принудительного соблюдения выбранного архитектурного стиля, и следите за связями в других областях, таких как модели данных. Дьявол кроется в деталях реализации.
Часть VII. Приложение
Архитектурная археология
Чтобы определить принципы хорошей архитектуры, совершим путешествие по последним 45 годам и познакомимся с некоторыми проектами, над которыми я работал начиная с 1970 года. Некоторые из этих проектов представляют определенный интерес с архитектурной точки зрения. Другие интересны извлеченными уроками, повлиявшими на последующие проекты.
Это приложение несколько автобиографично. Я старался придерживаться темы обсуждения архитектуры, но, как в любой автобиографической истории, иногда в нее вторгаются другие факторы. ;-)
Профсоюзная система учета
В конце 1960-х годов компания ASC Tabulating подписала контракт с местным профсоюзом водителей грузовиков на разработку системы учета. Компания ASC решила реализовать эту систему на машине GE Datanet 30, изображенной на рис. П.1.
Рис. П.1. GE Datanet 30 (фотографию предоставил Эд Телен (Ed Thelen), ed-thelen.org)
Как можно видеть на фотографии, это была огромная[73] машина. Она занимала целую комнату и требовала поддержания определенного микроклимата.
Эта ЭВМ была построена еще до появления интегральных микросхем. Она была собрана на дискретных транзисторах, и в ней имелось даже несколько радиоламп (хотя они использовались только в усилителях для управления ленточными накопителями).
По сегодняшним меркам машина была огромная, медлительная и примитивная. В ней имелось 16 К 18 бит оперативной памяти с временем цикла порядка 7 микросекунд[74]. Она занимала большую комнату с климатической установкой. Имела приводы для магнитной ленты с семью дорожками и жесткий диск емкостью около 20 Мбайт.
Этот диск был настоящим монстром. Вы можете увидеть его на рис. A.2, но эта фотография не позволяет оценить размеры чудовища. Он был выше моего роста. Пластины были 36 дюймов (примерно 91 сантиметр) в диаметре и 3/8 дюйма (9,5 миллиметра) в толщину. Одна из пластин изображена на рис. A.3.
Теперь подсчитайте пластины на первой фотографии. Их было более десятка. Для каждой имелась своя лапа с головкой, приводившаяся в движение пневматическим приводом. В процессе работы можно было видеть, как головки перемещаются поперек пластин. Время позиционирования головки колебалось от половины секунды до секунды.
Когда этот зверь включался, он рычал, как самолет. Пол ходил ходуном, пока тот набирал скорость[75].
ЭВМ Datanet 30 была знаменита своей возможностью асинхронно управлять большим количеством терминалов с относительно высокой скоростью. Именно это требовалось компании ASC.
Рис. П.2. Жесткий диск с пластинами (фотографию предоставил Эд Телен (Ed Thelen), ed-thelen.org)
Компания ASC находилась в Лейк Блафф, штат Иллинойс, в 30 милях к северу от Чикаго. Офис профсоюза размещался в центре Чикаго. Профсоюз нанял примерно десять человек для ввода данных в систему через терминалы CRT[76] (рис. П.4). Они также могли печатать отчеты на телетайпах ASR35 (рис. П.5).
Терминалы CRT поддерживали скорость обмена 30 символов в секунду. Это была хорошая скорость для конца 1960-х, потому что модемы в те дни были очень простенькими.
ASC арендовала у телефонной компании с десяток выделенных телефонных линий и вдвое больше 300-бодовых модемов для соединения Datanet 30 с этими терминалами.
В те времена компьютеры поставлялись без операционной системы. Они не имели даже файловых систем. У вас имелся только ассемблер.
Рис. П.3. Одна из пластин диска: 3/8 дюйма толщиной, 36 дюймов в диаметре (фотографию предоставил Эд Телен (Ed Thelen), ed-thelen.org)
Рис. П.4. Терминал CRT (фотографию предоставил Эд Телен (Ed Thelen), ed-thelen.org)
Рис. П.5. Телетайп ASR35 (с разрешения Джо Мейбла (Joe Mabel))
Если вам нужно было сохранить данные на диске, вы записывали их на диск. Не в файл. Не в каталог. Вы должны были определить дорожку, пластину и сектор для сохраняемых данных и затем управлять диском, чтобы записать туда данные. Да, это означало необходимость писать свой драйвер диска.
В системе учета имелось три вида записей с информацией об агентах, работодателях и членах профсоюза. Для этих записей поддерживались все четыре CRUD-операции[77], но кроме этого система включала операции для рассылки квитанций на уплату взносов, определения изменений в общем реестре и другие.
Первоначальная версия системы была написана на ассемблере консультантом, которому чудом удалось впихнуть ее в 16 К.
Как нетрудно догадаться, такая большая ЭВМ, как Datanet 30, была очень дорогой в обслуживании и эксплуатации. Услуги консультанта, поддерживавшего программное обеспечение, тоже обходились очень дорого. Более того, на рынке уже появились и стали набирать популярность более дешевые мини-компьютеры.
В 1971 году, когда мне было 18, компания ASC наняла меня и двух моих друзей-гиков, чтобы переписать систему учета для мини-компьютера Varian 620/f (рис. П.6). Компьютер стоил недорого. Наши услуги стоили недорого. Для ASC это было отличной сделкой.
Машина Varian имела 16-битную шину и 32 К 16 оперативной памяти. Длительность цикла составляла примерно 1 микросекунду. Эта машина была намного мощнее, чем Datanet 30. В ней использовалась дико успешная дисковая технология 2314, разработанная в IBM, позволявшая хранить 30 мегабайт на пластинах, имевших всего 14 дюймов в диаметре, которые уже не могли пробивать бетонные стены!
Конечно, у нас все еще не было операционной системы. Не было файловой системы. Не было и высокоуровневого языка программирования. У нас имелся только ассемблер. Но мы справились с заданием.
Рис. П.6. Мини-компьютер Varian 620/f (взято с сайта The Minicomputer Orphanage)
Вместо попытки втиснуть всю систему в 32 К, мы создали систему оверлеев. Приложения могли загружаться с диска в блок памяти, выделенной для оверлеев. Они могли выполняться в этой памяти и вытесняться со своими данными в памяти обратно на диск, чтобы дать возможность поработать другим программам.
Программы могли загружаться в область оверлеев, выполняться ровно столько, сколько необходимо для заполнения выходных буферов, и затем выгружаться на диск, чтобы освободить память для следующей программы.
Конечно, когда пользовательский интерфейс работает со скоростью 30 символов в секунду, программы тратят массу времени на ожидание. У нас в запасе оставалось достаточно времени, чтобы программы могли загружаться и записываться на диск, обеспечива максимальную скорость обмена с терминалами. Никто и никогда не жаловался на проблемы с временем отклика.
Мы написали вытесняющего диспетчера задач, управляющего прерываниями и вводом/выводом. Мы написали приложения; мы написали драйверы диска и драйверы терминалов, драйверы накопителей на магнитной ленте и все остальное в этой системе. В этой системе не было ни одного бита, написанного не нами. Это был тяжелый труд в течение множества 80-часовых недель, но мы запустили этого зверя за 8 или 9 месяцев.
Система имела простую архитектуру (рис. П.7). Когда приложение запускалось, оно генерировало данные до заполнения выходного буфера заданного терминала. Затем диспетчер задач выгружал это приложение и загружал новое. При этом диспетчер продолжал выводить информацию со скоростью 30 символов в секунду почти до его опустошения. Затем он вновь загружал приложение, чтобы снова заполнить буфер.
Рис. П.7. Архитектура системы
В этой системе есть две границы. Первая — вывод символов. Приложения не знали, что их вывод посылается терминалам со скоростью 30 символов в секунду. В действительности для приложений вывод символов был полностью абстрагирован. Приложения просто передавали строки диспетчеру задач, а тот заботился о загрузке их в буферы, отправке символов терминалам и загрузке приложений в память и выгрузке их из памяти.
Зависимости пересекали эту границу в прямом направлении, то есть их направленность совпадала с направленностью потока управления. Приложения имели зависимости времени компиляции от диспетчера задач, и поток управления следовал от приложений в сторону диспетчера. Граница оберегает приложения от просачивания в них информации о типе устройства, в который производится вывод.
Вторая граница пересекалась зависимостями в обратном направлении. Диспетчер задач мог запускать приложения, но не имел зависимости времени компиляции от них. Поток управления следовал от диспетчера к приложениям. Полиморфный интерфейс, инвертирующий зависимость, был прост: каждое приложение запускалось переходом по одному и тому же адресу в области оверлея. Граница оберегает диспетчера от просачивания в него информации об устройстве приложений, кроме адреса точки запуска.
Laser Trim
В 1973 году я поступил на работу в компанию Teradyne Applied Systems (TAS) в Чикаго. Это было подразделение корпорации Teradyne Inc. со штаб-квартирой в Бостоне. Мы занимались системой, управлявшей довольно мощными лазерами для обработки электронных компонентов с высокой точностью.
В ту пору производители электронных компонентов использовали метод шелкотрафаретной печати на керамической подложке. Подложки имели размер примерно 1 квадратный дюйм. Компонентами были обычные резисторы — устройства, создающие сопротивление электрическому току.
Величина сопротивления резистора зависела от множества факторов, включая состав и геометрию. Чем шире был резистор, тем меньшее сопротивление он оказывал.
Наша система позиционировала керамическую подложку в жгуте проводов, соединяющих датчики с резисторами. Система должна была измерять сопротивление и затем с помощью лазера отсекать части резистора, делая его тоньше и тоньше, пока не будет достигнута желаемая величина сопротивления с точностью до десяти процентов.
Мы продавали эти системы производителям, а также использовали некоторые свои системы для производства небольших партий по заказам менее крупных производителей.
Система работала на компьютере M365. Это было время, когда многие компании строили свои компьютеры: корпорация Teradyne строила M365 и передавала их своим подразделениям. M365 был усовершенствованной версией PDP-8 — популярного в те дни мини-компьютера.
M365 управлял координатным столом, который перемещал керамические подложки между датчиками. Также он управлял системой измерения и лазером. Позиционирование лазера осуществлялось посредством вращающихся X-Y зеркал. Компьютер также управлял мощностью лазера.
Среда разработки для M365 была довольно примитивной. Этот компьютер не имел диска. Данные сохранялись на картриджах с магнитной лентой, которые выглядели как старые 8-дорожечные аудиокассеты. Лента и приводы производились компанией Tri-Data.
Так же как 8-дорожечные аудиокассеты того времени, лента была склеена в петлю. Привод мог прокручивать ее только в одном направлении — в них не было функции перемотки! Если требовалось установить ленту в начальную позицию, нужно было прокручивать ее вперед до достижения «точки загрузки».
Лента прокручивалась со скоростью примерно 1 фут в секунду (примерно 30 сантиметров в секунду). То есть петля из ленты длиной 25 футов (чуть больше 7,5 метра) перематывалась до точки загрузки самое большее за 25 секунд. По этой причине Tri-Data выпускала картриджи с лентой разной длины, от 10 до 100 футов (примерно от 3 до 30 метров).
На передней панели M365 имелась кнопка, по нажатии которой производилась загрузка начальной программы в память и ее запуск. Эта программа могла прочитать первый блок с ленты и запустить его. Обычно в этом блоке хранился загрузчик, загружавший операционную систему, хранящуюся в остальных блоках на ленте.
Операционная система запрашивала у пользователя имя программы для запуска. Эти программы хранились на ленте, сразу вслед за операционной системой. Пользователь мог ввести имя программы — например, ED-402 Editor, — а операционная система отыскивала ее на ленте, загружала и запускала.
Консолью служил ASCII CRT терминал с зеленым свечением, шириной 72 символа[78] и высотой 24 строки. Он мог отображать только символы верхнего регистра.
Чтобы отредактировать программу, нужно было загрузить редактор ED-402 Editor и затем вставить ленту с исходным кодом. Редактор позволял прочитать с ленты в память один блок с исходным кодом и вывести его на экран. В одном блоке можно было сохранить до 50 строк кода. Для внесения изменений нужно было переместить курсор в требуемую строку и ввести текст, примерно так, как это делается в редакторе vi. По завершении требовалось записать блок на другую ленту и прочитать следующий блок с исходной ленты. Вы должны были продолжать эти манипуляции, пока не закончите.
Не было никакой возможности прокрутить блоки в обратном направлении. Правка программы производилась линейно, от начала до конца. Чтобы вернуться в начало, нужно было закончить копирование исходного кода на выходную ленту и затем начать новый сеанс редактирования уже с этой лентой в качестве исходной. Неудивительно, что при таких ограничениях мы сначала писали свои программы на бумаге, вносили все правки вручную красным карандашом и только потом правили программу блок за блоком, сверяясь с пометками в листинге на бумаге.
Закончив правку программы, мы возвращались в операционную систему и вызывали ассемблер. Ассемблер читал код с исходной ленты и записывал двоичный код на другую ленту, при этом выводил листинг на наш последовательный принтер.
Ленты не были на 100% надежными, поэтому мы выполняли запись сразу на две ленты. Это увеличивало вероятность, что хотя бы одна из них не будет содержать ошибок.
Наша программа состояла примерно из 20 000 строк кода, а ее компиляция занимала примерно 30 минут. Шансы получить в это время ошибку чтения с ленты были 1 : 10. Если ассемблер сталкивался с такой ошибкой, он издавал сигнал на консоли и затем начинал выводить поток ошибок на принтер. Этот сигнал можно было услышать во всей лаборатории. Также можно было услышать проклятия несчастного программиста, только что узнавшего, что ему придется вновь запустить 30-минутный процесс компиляции.
Программа имела типичную для тех дней архитектуру. У нас имелась главная операционная программа (Master Operating Program), которую мы называли «the MOP». Она осуществляла управление базовыми функциями ввода/вывода и предоставляла рудиментарную «командную оболочку» для консоли. Многие подразделения Teradyne использовали общий исходный код MOP, адаптируя его под собственные нужды. Как следствие, мы пересылали друг другу изменения в исходном коде в форме бумажных листингов с поправками, которые затем (очень тщательно) вносили вручную.
Измеряющим оборудованием, координатными столами и лазером управляла специальная утилита. Граница между этим уровнем и MOP была запутанной. Уровень утилиты часто вызывал уровень MOP, а специально модифицированная версия MOP часто вызывала утилиту. В действительности мы не разделяли их на два уровня. Для нас это был лишь некоторый код, добавляемый нами в MOP и создающий тесные связи.
Затем появился уровень изоляции. Этот уровень поддерживал интерфейс виртуальной машины для прикладных программ, которые писались на совершенно другом, предметно-ориентированном языке (Domain-Specific Language; DSL). Язык включал операции перемещения лазера и координатного стола, выполнения реза и измерений и т.д. Наши клиенты писали на этом языке свои программы управления лазером, а изолирующий уровень выполнял их.
Этот подход не предусматривал создания машинно-независимого языка программирования для управления лазером. В действительности язык имел множество связей с уровнями, лежащими ниже. Скорее этот подход дал прикладным программистам более «простой» язык, чем ассемблер M356, на котором они описывали свои задания для лазерной обработки.
Система могла загружать такие программы с ленты и выполнять их. По сути, наша система была операционной системой для приложений лазерной обработки.
Система была написана на ассемблере M365 и компилировалась в единственную единицу компиляции с абсолютным двоичным кодом.
Границы в этом приложении в лучшем случае были мягкими. Даже граница между системным кодом и приложениями на DSL не имела строгой силы. Повсюду были связи.
Но это было типично для программного обеспечения начала 1970-х.
Контроль алюминиевого литья под давлением
В середине 1970-х годов, когда организация ОПЕК вводила эмбарго на поставки нефти, а дефицит бензина провоцировал на заправочных станциях драки между озлобленными водителями, я поступил на работу в Outboard Marine Corporation (OMC). Это была головная компания, объединившая Johnson Motors и Lawnboy Lawnmowers.
OMC имела большое производство в Уокигане, штат Иллинойс, по производству литых алюминиевых деталей для всех видов двигателей и изделий, изготавливаемых компанией. Алюминий расплавлялся в огромных печах и затем в ковшах перевозился к десяткам и десяткам машин для литья. Каждой машиной управлял человек-оператор, отвечавший за установку изложниц, выполнение цикла литья и извлечение готовых деталей. Зарплата операторов зависела от количества отлитых деталей.
Меня наняли для разработки проекта по автоматизации цеха. OMC приобрела ЭВМ IBM System/7, которая была ответом IBM на появление мини-компьютеров. Они связали этот компьютер со всеми машинами для литья в цехе, чтобы мы могли подсчитать время работы и количество циклов каждой машины. Наша задача заключалась в том, чтобы собрать всю эту информацию и вывести ее на зеленые экраны терминалов 3270.
Программы для этой машины писались на языке ассемблера. И снова весь код, выполнявшийся на этом компьютере, был написан нами до последнего бита. У нас не было ни операционной системы, ни библиотек подпрограмм, ни фреймворков. Это был просто код.
Причем это был код, управляемый прерываниями и действующий в режиме реального времени. Каждый раз, когда какая-то машина завершала цикл, мы обновляли пакет статистических данных и посылали сообщение большой ЭВМ IBM 370, где работала программа на CICS-COBOL, выводившая эти данные на зеленые экраны.
Я ненавидел эту работу. Боже, как я ее ненавидел. Нет, сама работа была интересной! Но культура... Достаточно сказать, что я был обязан носить галстук.
Я старался. Я очень старался. Но я был очень недоволен этой работой, и мои коллеги знали об этом. Они понимали это, потому что я забывал важные даты или просыпал в дни, когда надо было рано вставать, чтобы прийти на важное совещание. Это была единственная работа, связанная с программированием, с которой меня уволили, — и поделом.
С архитектурной точки зрения в этой системе нет ничего поучительного, кроме одного. ЭВМ System/7 имела очень интересную инструкцию установки программного прерывания SPI (Set Program Interrupt). Она позволяла вызвать прерывание процессора, чтобы обработать любые другие низкоприоритетные прерывания, стоящие в очереди. В современном языке Java имеется схожий аналог, который называется Thread.yield().
4-TEL
В октябре 1976 года, после увольнения из OMC, я вернулся в Teradyne, но в другое подразделение, где я проработал следующие 12 лет. Там я занимался проектом под названием 4-TEL. Его целью было еженощное тестирование всех телефонных линий, обслуживаемых компанией, и создание отчета с перечислением всех линий, требующих ремонта. Это позволяло сосредоточить внимание обслуживающего персонала на конкретных телефонных линиях.
Эта система начинала свой путь с той же архитектуры, что и система Laser Trim. Это было монолитное приложение, написанное на языке ассемблера и не имевшее каких-то значительных границ. Но в то время, когда я вернулся в компанию, все должно было измениться.
Система использовалась сотрудниками сервисного центра (Service Center; SC). Сервисный центр охватывал несколько телефонных станций (Central Offices; CO), каждый из которых мог обслуживать до 10 000 телефонных линий. Аппаратура переключения линий и измерения уровня сигнала должна была размещаться в телефонных станциях (CO). Поэтому мы установили там компьютеры M365. Мы называли их тестерами линий в телефонных станциях (Central Office Line Testers; COLTs). Еще один M365 находился в сервисном центре (SC); он назывался компьютером зоны обслуживания (Service Area Computer; SAC). К компьютеру SAC было подключено несколько модемов для обмена данными с несколькими компьютерами COLT на скорости 300 бод (30 символов в секунду).
Сначала всю работу выполняли компьютеры COLT, включая все взаимодействия с консолями, обслуживание меню и составление отчетов. Компьютер SAC играл роль простого мультиплексора, получавшего данные от компьютеров COLT и выводившего их на экран.
Проблема такой организации состояла в том, что скорость 30 символов в секунду действительно была слишком маленькой. Работникам не очень нравилось наблюдать, как появляются символы на экране, особенно если учесть, что их интересовал небольшой объем ключевых данных. Кроме того, в те дни оперативная память в M365 стоила очень дорого, а программа была большой.
Поэтому было решено отделить часть программы, осуществляющую тестирование линий, от части, анализирующей результаты и печатающей отчеты. Последнюю предполагалось перенести на компьютер SAC, а первая должна была продолжать работать на компьютерах COLT. Это должно было позволить использовать в качестве COLT машины поменьше, с меньшим объемом памяти, и значительно повысить скорость вывода информации на терминал, потому что отчеты должны были генерироваться на компьютере SAC.
Результат превзошел самые смелые ожидания. Информация на экране обновлялась очень быстро (после соединения с соответствующим компьютером COLT), а объем памяти в компьютерах COLT значительно уменьшился.
Граница получилась очень четкой и надежной. Компьютеры SAC и COLT обменивались очень короткими пакетами. Эти пакеты были очень простой формой предметно-ориентированного языка с такими командами, как «DIAL XXXX» или «MEASURE».
Загрузка M365 осуществлялась с магнитной ленты. Накопители на магнитной ленте были дорогими и не очень надежными, особенно в промышленном окружении телефонных станций. Кроме того, сама машина M365 стоила довольно дорого в сравнении с остальной электроникой в компьютерах COLT. Поэтому мы приступили к реализации проекта по замене M365 микрокомпьютером на базе микропроцессора 8085.
Новый компьютер состоял из процессорной платы с микропроцессором 8085, платы ОЗУ с 32 Кбайт памяти и трех плат с ПЗУ, содержащих 12 Кбайт памяти, доступной только для чтения. Все эти платы помещались в шасси с измерительным оборудованием, благодаря чему можно было убрать громоздкое шасси, в котором размещалась ЭВМ M365.
Платы ПЗУ содержали по 12 микросхем Intel 2708 EPROM (Erasable Programmable Read-Only Memory — стираемое программируемое постоянное запоминающее устройство, СППЗУ)[79]. На рис. П.8 показано, как выглядела такая микросхема. Мы записывали программы в эти микросхемы, вставляя их в специальное устройство, которое назвалось программатором ППЗУ и управлялось нашей средой разработки. Информацию на микросхемах можно было стирать, подвергая их облучению ультрафиолетовым светом большой интенсивности[80].
Мой друг и я занялись переводом программ для COLT с языка ассемблера M365 на язык ассемблера 8085. Перевод выполнялся вручную и занял почти 6 месяцев. В результате получилось около 30 Кбайт кода 8085.
Наша среда разработки имела 64 Кбайт ОЗУ и не имела ПЗУ, поэтому скомпилированный двоичный код мы могли быстро загрузить в ОЗУ и протестировать.
Получив работоспособную программу, мы переключались на использование СППЗУ (EPROM). Мы программировали 30 микросхем и вставляли их в соответствующие гнезда в трех платах ПЗУ. Каждая микросхема подписывалась, поэтому мы точно знали, какую из них в какое гнездо нужно вставить.
30 Кбайт программного кода — это был единый двоичный блок длиной 30 Кбайт. Чтобы записать этот код в микросхемы ПЗУ, мы просто делили двоичный образ на 30 сегментов по 1 Кбайт и записывали каждый сегмент в микросхему с соответствующей надписью.
Рис. П.8. Микросхема СППЗУ (EPROM)
Эта схема прекрасно работала, и мы начали массовое производство оборудования и развертывание системы в поле.
Но программное обеспечение в первую очередь является программным[81]. Требовалось добавлять новые возможности, исправлять ошибки. А так как базовая система разрасталась, логистика обновления программного обеспечения путем программирования 30 микросхем на каждый экземпляр и замены всех 30 микросхем в каждом офисе превращалась в кошмар.
Возможны были все виды проблем. Иногда микросхемы подписывались неправильно или наклейки с подписями отваливались. Иногда инженер службы эксплуатации мог по ошибке заменить не ту микросхему или поломать один из выводов новой микросхемы. Как следствие, инженерам приходилось носить с собой все 30 микросхем.
Зачем менять все 30 микросхем? Каждый раз, когда добавлялся или удалялся код из 30-килобайтного блока выполняемого кода, изменялись адреса всех машинных инструкций. Изменялись также адреса подпрограмм и функций. То есть затрагивалась каждая микросхема, каким бы простым ни было изменение.
Однажды ко мне зашел мой начальник и попросил решить эту проблему. Он сказал, что нужно найти какой-то способ изменения микропрограммы без замены всех 30 микросхем ПЗУ. Мы в коллективе обсудили эту проблему и приступили к проекту «Векторизация». На его реализацию ушло 3 месяца.
Идея была до смешного простой. Мы разбили 30 Кбайт программного кода на 32 файла с исходным кодом, компилирующихся независимо в блоки меньше одного 1 Кбайт. В начало каждого файла с исходным кодом мы вставили инструкцию, сообщающую компилятору, в какой адрес должен компилироваться данный код (например, ORG C400 для микросхемы ПЗУ, вставляемой в гнездо C4).
Также в начало каждого файла с исходным кодом мы добавили структуру фиксированного размера с адресами всех подпрограмм в этом блоке (вектор переходов). Эта структура имела размер 40 байт, поэтому могла хранить до 20 адресов. Это означало, что блок для одной микросхемы не мог содержать более 20 подпрограмм.
Затем мы создали особую область в ОЗУ, которую называли массивом векторов. Она содержала 32 таблицы по 40 байт — достаточный объем для хранения указателей на начало каждого блока в отдельных микросхемах.
Наконец, мы заменили вызовы подпрограмм в каждом блоке косвенными вызовами через соответствующий вектор в ОЗУ.
Когда происходила загрузка программы, в массив векторов в ОЗУ загружались векторы переходов из всех микросхем ПЗУ, а затем осуществлялся переход в точку запуска главной программы.
У нас все получилось. Теперь, когда исправлялась ошибка или добавлялось что-то новое, мы могли перекомпилировать только один или два файла, записать их на соответствующие микросхемы и передать только эти микросхемы инженеру службы эксплуатации для замены.
Мы сделали блоки кода независимо развертываемыми. Мы изобрели полиморфную диспетчеризацию. Мы изобрели объекты.
Это была архитектура с подключаемыми модулями (плагинами). Мы подключали микросхемы. Мы разработали ее так, чтобы новые возможности можно было включать в наши продукты установкой микросхем с этими возможностями в соответствующие гнезда. Управляющее меню появлялось автоматически, и так же автоматически происходила привязка новых возможностей к приложению.
Конечно, тогда мы не знали о принципах объектно-ориентированного программирования и также ничего не знали об отделении пользовательского интерфейса от бизнес-правил. Но кое-какие основы были заложены, и они здорово нам помогли.
Описанный подход дал одно неожиданное преимущество: мы получили возможность обновлять микропрограмму через модемное соединение. Обнаружив ошибку, мы могли через модемное соединение связаться с устройством и посредством специальной программы-монитора изменить вектор в ОЗУ, ссылающийся на подпрограмму с ошибкой, подставив адрес в пустой области ОЗУ, и затем загрузить в эту область ОЗУ исправленную подпрограмму, вводя машинные коды в шестнадцатеричном формате.
Это было большим благом для службы эксплуатации и для наших клиентов. Если у них возникала проблема, им не требовалось заказывать у нас срочную отправку микросхем с исправленным кодом. Систему можно было исправить немедленно, а новые микросхемы установить в ближайший период обслуживания.
Компьютер зоны обслуживания
Роль компьютера зоны обслуживания (Service Area Computer; SAC) в 4-TEL играл мини-компьютер M365. Эта система взаимодействовала со всеми компьютерами COLT посредством выделенных или коммутируемых линий. Она могла отдавать компьютерам COLT команды на выполнение проверки телефонных линий, принимать результаты и анализировать их для выявления любых проблем.