Pull to refresh

ООП в графических языках программирования. ч.2 МОП и ООП

Reading time 6 min
Views 13K

В первой части я попытался показать, что черная кошка ООП в темной комнате графических языков существует, даже если это не кошка, а полудохлый кот живодера Шредингера, который то есть, то его нет. Был показан пример реализации методологии объектно-ориентированного программирования, когда программа – это не код на языке С++ или Java, а диаграмма Simulink, SimInTech, SimulationX или SCADE Esterel, — любая графическая нотация описания алгоритма.


В рекламных материалах Мatlab Simulink часто используют термин МОП — Модельно Ориентированное Проектирование (Model-based design). Во многих текстах они подчёркивают, что графическая схема алгоритма это модель, что, конечно, верно. Однако в первоначальном определении МОП, модель – это прежде всего модель объекта, к которому разрабатывают систему управления, включая управляющее программное обеспечение. Подробнее методолгия МОП описана здесь. Таким образом, разрабатывая систему управления по методологии МОП можно и нужно использовать методологию ООП для разработки управляющего ПО. И что бы окончательно закрыть вопрос с моделями, вот вам картинка с отличиями одного от другого. Если все понятно, то можно дальше не читать.




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


В рамках моей специальности мне приходилось работать с управляющим ПО для АЭС, где стандартами безопасности запрещено использовать С++, только чистый Си, а в некоторых случаях даже не Си, а «железную» логику, где алгоритмы реализованы в виде электронной схемы на транзисторах и реле. И за соблюдением стандартов следят суровые парни из надзорных органов.



Но и тут, как бы удивительно это не звучало, разработка все равно идет с использованием методологии ООП. Потому как, когда ООП использовать нельзя, но очень хочется, то можно. Правда, потом нужно все вернуть назад, и что бы никакого С++ и итоговом коде не оказалось, ибо безопасность и нормативы «über alles». Как говорится, и рыбку съесть, и за нарушения не сесть.


Чтобы сделать настоящий объект по определению ООП, мы связываем структуры данных и схемы их обработки в единый объект, это называется инкапсуляция. А поскольку для надежных систем АЭС нам нельзя использовать С++, мы при генерации кода должны все это разобрать обратно. Как пояснили в комментариях к предыдущей статье, первая компилятор С++ СFront работал так же, осуществлял трансляцию ООП С++ кода в чистый Си.


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


Рассмотрим второй вариант реализации методологии ООП в графических языках программирования. На рисунке 1 изображена схема работы алгоритма обработки датчика.



Рисунок 1. Программа обработки показаний датчика.


Это метод класса. Ему соответствует в базе данных своя категория «Датчики» — абстрактный класс с заданным наборов полей и экземпляр класса KBA31CFO1 конкретный датчик. У данного датчика поля имеют конкретные значения, часть полей задаются пользователем, часть полей рассчитывается процессе выполенения программы. см. рис. 2



Рисунок 2. База данных сигналов с открытой категорией «Датчик».


Пока все как в первом варианте, где у нас формировалась привязка расчетной схемы к конкретному датчику при установке блока на схему. «А где разница?» — спросите вы. А разница – внутри блока. Если в первом варианте внутри была расчетная схема, которая копировалась при каждой установке блока, то в этом варианте внутренность выглядит примерно так:



Рисунок 3. Внутренности схемы блока экземпляра класса.


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



Рисунок 4. Схема обработки сигнала датчиков в режиме просмотра значений.


Таким образом, одна схема обработки реализует обработку всех датчиков в проекте, причем каждый датчик обрабатывается со своими параметрами, заданными в базе данных как характеристики конкретного экземпляра класса. Например, ограничитель берет из базы данных максимальное значение, которое задано одинаковым для первых трех датчиков и отличается у четвёртого. (см. рис. 5)



Рисунок 5. Параметры блока ограничителя в схеме расчета.


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


/* Index=104
   UID=104
   GeneratorClassName=TSignalReader
   Name=buz1.sensor.Macro6.Macro3.Macro157.SignalReader3
   Type=Чтение из списка сигналов */
};
state_vars->kbastdv104_out_0_[0] = kba31cf001_mf_type;
state_vars->kbastdv104_out_0_[1] = kba32cf001_mf_type;
state_vars->kbastdv104_out_0_[2] = kba33cf001_mf_type;
state_vars->kbastdv104_out_0_[3] = uf40y329084320_mf_type

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


/* Index=211
   UID=211
   GeneratorClassName=TAndSrc
   Name=buz1.sensor.Macro6.And61
   Type=Оператор И */
for(i=0;i<4;i++){
locals->v211_out_0_[i] = state_vars->kbastdv125_out_0_[i] && (!(locals->v191_out_7_[i] > 0.5));

/* Index=212
   UID=212
   GeneratorClassName=TMulDbl
   Name=buz1.sensor.Macro6.Mul_oper1
   Type=Перемножитель */

locals->v209_out_2_[i] = consts->kbastdv121_a_[i]*state_vars->kbastdv127_out_0_[i];

/* Index=213
   UID=213
   GeneratorClassName=TSumSrc
   Name=buz1.sensor.Macro6.Add_oper1
   Type=Сумматор */

locals->v209_out_3_[i] = (1)*consts->kbastdv122_a_[i]+(1)*locals->v209_out_2_[i];
 …
}

После расчета записываем сигналы для каждого экземляра класса:

/* Index=776
   UID=776
   GeneratorClassName=TSignalWriter
   Name=buz1.sensor.Macro6.Macro3.SignalWriter4
   Type=Запись в список сигналов */

 kba31cf001_mf_xb01 = state_vars->kbastdv207_out_0_[0];
 kba32cf001_mf_xb01 = state_vars->kbastdv207_out_0_[1];
 kba33cf001_mf_xb01 = state_vars->kbastdv207_out_0_[2];
 uf40y329084320_mf_xb01 = state_vars->kbastdv207_out_0_[3];

Как видим, в конечном коде никаких объектов нет. Чистый, невинный и безопасный СИ. В приведенном примере реализации ООП в графическом языке векторная схема обсчитывает все однотипные датчики. Такой прием позволяет изменить одну схему для изменения обработки всех датчиков.


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


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


На рисунке 6 — пример двух блоков классов «родитель» и «наследник». Внутри блока сохраняется схема расчета родительского класса. Все данные уходят в общий векторный блок, аналогичный блоку на рис. 4. Полностью повторяется метод родительского класса. А потом у класса-наследника появляются дополнительное поле Yатм и дополнительный пересчет величины с помощью блока линейной интерполяции.


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



Рисунок 6. Полиморфизм у наследников классов.


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


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

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+3
Comments 8
Comments Comments 8

Articles