Непричесанные мысли о первичности ООП

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

  1. Интерфейс с внешним миром (входные и выходные данные расчетной задачи, входные и выходные сигнальные цепи, а также человеко-машинный интерфейс (HMI) - в задачах управления);
  2. Структура (всего программно-аппаратного комплекса, используемого при решении задачи);
  3. Процесс (функционирование аппаратно-программного комплекса при решении задачи);
  4. Результат (анализ результата).

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

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

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

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

Методика, инструмент проектирования Структура данных Структура программы (алгоритма) Кодирование программы (алгоритма) Процесс(характерные черты управления
алгоритмом)
классическое процедурное (алгоритмическое)
программирование
ручное кодирование - ручное кодирование прямое (последовательное) исполнение
алгоритма
функциональное программирование,
алгебра, а также (частично) язык Forth
ручное кодирование - ручное кодирование инверсное исполнение (вычисление
результата)
СУБД визуальное проектирование - автогенерация части кода (SQL) -
UML и т.п. + соответствующие CASE-средства - визуальное проектирование отдельных
участков, в особенности полезны
диаграммы состояний
автогенерация основной (скелетной)
части кода
обычно жестко соответствует схеме,
порядок исполнения зависит от
конкретного CASE
Событийное программирование (Smalltalk,
средства событийного моделирования)
моделирование данных при помощи
объектов
ручное и визуальное проектирование (в
особенности схемы обработки событий)
готовый самодостаточный код жестко соответствует схеме

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

1. Структурная схема и реализация алгоритма

Рассмотрим в качестве примера простейший участок кода, который на большинстве процедурных языков программирования выглядит как оператор присваивания: c=a+b. В виде структурной схемы его можно представить следующим образом:

Какую метаинформацию мы можем извлечь из данной структурной схемы? Мы видим, что переменная c зависит от значения переменных a и b, причем эта зависимость есть функция суммирования. Мы подразумеваем, что блоки a, b и c представляют собой абстракцию доступа к данным (извлечения исходных данных и место для сохранения результата). Мы подразумеваем также, что реализация этих абстракций в данном контексте нас не интересует, хотя бы для доступа к данным и было необходимо в реальности выполнить какие-либо сложные и длительные действия (от извлечения данных из поля экранной формы или их вычисления до отсылки результата по электронной почте). Многие средства разработки, наконец, сделали эту абстракцию прозрачной для пользователя (так, в языках Forth, Smalltalk, Delphi указание имени может означать не просто адресацию переменной, но и вызов процедуры, в т.ч. метода объекта; причем процедуры могут быть различны в зависимости от контекста употребления имени - для чтения или записи данных).

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

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

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

Обработка каждого элемента структурной схемы определяется внешним синхронизирующим источником.Обычно при проектировании алгоритма таким неявным источником служит строгий порядок его исполнения (и, соответственно, упорядоченная последовательность кодов (команд программы), этот алгоритм реализующих. Например, допустимыми последовательностями вычисления будут: a, b, +, c или b, a, +, c (или, скажем, даже c, a, b, + в зависимости от конструкции исполнителя). Последовательность, нарушающая зависимости, определенные данной схемой (например, сначала выполнить сложение и присвоение результата, а затем - вычисление a и b) будет в общем случае неверной (хотя в частных случаях приемлемой при итерационном расчете циклически замкнутых схем).Тип сообщений: синхронные.
Характеризуется активацией с "конца" (запрос на вычисление результата). В рассмотренном случае запрос на вычисление c приводит к активации функции сложения, которая, в свою очередь, запрашивает вычисления своих аргументов a и b. По окончании вычислений данные передаются отправителю запроса. Тип сообщений: синхронные. Допускает возможность оптимизации (например, "ленивые вычисления" в функциональном программировании) - если левый (по данной схеме) участок схемы является обособленным (ветка дерева без прямой или косвенной циклической связи с правым участком), то при повторном запросе значения c нет необходимости снова производить все посылки сообщений и вычислительные действия по всей цепочке, а достаточно сразу вернуть вычисленное ранее значение.
Характеризуется активацией с "начала" (распространение событий по ходу функциональных связей в схеме). Широко используется в системах имитационного моделирования и в SCADA. Естественным образом достигаемая оптимизация заключается в том, что обработка происходит только в тех участках схемы, где в результате какого-либо события произошли реальные изменения. В зависимости от решаемой задачи, синхронизирующие сообщения могут в данном примере передаваться либо через функцию "И" (сложение не активируется, пока не будут определены оба значения a и b), либо напрямую (тогда при изменении a схема сложит новое значение a со старым значением b; а при изменении b вновь будет произведен перерасчет). Разумеется, в ряде случаев для оптимизации целесообразно в некоторых объектах проверять, изменился ли его выход, и если нет, то не посылать сообщение дальше по цепочке. Кроме того, такой способ легко допускает распараллеливание действий при использовании асинхронных сообщений (объект активизировался, изменился, сообщил о своем новом состоянии по цепочке и снова перешел в пассивное состояние, не заботясь о дальнейшем).

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

Выводы:

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

Донской А. Н., 2000, http://simulators.narod.ru