Аннотация

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

Дэвид Л. Парнас
Дэвид Л. Парнас

Введение

Ясное изложение философии модульного программирования можно найти в учебнике 1970 года по проектированию системных программ за авторством Готье и Понта [1, ¶ 10.23], цитату из которого мы приводим ниже: 

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

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

Краткий обзор текущего положения дел

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

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

Преимущества, которые ожидают от модульного программирования: (1) организационны — срок разработки сокращается, поскольку разные группы могут работать над своими модулями, практически не взаимодействуя друг с другом; (2) гибкость продукта — появляется возможность вносить существенные изменения в один модуль без необходимости менять другие; (3) понятность — систему можно изучать постепенно, по одному модулю за раз. В результате вся система в целом проектируется лучше, поскольку становится более понятной.

Что такое модуляризация?

Ниже приведены несколько частичных описаний систем, называемых модуляризациями. В данном контексте «модуль» рассматривается скорее как единица распределения ответственности, нежели как подпрограмма. Каждая модуляризация включает проектные решения, которые должны быть приняты до того, как может начаться работа над отдельными модулями. Для каждой альтернативы приведены совершенно разные решения; однако во всех случаях цель заключается в том, чтобы описать все решения «системного уровня» (то есть решения, затрагивающие более одного модуля).

Пример системы 1: Система формирования KWIC-индекса

Приведенного ниже описания системы KWIC-индекса будет достаточно для целей данной статьи. Система принимает упорядоченный набор строк, где каждая строка представляет собой упорядоченный набор слов, а каждое слово — упорядоченный набор символов. Любая строка может быть циклически сдвинута путем последовательного удаления первого слова и добавления его в конец строки. Система выводит алфавитный список всех циклических сдвигов всех строк.

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

Модуляризация 1

Мы видим следующие модули:

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

Модуль 2: Циклический сдвиг. Данный модуль вызывается после того, как модуль ввода завершил свою работу. Он формирует индекс, который содержит адрес первого символа каждого циклического сдвига, а также исходный индекс строки в массиве, созданном модулем 1. Результат своей работы модуль оставляет в ядре в виде пар слов (исходный номер строки, начальный адрес).

Модуль 3: Алфавитная сортировка. Данный модуль принимает в качестве входных данных массивы, созданные модулями 1 и 2. Он формирует массив в том же формате, что и модуль 2. Однако в данном случае циклические сдвиги перечислены в ином порядке — алфавитном.

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

Модуль 5: Главное управление. Данный модуль в основном ограничивается управлением последовательностью выполнения остальных четырех модулей. Он также может обрабатывать сообщения об ошибках, распределение памяти и т.д.

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

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

Модуляризация 2

Мы видим следующие модули:

Модуль 1: Хранилище строк. Данный модуль состоит из набора функций или подпрограмм, предоставляющих средства для обращения к нему со стороны пользователя этого модуля. Функция CHAR(r,w,c) будет возвращать целое число, представляющее c-й символ в w-м слове r-й строки. Вызов вида SETCHAR(r,w,c,d) приведет к тому, что c-й символ в w-м слове r-й строки станет символом, представленным значением d (то есть CHAR(r,w,c) = d). Функция WORDS(r) возвращает количество слов в строке r. Существуют определенные ограничения на то, как могут вызываться эти программы; в случае нарушения этих ограничений программы передают управление подпрограмме обработки ошибок, которую должны предоставить пользователи программы. Дополнительно доступны программы, которые сообщают вызывающей стороне количество слов в любой строке, текущее количество хранимых строк и количество символов в любом слове. Предусмотрены функции DELINE и DELWRD для удаления частей уже сохраненных строк. Точная спецификация подобного модуля приведена в [3] и [8], и мы не будем повторять ее здесь.

Модуль 2: ВВОД. Данный модуль читает исходные строки данных с входного устройства и обращается к хранилищу строк для их сохранения.

Модуль 3: Генератор циклических сдвигов.  Основные функции, предоставляемые этим модулем, являются аналогами функций из модуля 1. Модуль создает видимость того, что мы создали хранилище строк, содержащее не исходные строки, а все их циклические сдвиги. Таким образом, вызов функции CSCHAR(l,w,c) возвращает значение, представляющее c-й символ в w-м слове l-го циклического сдвига. Определено, что (1) если i < j, то сдвиги строки i предшествуют сдвигам строки j, и (2) для каждой строки первый сдвиг является исходной строкой, второй сдвиг получается путем однократного циклического переноса первого слова в конец и т.д. Предоставляется функция CSSETUP, которую необходимо вызвать до того, как остальные функции будут возвращать указанные значения. Более точная спецификация подобного модуля приведена в [8]. 

Модуль 4: Алфавитный сортировщик. Данный модуль состоит в основном из двух функций. Первая, ALPH, должна быть вызвана до того, как другая функция начнет возвращать корректные результаты. Вторая, ITH, служит в качестве индекса. ITH(i) возвращает индекс циклического сдвига, который занимает i-ю позицию в алфавитном порядке. Формальные определения этих функций приведены в [8].

Модуль 5: Вывод. Данный модуль формирует требуемое представление набора строк или циклических сдвигов для печати. 

Модуль 6: Главное управление. Выполняет функции, аналогичные одноименному модулю в предыдущей модуляризации.

Сравнение двух модуляризаций

Общие замечания. Обе схемы работоспособны. Первая является вполне традиционной; вторая была успешно применена в учебном проекте [7]. Обе сводят разработку к относительно независимому программированию ряда небольших, обозримых программ.

Заметьте, что обе декомпозиции имеют единое представление данных и одинаковые методы доступа. Мы обсуждаем два разных способа нарезки того, что может быть одним и тем же объектом. Система, построенная согласно декомпозиции 1, вполне может оказаться идентичной построенной согласно декомпозиции 2 после ассемблирования

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

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

1. Формат ввода. 

2. Решение хранить все строки в ядре. Для больших объемов данных может оказаться неудобным или непрактичным постоянно хранить все строки в ядре. 

3. Решение упаковывать символы по четыре в машинное слово. В случаях работы с небольшими объемами данных упаковка символов может быть нежелательной; можно сэкономить время, используя формат «один символ на машинное слово». В других случаях может применяться упаковка, но в иных форматах. 

4. Решение создавать индекс для циклических сдвигов вместо их фактического хранения. Опять же, при небольшом индексе или большом объеме ядра непосредственная запись сдвигов может оказаться предпочтительнее. Или же мы можем отказаться от предварительных вычислений в CSSETUP. Все вычисления могут выполняться непосредственно при вызовах таких функций, как CSCHAR.

5. Решение выполнить однократную полную алфавитную сортировку списка, вместо того чтобы (а) искать каждый элемент по мере необходимости или (b) выполнять частичную сортировку, как в FIND Хоара [2]. В ряде случаев было бы целесообразно распределить вычислительные затраты на сортировку во времени, необходимом для формирования индекса.

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

В некоторых версиях этой системы декомпозиция включала дополнительный модуль. Модуль таблицы символов (описанный в [3]) использовался внутри модуля хранения строк. Этот факт был полностью скрыт от остальной части системы.

Четвертое изменение ограничено модулем циклического сдвига во второй декомпозиции, однако в первой декомпозиции о нем также будут знать модуль алфавитной сортировки и модуль вывода.

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

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

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

Критерии

Теперь многим читателям стало ясно, какие критерии были использованы в каждой из декомпозиций. В первой декомпозиции применялся критерий, согласно которому каждый значительный этап обработки данных становится отдельным модулем. Можно сказать, что для получения первой декомпозиции достаточно построить блок-схему. Это наиболее распространенный подход к декомпозиции или модуляризации. Он является прямым следствием традиционного обучения программистов, которое предписывает начинать с создания приблизительной блок-схемы и лишь затем переходить к детальной реализации. Блок-схема была полезной абстракцией для систем размером порядка 5–10 тысяч инструкций, однако при выходе за эти рамки она перестает быть достаточной; требуется нечто дополнительное. 

Вторая декомпозиция проводилась с использованием критерия «сокрытия информации» [4]. Модули здесь уже не соответствуют этапам обработки данных. Модуль хранения строк, к примеру, используется почти при каждом действии системы. Алфавитная сортировка может соответствовать фазе обработки, а может и не соответствовать — в зависимости от используемого метода. Аналогичным образом, циклический сдвиг в некоторых случаях может вообще не создавать таблиц, а вычислять каждый символ по запросу. Каждый модуль во второй декомпозиции характеризуется знанием определенного проектного решения, которое он скрывает от остальных. Его интерфейс или определение выбрано так, что раскрывать как можешь меньше информации о своем внутреннем устройстве.

Усовершенствование модуля циклического сдвига

Чтобы проиллюстрировать влияние этого критерия, рассмотрим более внимательно устройство модуля циклического сдвига из второй декомпозиции. Теперь, оглядываясь назад, становится ясно, что данное определение раскрывает больше информации, чем необходимо. Хотя мы тщательно скрыли способ хранения или вычисления списка циклических сдвигов, мы указали порядок следования в этом списке. Программы можно было бы эффективно писать, если бы мы указали лишь: (1) что все строки, указанные в текущем определении циклического сдвига, будут присутствовать в таблице, (2) что ни одна из них не будет включена дважды, (3) что существует дополнительная функция, позволяющая определить исходную строку по заданному сдвигу. Задавая порядок следования сдвигов, мы предоставили больше информации, чем необходимо, и тем самым необоснованно сузили класс систем, которые мы можем построить без изменения определений. Например, мы не предусмотрели систему, в которой циклические сдвиги создавались бы сразу в алфавитном порядке, функция ALPH была бы пустой, а ITH просто возвращала бы свой аргумент в качестве значения. То, что мы упустили эту возможность при построении систем со второй декомпозицией, несомненно, следует классифицировать как ошибку.

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

1. Структура данных, ее внутренняя компоновка, процедуры доступа и модификации являются частью единого модуля. Они не являются общими для нескольких модулей, как это обычно принято. Данная концепция, возможно, представляет собой лишь развитие принципов, изложенных в статьях Болцера [9] и Мили [10]. Проектирование с учетом этого подхода явно лежало в основе создания языка BLISS [11].

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

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

4. Коды символов, правила алфавитной сортировки и подобные данные должны быть скрыты в отдельном модуле для обеспечения максимальной гибкости.

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

Эффективность и реализация

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

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

Декомпозиция, общая для компилятора и интерпретатора одного языка

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

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

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

Более подробное обсуждение данного примера содержится в [8].

Иерархическая структура

В системе, определенной согласно декомпозиции 2, можно обнаружить программную иерархию в смысле, описанном Дейкстрой [5]. Если таблица символов существует, она функционирует без каких-либо других модулей и, следовательно, находится на уровне 1. Хранилище строк находится на уровне 1, если таблица символов не используется, или на уровне 2 в противном случае. Ввод и Генератор циклического сдвига требуют для своей работы хранилище строк. Вывод и Алфавитный сортировщик будут требовать Генератор циклического сдвига, но поскольку Генератор циклического сдвига и хранилище строк в некотором смысле совместимы, было бы несложно создать параметризованные версии этих программ, которые можно было бы использовать для алфавитной сортировки или вывода как исходных строк, так и циклических сдвигов. В первом случае они бы не требовали Генератор циклического сдвига; во втором — требовали. Иными словами, принятое проектное решение позволило нам получить единое представление для программ, которые могут работать на любом из двух уровней иерархии.

При обсуждении структуры системы легко спутать преимущества хорошей декомпозиции с преимуществами иерархической структуры. Мы имеем дело с иерархической структурой, если между модулями или программами может быть определено некоторое отношение, являющееся отношением частичного порядка. Отношение, которое нас интересует, выражается словами «использует» или «зависит от». Предпочтительнее использовать отношение между программами, поскольку во многих случаях один модуль зависит лишь от части другого модуля (например, Генератор циклических сдвигов зависит только от выходных частей хранилища строк, но не от корректной работы функции SETWORD).

Можно предположить, что мы могли бы получить обсуждаемые преимущества и без такого частичного упорядочения — например, если бы все модули находились на одном уровне. Частичное упорядочение дает нам два дополнительных преимущества. Во-первых, отдельные части системы выигрывают (упрощаются), поскольку пользуются услугами нижних* уровней. Во-вторых, мы можем отсечь верхние уровни и все равно получить работающий и полезный продукт. Например, таблицу символов можно использовать в других приложениях; хранилище строк может стать основой для системы ответов на вопросы. Существование иерархической структуры гарантирует, что мы можем «обрезать» верхние уровни дерева и начать растить новую крону на старом стволе. Если бы мы спроектировали систему, в которой «нижнеуровневые» модули так или иначе использовали «высокоуровневые», у нас не было бы иерархии, удаление частей системы было бы гораздо более сложной задачей, а сам термин «уровень» потерял бы в нашей системе всякий смысл.

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

Заключение

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

Получено в августе 1971 года; доработано в ноябре 1971-го.

Примечания

* Здесь «нижний» означает «с меньшим порядковым номером».

Источники

1. Gauthier R., Pont S. Designing Systems Programs, (C), Prentice-Hall, Englewood Cliffs, N.J., 1970.

2. Hoare C.A.R. Proof of a program, FIND. Comm. ACM 14, 1 (Jan. 1971), 39-45.

3. Parnas D.L. A technique for software module specification with examples. Comm. ACM 15, 5 (May, 1972), 330–336.

4. Parnas D.L. Information distribution aspects of design methodology. Tech. Rept., Depart. Computer Science, Carnegie-Mellon U., Pittsburgh, Pa., 1971. Также представлено на IFIP Congress 1971, Любляна, Югославия.

5. Dijkstra E.W. The structure of "THE"-multiprogramming system. Comm. ACM 11, 5 (May 1968), 341–346.

6. Galler B., Perlis A.J. A View of Programming Languages, Addison-Wesley, Reading, Mass., 1970.

7. Parnas D.L. A course on software engineering. Proc. SIGCSE Technical Symposium, Mar. 1972.

8. Parnas D.L. On the criteria to be used in decomposing systems into modules. Tech. Rept., Depart. Computer Science, Carnegie-Mellon U., Pittsburgh, Pa., 1971.

9. Balzer R.M. Dataless programming. Proc. AFIPS 1967 FJCC, Vol. 31, AFIPS Press, Montvale, N.J., pp. 535–544.

10. Mealy G.H. Another look at data. Proc. AFIPS 1967 FJCC, Vol. 31, AFIPS Press, Montvale, N.J., pp. 525–534.

11. Wulf W.A., Russell D.B., Habermann A.N. BLISS, A language for systems programming. Comm. ACM 14, 12 (Dec. 1971), 780–790.