Внедрение таких методик вполне возможно и вне рамок японского общества. New United Motor Manufacturing Inc (NUMMI) – знаменитое совместное предприятие Toyota и General Motors, ещё в 1970-х годах вошло в «кейсы» бизнес-школ как пример повышения эффективности и качества через смену культуры работы.
Качество программного продукта – многозначное и сложное понятие. Производственная культура – ещё более сложное. В одном можно быть уверенным: ни о какой культуре софтостроения не может идти и речи, если любой программист из коллектива не способен остановить бессмысленный циклический процесс для выяснения, какого же рожна по историям заказчика потребовалось обобщать четырёхногих коров и обеденные столы.
Приключения с TFS
Как вы заметили, я стараюсь перемежать темы, несколько напрягающие мыслительные органы, с занимательными зарисовками из жизни. Продолжим традицию историей о совместимости компонентов сложной тиражируемой системы.
К концу одной недели установка системы под названием MS Team Foundation Server 2008 на виртуальном сервере с Windows 2008 и SQL Server 2005 ещё не завершилась, но пользоваться репозиторием уже было можно.
Все началось в предшествующую пятницу. В процессе было задействовано несколько человек и от клиента, и от нашей команды, включая меня по части установки компонентов СУБД. На неделю практически парализована всякая деятельность, ещё не возобновившаяся в полной мере, до сих пор работает только базовый функционал.
Заклинания, «гугление», помощь консультантов по TFS и даже переписка с Microsoft применялись многократно. Моё осторожное предложение «установить за пару часов SVN для небольшой бригады», высказанное перед началом этой «операции «Ы», не встретило понимания. Тогда как использовать систему предполагалось практически только для работы с исходниками, то есть функционал управления задачами не востребован.
Заказчик, в лице своих администраторов, кстати позитивно отнёсшийся к установке SVN – крупная корпорация национального уровня со своей сложившейся внутренней инфраструктурой и несколькими уровнями бюрократии в эксплуатационном секторе, что означает: все решения принимаются медленно и проходят длительные согласования. Но команда привыкла работать в TFS.
Второе отступление по поводу требуемых компонентов. Собственно, TFS 2008, как выяснилось, требует для себя:
• SQL Server 2005 или выше;
• Reporting Services 2005 или выше;
• Analysis Services 2005 или выше;
• Sharepoint Services не ниже 3 SP1;
• Internet Information Server 6 или выше;
•.NET 3.5 SP1;
• ещё что-то по мелочи вроде конкретных хотфиксов.
По мере изучения сего списка я постепенно впадал в прострацию, сравнивая с ничего не требующим пакетом SVN.
Версия MS SQL Server 2005, поскольку 2008 клиентом не эксплуатируется и даже ещё не куплена. Гетерогенная сеть хоть и интегрируется с Active Directory, но не до конца: интегрированная безопасность (
Изначально под TFS был выделен виртуальный сервер с Win 2008, но базы данных необходимо располагать на другом сервере в обслуживаемом поддержкой кластере.
Предлагаю использовать виртуальный сервер с Win 2003, чтобы избежать возможных проблем с 2005-ми версиями программ. Снова не встречаю понимания, так как скопировать образ – лениво, да и вообще надо переходить на новые технологии.
Через полтора дня ко мне обращаются за консультацией по этому поводу. Устанавливаем службу отчётов (
Убеждаюсь что установщик TFS не находит службу аналитики (
Служба там не установлена, так как сервер в кластере был выделен под базы данных. Ставить дополнительно Analysis Services на эксплуатационный кластер никто в здравом уме не хочет. Администратор пишет письмо Microsoft. Их поддержка изящно уходит от прямого ответа, говоря, что Analysis Services в кластере да ещё и рядом с эксплуатационной СУБД – это не вполне хорошо. Но в то же время говорит, что теоретически можно поставить и на разные серверы.
Только в официальной документации об этом не написано, нужен консультант, желательно от самого Microsoft. Вызов консультанта с предварительным утверждением бюджета – это ещё неделя простоя. Поэтому вариант отвергается.
Принято решение ставить все на один виртуальный сервер. Администраторы кисло морщат лбы, потому что кластер уже настроен под регулярные процедуры поддержки баз данных, а так они получат ещё один сервер в обслуживание, хоть и виртуальный. Но нехотя соглашаются.
Переустанавливаем на него: SQL Server, Reporting Service, Analysis Services. СУБД и все компоненты настроены и работают. Ухожу заниматься собственно делами проекта.
Вскоре у коллег начинаются новые проблемы: не ставится TFS с неким сообщением об ошибке, на которое в гугле находится всего 2 записи, и кода прохождения этого уровня занимательной игры в них нет.
Начинается переписка с экспертами по TFS уже нашей родной конторы. Находим, что вручную и строго предварительно нужно было ставить Sharepoint не ниже 3.0 Service Pack 1. Потому что под Windows 2008 Server установщик TFS этого не делает. Выясняется, что в Windows 2008 Server стандартной версии при попытке добавить роль AppServer искомые службы Sharepoint в списке отсутствуют. Надо её скачивать и устанавливать. Что и было в итоге сделано.
Чуть позже обнаружилось, что при инсталляции служб Sharepoint был выбран полный режим, что неправильно, потому что при этом установщик ставит собственный экземпляр SQL Server Express, игнорируя уже имеющийся на сервере.
Сносим службы Sharepoint, но оказывается, что созданный экземпляр SQL Server инсталлятор Sharepoint не удаляет. Ну что же, удаляем сервис руками, чистим каталоги и реестр.
Дальше работа возобновляется без моего участия, и к вечеру четверга приходит радостная весть: TFS встал, можно попытаться к нему присоединиться.
У всех пользователей похожие имена типа tfs_userNN. На моей локальной виртуальной машине есть проблема: не могу добавить файлы в репозиторий. Права у всех одинаковые. Сообщение об ошибке, связанное с «
В этот момент администраторы прописывают имя нашего сервера в корпоративной DNS [123] , теперь можно отказаться от использования прямого адреса.
Первое же действие выдаёт окошко регистрации, которая проходит нормально, но все последующие операции дают ошибку 401 not authorized.
На удивлённый вопрос «Что случилось?» от администраторов приходит «письмо счастья» с прикреплённым файлом конфигурации для
Программная фабрика: дайте мне модель, и я сдвину Землю
Идея разрабатывать программы, минимизируя стадию кодирования на конкретных языках под заданные платформы, появилась достаточно давно. Прежде всего в связи с неудовлетворительной возможностью языков высокого уровня третьего поколения (3GL) описывать решаемые прикладные задачи в соответствующих терминах. За последнее время к этой причине добавилась ещё и поддержка независимости от целых платформ, ведь прогресс, как мы знаем, неотвратим, особенно «прогресс».
В управляемой моделями разработке [124] (УМР) и в программной фабрике [125] в частности наиболее интересной возможностью является генерация кода, скомпилировав который, можно сразу получить работающее приложение или его компоненты. Мы проектируем и сразу получаем нечто работающее, пусть даже на уровне прототипа. Уточняя модели, мы на каждом шаге имеем возможность видеть изменения в системе. Проектирование становится живым процессом без отрыва от разработки.
Историю управляемой моделями архитектуры и разработки, обобщаемой под термином УМИ – управляемой моделями инженерии [126] , можно вести с 1970-х годов. Именно тогда появились первые языки спецификации требований к программам и целые стандарты, типа упоминавшегося IDEF, включающего в себя ряд языков и нотаций. Однако реальная и доступная многим пользователям автоматизация моделирования появилась лишь вместе с персональными компьютерами. Не случайно форматы IDEF-диаграмм исторически сохранили рамки с ячейками информации, столь необходимыми при бумажной технологии анализа проектирования.
Первые инструменты CASE, выросшие из редакторов графических примитивов, были представлены в 1980-х годах, а одним из пионеров был небезызвестный Эдвард Йордон, соавтор, в компании с Томом ДеМарко, популярной методологии SADT [127] . В начале 1990-х годов наблюдалось возникновение множества языков, нотаций, подходов к анализу и проектированию и, как следствие, пик многочисленных CASE-инструментов, их реализующих.
У программистов «от сохи» отношение к CASE, как правило, негативное на уровне «я не верю, что какие-то картинки генерируют код лучше написанного руками». В таком отношении есть своя сермяжная правда, действительно, экскаватор, в отличие от мужика с лопатой, может вырыть не всякую яму. Доказывать обратное – бесполезная потеря времени.
У более продвинутых программистов, имевших опыт написания и сопровождения тысяч строк однотипного рутинного кода, претензии становятся обоснованными и касаются, как правило, следующих сторон применения CASE-средств:
• Если ручное написание кода принять за максимальную гибкость, то CASE может навязывать каркас, стиль кодирования и шаблоны генерации частей программ, ограничивающие не столько полёт фантазии программиста, сколько возможность тонкой настройки генерируемого кода. Неважно, что такая настройка не требуется в большинстве случаев, но если её нет, то менять придётся непосредственно сгенерированный код.
• CASE работает только в условиях дисциплины, когда ручные изменения генерируемого кода исключены или автоматизированы (пост-обработка). Как только программист залезает руками в код каркаса, модель оказывается рассинхронизированной по отношению к исходным текстам программ и процесс разваливается.
В качестве решения перечисленных проблем появились так называемые двусторонние CASE-инструменты (
Рис. 17. Двусторонний CASE-инструментарий ModelMaker имеет возможности работы как с моделями, так и непосредственно с кодом приложения
Нетрудно заметить, что двунаправленный подход в CASE-инструментарии в большей степени является мощным средством автоматизации отдельных программистов, так как обладает рядом ограничений:
• как правило, инструмент привязан к языкам и платформам;
• технология не выходит за рамки разработки конкретных программ и подсистем. То есть слои системы и архитектура остаются за рамками процесса;
• коллективная работа над моделями одновременно с кодом практически невозможна: приходится делить модели на независимые части, например подсистемы, разрабатываемые одним программистом;
• для достижения нужного эффекта методика по-прежнему требует навыков моделирования как минимум на уровне диаграммы классов. В противном случае CASE оказывается лишь очередным инструментов рефакторинга.Следующим шагом в развитии автоматизированных средств софтостроения явилась программная фабрика – синтез подходов управляемой моделями разработки и архитектуры, генерирующий не только отдельные компоненты системы, но целые слои в соответствии с выбранной архитектурой и платформами. На рынке уже имеется немало продуктов типа «
Лампа, полная джиннов
Метафора системы достаточно проста: хочешь генерировать код компонента или слоя – попроси об этом соответствующего «джинна» в форме стандартного «заклинания». Джинны, как им и положено, живут в лампе.
Переходя к техническим терминам, программист описывает задачу в терминах логической модели, представляющей собой набор сущностей, их атрибутов, операций и связей между ними. Язык создан на основе XML, поэтому делать описания можно непосредственно руками в обычном текстовом редакторе.
Рис. 19. Общая схема работы с «лампой» и «джиннами»
Модель в виде XML-файлов поступает на вход «заклинателю» – входящей в состав пакета консольной утилите. Производятся проверки непротиворечивости модели, выдающие ошибки либо предупреждения разной степени важности. Во время анализа модель также преобразуется во внутренний формат в виде множества объектов с открытыми интерфейсами доступа.
Если модель корректна, «заклинатель» начинает призывать «джиннов» сделать свою работу, передавая каждому на вход кроме самой модели ещё и разнообразные параметры, конфигурацию, касающуюся не только самих джиннов, но и, например, таких настроек, как правила именования в конкретном слое системы.
Обработав модель в соответствии с конфигурацией проекта, джинн выдаёт готовый к компиляции в среде разработки код. Для слоя хранения данных кроме генерации специфичных для СУБД SQL-скриптов производится их прогон на заданном сервере разработки.
В случаях, когда система уже существует и подлежит, например, переделке, можно восстановить модель из схемы базы данных. Конечно, даже теоретически такое восстановление не может быть полным из-за разницы в семантике, но большую часть рутинной работы оно выполняет. Проведя один раз импорт, далее мы редактируем, структурируем модели и продолжаем работать только в обычном цикле изменений «через модель».
На что похожа логическая модель? Приведу пример описания из рабочего проекта, содержащего один пользовательский тип, один перечисляемый тип, две сущности и одну связь (отношение) между ними.Пример модели в Genie Lamp
<Type name ="TEntityId" baseType ="int" />
<Enumeration name ="Granularity">
<Doc><Label lang ="ru">Грануляция учётного периода</Label></Doc>
<Item name ="Day" value ="0">
<Doc><Label lang ="ru">День</Label></Doc>
</Item>
<Item name ="Month" value ="1" default ="true">
<Doc><Label lang ="ru">Месяц</Label></Doc>
</Item>
<Item name ="Year" value ="2">
<Doc><Label lang ="ru">Год</Label></Doc>
</Item>
</Enumeration><Entity name ="FiscalYear">
<Doc><Label lang ="ru">Финансовый год</Label></Doc>
<Attribute name ="Id" type ="TEntityId" primaryid ="true" autoincrement ="true" />
<Attribute name ="Name" type ="TCaption" uniqueid ="true">
<Doc><Label lang ="ru">Обозначение года</Label></Doc>
</Attribute>
<Attribute name ="Granularity" type ="Granularity">
<Doc><Label lang ="ru">Грануляция периодов</Label></Doc>
</Attribute>
<Attribute name ="FromDate" type ="date">
<Doc><Label lang ="ru">Дата начала</Label></Doc>
</Attribute>
<Attribute name ="ToDate" type ="date">
<Doc><Label lang ="ru">Дата окончания</Label></Doc>
</Attribute>
<Attribute name ="Closed" type ="boolean" default ="false">
<Doc><Label lang ="ru">Год закрыт?</Label></Doc>
</Attribute>
<Attribute name ="GranularityName" type ="string" persisted ="false">
<Doc>
<Text lang ="ru">Возвращает локализованое название грануляции</Text>
</Doc>
</Attribute>
<Operation name ="CreatePeriods" access ="public">
<Doc>
<Text lang ="ru">Создает периоды финансового года
между датами начала и окончания
в соответствии с грануляцией. Например, для фин. года,
совпадающего с календарным, и помесячной грануляцией
будут созданы 12 месячных периодов
</Text>
</Doc>
<Returns type ="void"/>
</Operation>
<Operation name ="FindPeriodIdByDate" access ="public">
<Doc>
<Text lang ="ru">
Возвращает ID периода по заданной дате, "0" если не найден
</Text>
</Doc>
<Param name ="periodDate" type ="datetime"/>
<Returns type ="TEntityId"/>
</Operation>
<Operation name ="DeleteCascade" access ="public">
<Returns type ="void"/>
</Operation>
</Entity><Entity name ="Period">
<Doc><Label lang ="ru">Учётный период</Label></Doc>
<Attribute name ="Id" type ="TEntityId" primaryid ="true" autoincrement ="true" />
<UniqueId>
<Attribute name ="FiscalYearId" type ="TEntityId">
<Doc><Label lang ="ru">ID финансового года</Label></Doc>
</Attribute>
<Attribute name ="FromDate" type ="date">
<Doc><Label lang ="ru">Дата начала</Label></Doc>
</Attribute>
</UniqueId>
<UniqueId>
<OnAttribute name ="FiscalYearId"/>
<Attribute name ="PeriodNumber" type ="smallint">
<Doc><Label lang ="ru">Номер периода</Label></Doc>
</Attribute>
</UniqueId>
<Attribute name ="ToDate" type ="date">
<Doc><Label lang ="ru">Дата окончания</Label></Doc>
</Attribute>
</Entity><Relation entity ="Period" name ="FiscalYear"
entity2 ="FiscalYear" name2 ="Periods"
cardinality ="M:1">
<AttributeMatch attribute ="FiscalYearId" attribute2 ="Id" />
</Relation>Теперь необходимо задать конфигурацию в описании проекта. Предположим, что мы хотим создать 3-звенное приложение со следующими логическими слоями:
• слои хранения будут развёрнуты на SQL Server или Oracle;
• слой домена под управлением NHibernate;
• слой веб-служб на базе ServiceStack (вместо WCF, имеющего под Mono/Linux ограничения).Пример конфигурации проекта в Genie Lamp
<! – Включаем файл(ы) модели в проект – >
<ImportModel fileName ="MyModel.xml" />
<! – Будем использовать джинна SQL Server – >
<Genie name ="SqlServer"
type ="GenieLamp.Genies.SqlServer.SqlServerGenie"
assembly ="GenieLamp.Genies.SqlServer"
active ="false"
outDir ="%PROJECT_DIR%/../SQL/SqlServer-%TARGET_VERSION%"
outFileName ="%PROJECT_NAME%.sql"
updateDatabase ="true"
targetVersion ="2008">
<Param name ="Database.Create" value ="false" />
… Другие параметры "заклинания"
</Genie><! – Будем использовать джинна Oracle – >
<Genie name ="OracleDb"
type ="GenieLamp.Genies.Oracle.OracleGenie"
assembly ="GenieLamp.Genies.Oracle"
active ="true"
outDir ="%PROJECT_DIR%/../SQL/Oracle-%TARGET_VERSION%"
outFileName ="%PROJECT_NAME%.sql"
outFileEncoding ="iso-8859-1"
updateDatabase ="false"
targetVersion ="10g">
<Param name ="UniqueIndexConstraint" value ="true" />
…
</Genie><! – Будем использовать джинна NHibernate для генерации домена – >
<Genie name ="NHibernate"
type ="GenieLamp.Genies.NHibernate.NHibernateGenie"
assembly ="GenieLamp.Genies.NHibernate"
active ="true"
outDir ="%PROJECT_DIR%/../Domain"
outFileName ="%PROJECT_NAME%.Domain.cs"
targetVersion ="*">
<Param name ="TargetAssemblyName" value ="Company.Business.%PROJECT_NAME%.
Domain" />
</Genie><! – Будем использовать первого джинна ServiceStack
для генерации интерфейсов к веб-службам – >
<Genie name ="ServiceStack Services Interfaces"
type ="GenieLamp.Genies.ServicesLayer.ServiceStack.ServicesInterfacesGenie"
assembly ="GenieLamp.Genies.ServicesLayer"
active ="true"
outDir ="%PROJECT_DIR%/../Services.Interfaces"
targetVersion
="*">
</Genie><! – Будем использовать второго джинна ServiceStack
для генерации собственно веб-служб – >
<Genie name ="ServiceStack Services"
type ="GenieLamp.Genies.ServicesLayer.ServiceStack.ServicesGenie"
assembly ="GenieLamp.Genies.ServicesLayer"
active ="true"
outDir ="%PROJECT_DIR%/../Services"
targetVersion ="*">
</Genie><Configuration>
<! – Конфигурация слоя хранения данных – >
<Layer name ="Persistence">
<NamingConvention style ="uppercase" maxLength ="30">
<Param name ="PrimaryKey.ColumnTemplate" value ="NI%TABLE%" />
<Param name ="PrimaryKey.ConstraintTemplate" value ="PK_%TABLE%" />
… Другие шаблоны именований
</NamingConvention>
<Param name ="ForeignKey.CreateIndex" value ="true" />
<Param name ="BooleanValues" value ="YesNo"/>
</Layer><! – Конфигурация слоя домена – >
<Layer name ="Domain">
<Param name ="BaseNamespace" value ="Company.Business.%PROJECT_NAME%" />
</Layer>
<! – Конфигурация слоя служб – >
<Layer name ="Services">
<Param name ="BaseNamespace" value ="Company.Business.%PROJECT_NAME%" />
</Layer><! – Шаблон "Реестр объектов" – >
<Pattern name ="Registry">
<Param name ="Schema" value ="Core" />
<Param name ="PersistentSchema" value ="CORE" />
<Param name ="RegistryEntity.Name" value ="EntityRegistry" />
<Param name ="TypesEntity.Name" value ="EntityType" />
<Param name ="TypesEntity.PrimaryId.Type" value ="smallint" />
<Param name ="PrimaryId.Type" value ="bigint" />
</Pattern><! – Шаблон "Версия состояния" для хранимых объектов – >
<Pattern name ="StateVersion">
<Param name ="Attribute.Name" value ="Version" />
<Param name ="Attribute.Type" value ="int" />
</Pattern><! – Шаблон "Аудит" для минимального отслеживания изменений – > <Pattern name ="Audit" />
<! – Шаблон "Локализация" – > <Pattern name ="Localization" />
<! – Шаблон "Безопасность" для веб-служб – >
<Pattern name ="Security" />
</Configuration>В описании конфигурации джиннов видно, что его основу составляет сборка, один из классов которой, реализующий интерфейс IGenie, является точкой входа. Каждый джинн имеет как общие для всех параметры, например каталог для выходных файлов, так и специфичные, передаваемые через тег Param, описываемые в документации.
За джиннами следуют конфигурации слоёв. Если для домена и служб можно пока ограничиться спецификацией базового пространства имён, то для слоя хранения, особенно при поддержке более чем одной СУБД, необходимо указать дополнительные ограничения вроде максимальной длины имён.
Заключительная часть конфигурации представляет собой описания шаблонов. Но не тех, о которых идёт речь в книжке «банды четырёх», а о шаблонах реализации типовых задач уровня ядра и системных служб:
• Например, шаблон «Реестр объектов» добавляет к системе возможность ведения централизованного реестра всех создаваемых объектов. Реализован он как соответствующий класс и таблица, ссылка на которые добавляется ко всем другим классам (некоторые классы можно исключить через параметры шаблона).
• Шаблон «Версия состояния» является встроенной в NHibernate возможностью отслеживания конфликтов в многопользовательской среде. Например, если два пользователя изменяют один и тот же объект, то последний из них, сохранивший объект, получит исключение, оповещающее о том, что данные были изменены со времени последнего редактирования. Шаблон реализуется добавлением соответствующего атрибута номера версии ко всем классам.
• Шаблон «Аудит» в простейшем варианте является регистрацией для каждого хранимого объекта информации о времени его создания, последнем редактировании и авторе.
• Шаблон «Локализация» добавляет в генерируемый код возможность перевода сообщений в рамках технологии GNU gettext.
• Наконец, шаблон «Безопасность» в простейшем варианте ограничивает доступ к веб-службам через механизм аутентификации, логику которой необходимо реализовать в переопределяемом методе соответствующего класса. Например, обратиться к стороннему LDAP или непосредственно к базе данных с регистрационной информацией для проверки имени пользователя и хеша пароля.
Теперь, если запустить «заклинатель» с параметром файла конфигурации проекта и не будет обнаружено ошибок, на выходе мы получим инициализированные структуры баз данных и готовые к компиляции файлы. Рассмотрим их чуть подробнее.
Слой хранения (СУБД)
Джинны SQL Server и Oracle создадут нам в указанном каталоге подкаталоги, соответствующие целевой СУБД и её версии. В каждом подкаталоге находятся три SQL-скрипта, предназначенные, соответственно, для создания, обновления или удаления схемы БД.
Если посмотреть на созданные в СУБД структуры, то мы увидим, что из одной и той же модели логического уровня были созданы две реализации, различающиеся на физическом уровне. Например, используемые типы данных различаются. С другой стороны, необходимость поддержки слоем домена сразу двух СУБД приводит к тому, что вместо оптимального, но специфичного для SQL Server типа bit для поддержки булевых величин используется принятый в среде Oracle символьный тип.
Рис. 20. Результат работы джинна SQL Server
Рис. 21. Результат работы джинна Oracle
Слой домена (NHibernate)
Джинн NHibernate генерирует три C#-файла и один XML-файл проекции (маппинга) классов на структуры хранения.
Все классы домена являются расширяемыми (
Перечисляемый тип создаётся вместе с классами, позволяющими локализовать пользовательские названия его элементов, описанные в модели. По умолчанию будет использован язык модели, перевод необходимо осуществлять в технологии
Рис. 22. Перечисляемый тип слоя домена и его локализация
Для определяемых моделью сущностей генерируются классы, содержащие кроме соответствующих объявленным атрибутам свойств ещё и группу служебных методов для управления объектами, например, для сохранения или выборки по запросу на HQL или даже SQL. Такие методы часто используют служебные классы, объявленные в DomainSupport.cs.
Для объявленных в модели операций генерируется соответствующий метод интерфейса. Реализовать их нужно программисту в рамках расширения
Отображение классов на структуры РСУБД соответствует стратегии «одна таблица на подкласс без дублирования атрибутов предка», достаточно хорошей в большинстве случаев, но и она может быть изменена.
Фрагмент файла проекции для класса «Финансовый год»
<!-Engine.FiscalYear->
<class name ="Domain.Engine.FiscalYear" table ="FISCAL_YEAR" schema ="MYAPP">
<id name ="Id" access ="property" column ="NIFISCAL_YEAR">
<generator class ="native">
<param name ="sequence">MYAPP.SEQ_FISCAL_YEAR</param>
</generator>
</id>
<version name ="Version" column ="VERSION" type ="int" access ="property" />
<set name ="Periods" table ="MYAPP.PERIOD" inverse ="true" lazy ="true" cascade ="none">
<key column ="NI_FISCAL_YEAR" />
<one-to-many class ="Domain.Engine.Period" />
</set>
<many-to-one name ="EntityRegistry" class ="Domain.Core.EntityRegistry" unique ="true">
<column name ="NI_ENTITY_REGISTRY" not-null ="false" />
</many-to-one>
<property name ="Name" access ="property">
<column name ="NAME" not-null ="false" />
</property>
<property name ="Granularity" access ="property">
<column name ="GRANULARITY" not-null ="true" />
</property>
<property name ="FromDate" access ="property">
<column name ="FROM_DATE" not-null ="true" />
</property>
<property name ="ToDate" access ="property">
<column name ="TO_DATE" not-null ="true" />
</property>
<property name ="Closed" access ="property" type ="YesNo">
<column name ="CLOSED" not-null ="true" />
</property>
<property name ="CreatedBy" access ="property">
<column name ="CREATED_BY" not-null ="false" />
</property>
<property name ="CreatedDate" access ="property" type ="timestamp">
<column name ="CREATED_DATE" not-null ="false" />
</property>
<property name ="LastModifiedBy" access ="property">
<column name ="LAST_MODIFIED_BY" not-null ="false" />
</property>
<property name ="LastModifiedDate" access ="property" type ="timestamp">
<column name ="LAST_MODIFIED_DATE" not-null ="false" />
</property>
</class>
Слой веб-служб и интерфейсов доступа (ServiceStack)
Генерируемые для слоя веб-служб C#-файлы предназначены для создания двух сборок: собственно служб и интерфейсов к ним, используемых клиентами.
Рис. 25. Классы, реализующие службы доступа к объектам домена
Рис. 26. Класс службы сохранения объектов
Интерфейсы доступа к службам также содержат описания перечислимых типов с локализацией, классы DTO для передачи состояния между программой-клиентом и доменом, классы для непосредственного доступа к вызовам служб.
Рис. 27. Перечисляемый тип слоя веб-служб
Рис. 28. Классы вызова специфицированных методов
Рис. 29. Классы вызова веб-служб, касающихся «финансового года»
Рис. 30. Класс адаптера для работы с объектом «Финансовый год»
Рис. 31. Класс адаптера для работы с коллекцией объектов «Финансовый год»
Работать с DTO и коллекциями не слишком комфортно, проявляется много ненужных деталей. Но если обернуть операции с DTO адаптерами, то код становится гораздо более читаемым и коротким.Пример работы с DTO
CurrencyDTO curr1 = new CurrencyDTO();
curr1.Code = "RUR";
curr1.Name = "Currency 1";
UnitOfWorkDTO uow = new UnitOfWorkDTO();
uow.Save(curr1);PersistenceRequest prq1 = new PersistenceRequest();
prq1.UnitOfWork = uow;
PersistenceResponse prr1 = client.Post<PersistenceResponse>("/Persistence", prq1);
Assert.IsFalse(prr1.CommitResult.HasError, prr1.CommitResult.Message);Пример работы с адаптерами
Currency curr1 = new Currency();
curr1.Code = "RUR";
curr1.Name = "Currency 1";
CommitResult cr1 = curr1.Save();
Assert.IsFalse(cr1.HasError, cr1.Message);
Программа-клиент
В рамках простейшего WinForms-приложения создадим форму, содержащую сетки отображения финансовых годов и их периодов. Не вдаваясь в технику разработки приложений этого типа, просто приведу фрагменты кода, запрашивающие у служб коллекции соответствующих типов.Извлечение списка финансовых годов, отфильтрованного по названию
FiscalYearCollection years = FiscalYearCollection.GetByQuery(
"from FiscalYear where Name like: name order by Name",
new ServicesQueryParams()
AddParam("name", txtYearName.Text)
);
dgvYears.DataSource = years;Извлечение списка учётных периодов заданного года
PeriodCollection periods = PeriodCollection.GetByQuery(
"from Period where FiscalYear.Id =:yearId order by FromDate",
new ServicesQueryParams()
AddParam("yearId", CurrentYear.Id),
0, 1000);
dgvPeriods.DataSource = periods;Запускаем клиентское приложение, предварительно запустив сервер веб-служб, и видим на экране примерно такую картинку, как на рис. 32.
Остановиться и оглянуться
Рассмотренная выше подсистема состоит из минимального набора слоёв трёхзвенной архитектуры на основе веб-служб. Тем не менее даже в таком минимальном варианте обилие деталей, промежуточных и служебных классов, проекций и преобразований должно дать представление о проблеме сложности современного состояния софтостроения.
Одним из способов обхода этой проблемы является описанная технология программной фабрики, несомненно далёкая от совершенства и ограниченная в наборе целевых платформ.
Какова же эффективность?
Если рассмотреть метрики относительно небольшого проекта, то 40 прикладных сущностей в модели, состоящей примерно из 600 строк XML-описаний, порождают:
• около 3 тысяч строк SQL-скриптов для каждой из целевых СУБД;
• порядка 10 тысяч строк домена;
• 1200 строк XML для проекций классов на реляционные структуры (таблицы);
• около 17 тысяч строк веб-служб и интерфейсов.Таким образом, соотношение числа строк мета-кода описания модели к коду его реализации на конкретных архитектурах и платформах составляет около 600 к 30 тысячам или 1 к 50 .
Это означает, что оснащённый средствами автоматизации программист с навыками моделирования на этапе разработки рутинного и специфичного для платформ/архитектур кода производителен примерно так же, как и его 50 коллег, не владеющих технологией генерации кода по моделям. Любое внесение изменений в модель тут же приводит в соответствие все генерируемые слои системы, что ещё более увеличивает разрыв по сравнению с ручными модификациями. Наконец, для генерируемого кода не нужны тесты. Производительность возрастает ещё как минимум вдвое.
Даже если принять во внимание, что доля рутинного и прочего инфраструктурного кода по отношению к прикладному, то есть решающему собственно задачи конечных пользователей, снижается с масштабом системы, есть о чём поразмыслить в спокойной обстановке.Cherchez le bug, или Программирование по-французски
Этот рассказ я написал более 10 лет назад, летом 2001 года, в поездках на пригородном поезде между Парижем и Moulin-Galant, где размещался филиал IBM, и поначалу сомневался в необходимости его включения в книгу. Но, просмотрев старый текст, с некоторым удивлением я обнаружил, что если заменить аббревиатуры в названиях технологий на «новые и прогрессивные», то суть повествования останется прежней. Изменится ли что-нибудь ещё через 10 лет, покажет время.
Хаос наступает внезапно
Что такое «баг» знают, наверное, все программисты. Кто не знает, поясню: «баг» (от англ. bug – жучок) – это «жучок-вредитель» в программе, то есть ошибка, аномалия, сбой. По-французски это интернациональное словечко произносится примерно как «бёг», но буква «ё» произносится ближе к звуку «о». Особенностью французской, как, собственно, и любой другой программной индустрии – громкое слово, несколько облагораживающее нашу всемирную «багодельню», является одно из национальных состояний французской души, характеризующее этакого сангвиника после распитого с друзьями бокала вина, галантного и совсем не торопящегося жить. Разумеется, сангвиник считает, что все продукты его труда велики и прекрасны, как вид на ночной Париж с Монмартрского холма или пыльный гобелен над кроватью Наполеона в замке Фонтенбло.
Первый опыт пришёл ко мне уже через пару недель после начала работы на новом месте. Не считая себя экспертом по программированию на С++, увидев исходные тексты, я ощутил мягко говоря, некоторое беспокойство. Столкнулся я с этими текстами не сразу, так, прежде в конторе трудились двое русских программистов, один из которых по разным причинам покинул фирму, а с другим, Димой, мы работали немногим более года вплоть до её ликвидации.
Итак, некоторое время я находился в счастливом неведении относительно общего состояния дел. Прежде всего пришлось оценить качество кода. То, что единые соглашения отсутствовали, а Java-подобный стиль не очень хорош в программах на С++ не являлось большим недостатком – лучше такой стиль, чем никакой, тем более что кода на Java в системе было много. Прежние авторы, видимо, не смогли выразить себя в объектно-ориентированным подходе, а до структурного программирования и функциональной декомпозиции не опустились. Если первое не сразу бросалось в глаза наличием достаточного числа классов с пустыми конструкторами, то второе резало глаз обилием возвратов из разных мест одной функции и очень похожими кусками кода в одноимённых перегруженных функциях, написанных методом копирования текста через буфер обмена, «copy-paste». При виде некоторых фрагментов кроме чисто русских эмоциональных выражений из меня периодически вылезало французское «что за бордель», вызвавшее похвалу моего коллеги, отмечавшего, что я делаю успехи в освоении языка.
В жизни каждого мало-мальски сложного программного продукта есть стадия, когда система проходит некий порог увеличения сложности, за которым наступает состояние, которое я называю «самостоятельной жизнью». Это ещё далеко не полный хаос, но уже давно и далеко не порядок. Все попытки как-то организовать процесс разработки программ, всяческие методологии, применение парадигмы конвейера, стандарты и административные меры худо-бедно, но помогают оттянуть этот критический порог на некоторое время. В идеале – до того момента, когда развитие системы останавливается и она, побыв некоторое время в стабильном состоянии, потихоньку умирает. Одна из проблем организации промышленного производства программного обеспечения состоит в отсутствии каких-либо формальных описаний деятельности программиста. Можно определить в технологической карте, как работает сварщик или каменщик, но как пишет программу программист зачастую не знает и он сам. До художника, конечно, далеко не все дотягивают, а вот с деятельностью рядового журналиста «независимой» газеты непосвящённому в софтостроение человеку сравнивать вполне можно. Этакий ядрёный сплав ремесла, некого богемного искусства, со вкраплениями науки, вперемешку с халтурой, шабашкой и постоянным авралом. Попытки же принудить программиста делать однотипные операции противоречат самой цели существования программного обеспечения как самого гибкого из существующих средств автоматизации рутинных процессов и потому изначально обречены на неудачу.
Вернемся к нашим «жучкам». Система, с которой я начал работать, уже успешно прошла свой порог несколько месяцев назад и жила полнокровной, отдельной от авторов жизнью. Определить, что критический порог пройден, несложно, для этого у меня есть один простой признак: «Ты изменил чуть-чуть программу в одном месте, но вдруг появилась ошибка в другом, причём даже автор этого самого места не может сразу понять, в чем же дело» [128] . Существует и второй признак, не менее практичный: «Смотришь на чужой текст программы, и тебе не вполне понятен смысл одной его половины, а вторую половину тебе хочется тут же полностью переписать».
Поскольку я определил, что налицо сразу два признака, то, кроме как включиться в борьбу за продление жизни системы, уже ничего не оставалось.
При самых удачных раскладах такая борьба может длиться годами. Например, в таком богатом и « баг атом» учреждении, как Microsoft, несколько лет боролись за жизнь Windows 95, выпустили даже Windows 98, но в результате все-таки сил осталось только на Windows 2000. Для неспециалистов это может быть неочевидно, поэтому поверьте мне на слово, что эти системы совершенно разные, как дельфин и русалка. Второй способ: прекратить текущую разработку программы и начать новую, используя опыт предыдущего прототипа. На это кроме обычного мужества признания ошибок и риска полететь с должности начальника требуется ещё и немало денег. Третий способ – выдать желаемое за действительное, побольше маркетинга, шуму, «подцепить» несколько заказчиков и на их деньги попытаться всё-таки перейти ко второму или первому способу. Такой трюк может сработать, если заказчику честно предлагают за какие-то вкусные для него коврижки побыть немного «подопытным кроликом», на котором система вскоре «должна заработать».
Кстати, если программист говорит вам, что в данном месте программа «должна работать», это значит, что с очень большой вероятностью она не работает, а он её просто не проверял в этом месте. Если же программист уже после обнаружения ошибки говорит: «А у меня она в этом месте работает…», лучше сразу его уволить, чтобы не мучился. Это проза жизни, также относящаяся к коллекции моих практик.
Контора пошла по третьему пути с прицелом на последующий переход к первому. Одно из самых скромных маркетинговых утверждений на рекламном проспекте гласило: «Наша система работает под Windows и под UNIX, она может взаимодействовать с любыми базами данных». Далее шёл список названий СУБД из первой десятки, в одном названии была ошибка. Реально же, уже к моменту, когда порог сложности был перейдён, имелась только версия программных модулей с постоянным номером, «текущая» под Windows NT и отстающая от нее на пару недель под Linux Mandrake, а из баз данных использовался Microsoft Access. Заказчики ходили косяками, поэтому шанс найти авантюриста увеличивался от недели к неделе. Но он всё не находился. И только благодаря тому, что у пары конкурирующих контор дела обстояли ещё хуже (это было для меня просто потрясающей новостью!), некая достаточно крупная фирма после небольшого конкурса решилась на опытное внедрение. Её примеру последовала и вторая. Тут-то и начались настоящие приключения.
Что-то с памятью моей стало
Отступив от основной темы, опишу в общих чертах структуру конторы, в которой мы работали. Основателей у фирмы двое: генеральная директриса Софи – экспрессивная француженка, незамужняя, уже в летах, и технический директор Блез – почти наш ровесник, но, по-видимому, реально взявшийся за свой первый проект. Тут ведь всё неспешно происходит, студенты учатся, а не работают, поэтому такая ситуация для специалиста к тридцати годам нормальна. Софи заведует общими вопросами и руководит подразделением маркетинга, то есть создаёт полезный фон или, наоборот, шумовые помехи в зависимости от ситуации и квалификации представителей заказчика. Блез, соответственно, должен заниматься созданием продукта. Изначально продукт делался силами нескольких человек в течение полугода, включая самого техдиректора, после чего кое-кто ушёл, сам Блез понял, что программировать он уже не успевает, а некоторые оставшиеся просто откровенно не умеют. В результате появились двое русских программистов, на которых легла вся системная разработка и поддержка последнего уровня. Из тех, кто программировать не умеет, была составлена группа прикладных программистов и некое подобие группы поддержки всего процесса в виде администрирования репозитория исходного кода, создания резервных копий и дистрибутивов. В задачу прикладного программиста должно было входить быстрое создание веб-приложения для заказчика на основе собственно продукта. Итого набралось человек 15, включая директоров и весёлый шумный маркетинг.
Центральным звеном системы является модуль с типичным названием «ядро» (
Заказчики, конечно, и не подозревали о том, что месяцев десять назад ядро состояло из кучки «ядрышек», которые просто слили в один кусок, когда на первых запусках время ожидания ответа не устроило даже самих авторов. Однако после результатов такого слияния бдительность была потеряна надолго, хотя ничто не мешало параллельно с разработкой делать хотя бы простые тесты и прогоны. В результате после первых дней работы системы в условиях опытной эксплуатации наш технический директор стал ещё более нервным, чем раньше, особенно когда Дима или я заводили разговор о том, что «вообще-то это все не будет работать» и надо не перекраивать испорченный костюмчик, а шить новый, хотя бы по старым выкройкам.
Нервность Блеза раньше выражалась в том, что он просто частенько прохаживался по нашей комнате и раз в неделю, вдохновившись беседами с потенциальными клиентами, выдавал очередную гениальную идею о развитии продукта, иногда противоречащую идее предыдущей недели, но всегда очень расплывчатую и вызывающую необходимость нам с Димой садиться и писать некое подобие спецификации. Созданная спецификация Блезом практически никогда не менялась и периодически вовсе игнорировалась, поскольку это означало необходимость отвечать за свой же «базар» в уже конкретном формальном виде, но нам она была нужна для самоконтроля и взаимодействия, к тому же ее мы выдавали ещё и как заменитель инструкции для прикладного программиста.
Усиление нервозности выражалось в хождении по комнате, причём с гораздо большей скоростью, теперь уже практически всё время в перерывах между поездками к потенциальным заказчикам и встречами с ними в офисе. К тому же теперь хождение часто сопровождалось разговорами по телефону. Блез брал аппарат в правую руку, трубку в левую и объяснял заказчику недокументированные тонкости души продукта, попутно периодически задевая шнур и роняя телефон. На самом деле я очень не люблю, когда в комнате, где люди работают большее время дня в сидячем положении и относительной тишине, начинает кто-то мельтешить перед глазами и шуметь. Думаю, многие будут со мной солидарны. Отвлекает.
После нескольких дней падений блезовского телефона выяснилось, что ядро с немереным аппетитом кушает оперативную память сервера и никак не хочет отдавать ее обратно даже после отключения всех пользователей. Утечки памяти составили около одного гигабайта (!) в сутки. Мы принялись за тесты, которые по-хорошему надо было бы начинать ещё первым авторам месяцев десять назад, и на напоминания о необходимости которых уже с нашей стороны ответов не следовало. Тесты на небольшом макете показывали примерно ту же картину, что и у заказчика, оставалось со спокойным сердцем сесть и разобраться в причинах. Однако времени, как выяснилось, уже не было. Прибежавшая в восемь часов вечера Софи застала нас с Димой уже практически в дверях и сообщила, что все плохо, надо срочно что-то делать, иначе контора может просто накрыться.
В тот день мы просидели до трех часов ночи, на следующий день ещё до часу, пришлось выйти и в воскресенье. Таким образом, три дня к отпуску я заработал, сам того не желая. Но результаты были не очень хорошими: удалось только ликвидировать утечки памяти, но остались проблемы взаимных блокировок, которые появлялись изредка и в совершенно разных ситуациях. Если вспомнить, на что похожи исходные тексты программ, то поиск таких ситуаций был на порядок сложнее поиска иголки в стоге сена.
Три дня в IBM
У второго заказчика был сервер от всемирно известной фирмы IBM, под управлением операционной системы AIX–IBM-овского варианта UNIX. Поскольку нашим маркетингом заявлялось, что система будет работать чуть ли не везде, то по русской поговорке «назвался груздем…» следовало лезть в этот самый кузов. Блез договорился с филиалом IBM о том, что в их демонстрационно-учебном зале несколько дней поработают программисты. Дима, как единственный из нас двоих знакомый с Linux, ответственный за компиляцию и сведение версии ядра под него, был настроен оптимистично и поехал собирать систему под AIX. Я остался в офисе делать сервис под Windows NT, который управлял бы запуском нашей системы в рабочем режиме.
Однако прошла неделя, а от Димы приходили только редкие послания с описаниями новых проблем. Например, компилятор C++ от IBM не переваривал некоторые конструкции, которые заявлялись как стандартные, но не оказывались таковыми на практике. Поскольку система состоит из многих собственных компонентов на C++ и Java, а также нескольких библиотек сторонних разработчиков, то проблемы нарастали как снежный ком, причём всплыла и старая, казалось бы, ликвидированная беда с утечкой памяти, появились уже стабильные признаки блокировки процесса. В итоге Блез решил отправить меня на подмогу Диме, поскольку мы периодически с ним весьма эффективно практиковали парное программирование – один из полезных методов для работы в экстремальных условиях.
Филиал IBM находился в 25 километрах к юго-востоку от Парижа, недалеко от железнодорожной станции с названием Moulin-Galant. Дорога туда не ближняя и занимала около полутора часов от дома до входа в офис, располагавшийся в помещениях завода по производству полупроводников. Я мысленно пожалел бедного коллегу, который уже вторую неделю каждый день вставал в 7 утра и приезжал домой в 10 часов вечера. От станции надо пройти пешком 15–20 минут, но, когда мы шли, то кроме ещё одного человека ни одной живой души на расстоянии нескольких километров не было, только проезжали по узкому шоссе машины. Вдоль дороги тесными рядами стояли домики и малоэтажные постройки – типичный европейский пейзаж. За все три дня ни одного человека во внутренних садиках этих домиков я так и не увидел.
Первый день мы начали с экспериментов. Сделали тестовое приложение, которое работало со сторонней библиотекой, и через пару часов убедились, что память расходуется именно в ней. Заодно, даже не помню точно, как, выяснилось, что устойчивая ошибка, приводящая к краху ядра, была обусловлена настройками памяти на уровне операционной системы. Консультант, дядька лет пятидесяти, исправил нам эти параметры и пожелал дальнейших успехов. Кстати, у них там вся команда консультантов и менеджеров состояла из мужчин предпенсионного возраста – факт отрадный, если вспомнить откровенную и циничную дискриминацию по возрасту при приёме на работу во многих российских фирмах.
Ещё несколько часов ушло на настройку альтернативного компилятора GNU С++ и – о, чудо! – получение нормального результата без всяких утечек для того же теста. К сожалению, прежний компилятор от IBM этим происшествием нас разочаровал полностью, это была капля, переполнившая чашу, ставшая причиной исключения виновника из арсенала. Но радоваться было рано. Другая используемая библиотека просто не имела официальной версии под AIX для нашего альтернативного компилятора, в наличии была только версия, сделанная «репрессированным» нами IBM-овским транслятором. Исходные тексты библиотеки также не содержали файлов инструкций (
В итоге мы задержались с этим процессом до половины девятого вечера, поэтому, придя на станцию, обнаружили неприятный сюрприз: станция фактически закрылась, а поезда кончились ещё час назад. Собственно говоря, станция представляла собой домишко и две длинные платформы. Телемониторы, на которых обычно показывают четыре-пять ближайших поездов, были выключены, а домик закрыт на замок, и металлические жалюзи опущены. Поскольку мы не сразу обнаружили отсутствие поездов, то сначала купили билеты. Автомат по продаже был один, и он сильно напоминал старый аппарат по продаже топлива для «пепелацев» из культового кинофильма «Кин-дза-дза». Набрав номер станции назначения, я услышал скрип и с содроганием сунул в кривую прорезь кредитную карточку. После минутной паузы автомат издал визгливый звук – это модем пытался соединиться и отправить сведения об оплате в свой центр. Ещё секунд через 30 из самого нижнего окошечка выпал жёлтый билет, который я засунул в другую неровную прорезь для компостирования. Наконец, выждав ещё секунд 20, зловеще поскрежетав иголками принтера, автомат выплюнул отмеченный билетик все из того же нижнего окошечка. Я слегка присел перед ним, расставив руки, и сказал «Ку!».
То, что народу на станции было полтора человека, нас не насторожило, это обычное дело. Но вот погасшие мониторы были плохим признаком. Наконец, после поисков мы увидели на стенке домика болтающийся листок, оказавшийся ксерокопией расписания и поведавший нам горькую правду. Не теряя времени, мы поднялись к шоссе в надежде сесть на автобус и доехать до узловой станции, где поезда ещё ходили. Но и там нас ждало разочарование: последний из них ушёл ещё в половине восьмого вечера. Смеркалось, вокруг практически ни души. Дима решил использовать преимущества мобильной связи и вызвать такси. Однако диспетчер после долгого выяснения, где же мы все-таки находимся, сказала, что этот район не обслуживается, после чего связь прервалась. Засунув телефон обратно в карман, Дима предложил идти до узловой станции пешком: хотя карты у нас не было, но он вроде как помнит примерное направление. По нашим расчётам станция находилась в трех-четырёх километрах, поэтому мы двинули в путь.
Через сорок минут быстрой ходьбы по практически безлюдным улицам мы вышли к цели и, купив по «Сникерсу» в другом автомате, сели на отходящий в 22:08 полупустой поезд на Париж. Я попытался воспользоваться Диминым телефоном и позвонить домой, дабы предупредить жену о вынужденной задержке вследствие наших мытарств, но голос оператора сообщил, что денежки практически закончились и сделать вызов нельзя. Дима решил сразу же пополнить счёт по карте, однако и здесь нас ждало разочарование: сервер по обслуживанию платежей был «на обслуживании», или, попросту говоря, не работал. До дому я добрался к полуночи, но мы договорились, что приедем завтра на час позже, чтобы немного отдохнуть.
Назавтра, в поезде, Дима опять пытался зарядить мобильный телефон порцией денег с «Визы», но сервер все ещё не воскрес, и телефон мог только принимать периодические звонки Блеза, который внезапно обнаружил серьёзную проблему в одном из модулей сопряжения с администраторским интерфейсом. По приезде в офис нас ждал новый сюрприз: вчерашний день был по плану последним, тогда как начальство забыло предупредить IBM-овцев о том, что мы ещё поработаем немного. Поэтому мы застали все временные каталоги на нашей машине чистыми от файлов после регулярной утренней процедуры «уборки мусора за клиентами». Но это не было большой бедой, через час Блез примчался на своем мотоцикле с кассетой, и мы благополучно восстановили файлы недельной давности, а ещё за пару часов повторили с ними все вчерашние манипуляции. Но модули в новой редакции все так же не работали, как и вчера. Ну что же, опять чтение документации в Интернете, шаманство с опциями компиляции, сравнение работающего примера с неработающим модулем… Наши неутомимые поиски прервал менеджер, который попросил нас в целях соблюдения режима безопасности (интересно, а вчера что, день особый был?) закончить работу в шесть-тридцать вечера. Мы двинулись домой, что называется, несолоно хлебавши, но зато нас провели коротким путём через помещения завода. Я обратил внимание на многочисленные большие залы с IBM-овскими вычислительными машинами, представлявшими собой металлические шкафы размером с платяной. Рядом высились стойки с магнитными лентами. Я даже ощутил подобие ностальгии по безвременно разрушенным и разворованным на цветные металлы большим машинам у нас в СССР. А тут они ещё работали, и никто пока их выбрасывать не спешил.