Аннотация

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

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

Введение

Ясное изложение философии модульного программирования можно найти в учебнике 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.

Комментарии (17)


  1. Emelian
    19.10.2025 14:48

    «модуль» рассматривается скорее как единица распределения ответственности, нежели как подпрограмма

    В принципе, да, только, «ответственности» кого перед кем?

    Возьмем простейший случай – оконную программу, на C++, в Windows, поддерживающей различного рода дочерние компоненты.

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

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

    Кроме того, компоненты должны как-то располагаться в главном окне приложения и менять свои координаты и размеры, при изменении размеров главного окна. Кстати, многие, по этой причине любят диалоги, там размеры и координаты компонентов фиксированы. Хотя, потом изобретают «велосипеды», вроде «ресайзэйблных» диалогов.

    Так вот, для управления только стилем компонентов, нужно, где-то, определять структуры данных для этого. Либо на стороне «сервера», в данном случае, главного окна, либо на стороне «клиента», то бишь, внутри класса компонента (обычно наследующего класс стандартного компонента).

    Иногда, это удобно делать на «сервере», а, иногда, на «клиенте». Бывают, нередко, более предпочтительны смешанные случаи.

    Вот вам и «модулярная модулизация». Правы будут и одни и другие и третьи. Фактически все зависит от конкретного проекта.

    И это мы еще не рассматривали логику работы компонентов, обмен данными между ними и т.д. Именно, по этой причине я себе чуть мозги не сломал, организуя взаимодействие различных, формально независимых, классов графического интерфейса пользователя (GUI), при работе с одними и теми же данными, в своей обучающей программе «L'école», с её шестью режимами работы и разными вариантами их использования (через меню). Для конкретики, можете посмотреть ее последнюю версию в моей статье: «Роль данных при изучении иностранного языка» – https://habr.com/ru/articles/930868/ .

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


    1. OlegZH
      19.10.2025 14:48

      и, всё-таки, в чём конкретно Вы видите "модульность"?


      1. Emelian
        19.10.2025 14:48

        и, всё-таки, в чём конкретно Вы видите "модульность"?

        На уровне проектов, на C++ / WTL, с которыми я имею дело, в своих пет-проектах, под модульностью, я понимаю:

        1. Изолированный код, который легко добавлять и удалять из проекта.
        2. Иерархию вида:

        • Функции;

        • Классы;

        • Namespaces;

        • Файлы;

        • Каталоги;

        • Проекты;

        • Решения.

        3. Шаблоны проектирования (для каждой итерации проекта):

        • Проект «Main», с номером итерации, скажем, последняя итерация моего последнего пет-проекта ( https://habr.com/ru/articles/955838/ ), была – 24;

        • Точка входа: «Main.cpp», c функцией «WinMain()» – одинакова во всех проектах, которая создаёт и вызывает экземпляр класса «CApp» и ничего более;

        • В классе «CApp» создается экземпляр класса «CMainWindow», инициализируются необходимые библиотеки, вроде, «GDIplus», класс (менеджер) потоков, при необходимости, цикл, либо менеджер событий и класс (менеджер) видов (дочерних окон, занимающих всю клиентскую область главного окна);

        • В классе «CMainWindow» создаются и инициализируются компоненты главного окна и функция их отображения при изменении размеров окна приложения;

        • Каждый используемый компонент (класс) размещается в отдельной паре файлов: *.cpp и *.h;

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

        Единственным слабым местом, для меня, здесь является проектирование общей программной логики. Именно на ней уходит больше всего времени, не считая поисков ответов, типа, «ноу-хау» (как именно сделать то, сё, пятое, десятое). Здесь хорошо помогают ИИ-сервисы, но, бывает, решение они дают и оно работает, но, как именно и почему – непонятно. Что сильно усложняет модификацию этого кода. Поэтому, владеть темой, безусловно, необходимо и не жалеть на это времени. Тогда легче и «правильные» вопросы задавать ИИ-ям и, вообще, вести с ними диалог.

        Как-то так. Ответов на все вопросы это не дает, но, я, практически, использую именно этот метод «итерационно-модульного» программирования, на C++ / WTL. Думаю, что это лучше, чем ничего.


        1. OlegZH
          19.10.2025 14:48

          Позволю конкретизировать свой вопрос. Что же такое модульность на уровне программной логики? Пусть у нас есть два модуля — модуль А и модуль Б. Можно ли. например, выделить модуль (А) для обслуживания загрузки/выгрузки приложения и модуль (Б) для печати? Можно ли выделить модуль (А) для работы с пользовательским интерфейсом и модуль (Б) для работы с базами данных? Можно ли выделить модуль (А) для работы с заказами и модуль (Б) для работы с накладными? И всё это — на уровне программной логики?


          1. Emelian
            19.10.2025 14:48

            Трудно говорить о модульной логике без привязки к системе разработки. Если мы программируем конфигурацию для «1С», то там вся пользовательская логика – локальна, в рамках модели используемой платформы. Поэтому, там легко все делиться, ибо «всё уже разделено до нас».

            Другое дело приложения уровня С++, с легкими фреймворками, вроде WinAPI или WTL. Там вся логика сосредоточена в менеджере потоков, менеджере событий и менеджере интерфейса (либо, более общо, GUI), которые программист делает сам, индивидуально, для каждого проекта.

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

            Вот эта иерархическая балансировка «ответственности», о чём упоминалось в статье, и есть искомая логическая модульность. Другое дело, что она плохо формализуема. То, что хорошо для одного проекта, может быть плохо для другого. Я, в таких случаях говорю, что главный принцип программирования – это метод «здравого смысла». По крайней мере, в самых сложных случаях, в моей практике, именно он меня выручал, а не новомодные принципы программирования.

            Однако, если вы перейдете на «тяжёлые» фреймворки, вроде, MFC, wxWidgets, Win32++ или Qt, то там уже надо «вписываться» в их модель разработки, подчинять их программной логике. Иногда, это сильно облегчает жизнь, как в случае «1С» (особенно, «семерки»), иногда, наоборот, страшно ограничивает.

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


  1. ncix
    19.10.2025 14:48

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

    Пример: допустим, есть какая-нибудь CRM. Бизнес захотел, чтобы в карточке клиента появился аттрибут "персональный менеджер". Для этого нам нужно:
    1. Добавить поле в БД, написать миграцию для этого.
    2. Добавить работу с этим полем в 3 SQL-запроса (insert, update, select) или в аналогичные выражения ORM
    3. Добавить атрибут в объект данных (entity), возможно не в один
    4. В зависимости от архитектуры, изменить интерфейсы и добавить новую логику в несколько слоев бэкенда: репозиторий, сервис, контроллер, допилить валидацию
    5. Расширить API бэкенда
    6. Дополнить еще пару слоёв во Фронтенде
    7. И только теперь добавить в UI новый элемент.
    8. Дописать тесты

    А ведь бизнес с такими простыми запросами приходит каждую неделю.


    1. ALexKud
      19.10.2025 14:48

      Если CRM не на WEB то все гораздо проще. Иногда WEB это либо дань моде или даже человеческий фактор, программисты не умеють писать не на WEB. Отсюда ненужные трехзвенки, PHP и пр. пр. пр.


      1. ncix
        19.10.2025 14:48

        Сугубо десктопные CRM - это какая доля рынка? 1% хотя бы есть?

        Отсюда ненужные трехзвенки

        А как вы представляете архитектуру CRM для хотя бы двух пользователей? А для 1000, в разных городах, странах, языках и часовых поясах?


      1. akardapolov
        19.10.2025 14:48

        Просто Web подходит не для всего. Некоторые задачи удобнее решать с помощью desktop. Это на текущем уровне развития технологий конечно.


    1. akardapolov
      19.10.2025 14:48

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

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


      1. ncix
        19.10.2025 14:48

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


    1. OlegZH
      19.10.2025 14:48

      Пример: допустим, есть какая-нибудь CRM. Бизнес захотел, чтобы в карточке клиента появился аттрибут "персональный менеджер".

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

      Для этого нам нужно:1. Добавить поле в БД, написать миграцию для этого.

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

      2. Добавить работу с этим полем в 3 SQL-запроса (insert, update, select) или в аналогичные выражения ORM

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

      3. Добавить атрибут в объект данных (entity), возможно не в один

      А что это такое?

      4. В зависимости от архитектуры, изменить интерфейсы и добавить новую логику в несколько слоев бэкенда: репозиторий, сервис, контроллер, допилить валидацию

      Неужели трудно в подавляющих случаях строить интерфейс на лету. Вернёмся к списку полей карточки клиента. Что мешает один описать всю логику работы с данным полем (персональный менеджер) в одном месте?

      5. Расширить API бэкенда

      Интересно, что Вы имеете здесь в виду? Возможность организации запросов вида "Все действующие (персональные) менеджеры", "История работы персонального менеджера" и "История взаимоотношения с клиентом (через посредство работы с одним или несколькими персональными менеджерами)"?

      6. Дополнить еще пару слоёв во Фронтенде

      Уже было сказано.

      7. И только теперь добавить в UI новый элемент.

      Уже было сказано.

      8. Дописать тесты

      Дописывать тесты приходится всегда, когда появляется новая функция. А какие тест-кейсы Вы здесь ожидаете?


      1. ncix
        19.10.2025 14:48

        на этапе составления требований упустили очевидное бизнес-правило: когда у каждого клиента должен быть свой персональный менеджер.

        Вы так говорите, как будто системы проектируют один раз на берегу, и они потом живут годами долго и счастливо без изменений. Так не бывает. В конкретном примере может быть так, что при первичном внедрении понятие "персональный менеджер" просто отсутствовало в бизнесе. А потом появилось. Реальный живой бизнес динамичен, он меняется и подстраивается под рынок, клиента, владельцев. IT-системы должны быть не препятствием в этом, а помощником.

        А что, если содержимое карточки описывается подчинённой таблицей, где каждая строка соответствует отдельному полю карточки?

        А если нет? И одной подчиненной таблицей вы не обойдетесь, т.к. поля могут быть разного типа данных (в том числе ссылочные, следовательно надо обеспечить ссылочную целостность). Можно конечно сделать "широкую" таблицу со всевозможными типами. Или хранить всё в строках, или в JSONах. Но с такими подходами рано или поздно вас ждет инженерный ад - посмотрите на Битрикс (недавно была статья), там как раз так сделано.

        > Расширить API бэкенда

        Интересно, что Вы имеете здесь в виду?

        Фронтенд (веб-морда) как-то общается с бэк-эндом (сервером), верно? Это называется API. Чтобы фронт получил/записал данные нового поля, в API оно должно появиться, верно?

        Неужели трудно в подавляющих случаях строить интерфейс на лету.

        Хорошо бы. Но в команде есть UI/UX специалист, который думает иначе, и проектирует интерфейсы исходя из удобства пользвателя а не удобства программиста.

        А какие тест-кейсы Вы здесь ожидаете?

        Банальный CRUD с новым полем и валидацию, хотя бы.


  1. swame
    19.10.2025 14:48

    В моем комплексе (больше 5 млн строк, больше 50 приложений) основное правило такое: размер модуля не должен превышать 1000 строк. Если превышает, то такой модуль кандидат на разбиение. Таких модулей 95%. Есть и исключения по 2-4 тыс строк. Естественно, модули группируются с учетом назначения и архитектурного слоя. Но в одном модуле может быть несколько связанных или однородных классов. Наша особенность - модули могут использоваться повторно в разных приложениях и сочетаться в них в разных комбинациях. Как показал опыт, такой подход оптимальный для поддержки, рефакторинга и уменьшения зависимостей.

    Раньше в легаси у нас были модули по 10-15 тыс строк, поддерживать такое был ад.

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


    1. OlegZH
      19.10.2025 14:48

      В моем комплексе (больше 5 млн строк, больше 50 приложений) основное правило такое: размер модуля не должен превышать 1000 строк

      Довольно прямолинейный подход. Здесь пригодилась бы какая-нибудь подходящая ортогонализация. Как в математике. кто предложит?


    1. ncix
      19.10.2025 14:48

      размер модуля не должен превышать 1000 строк

      Не многовато ли? Почему именно 1000? Меня лично начинают напрягать модули больше 200 строк.


  1. kgenius
    19.10.2025 14:48

    Намечен альтернативный подход к реаизации, лишенный этого недостатка

    Проверьте текст на ошибки