Содержание

1.1 Компонентно-ориентированное программирование

1. Компонентно-ориентированное программирование

Эта парадигма программирования направлена прежде всего на повышение надёжности открытых динамических систем. Суть компонентно-ориентированного программирования (далее КОП) сводится к возможности контролировать взаимодействие проектируемых и выполняемых частей программы на предмет согласованности информационных структур. Идея является относительно новой. Частично идеи КОП воплощены в такие языки как Ада, C#, прямым применением идей КОП являются языки программирования Modula-2, Оберон, Оберон-2, и наиболее известный из семейства КОПКомпонентный Паскаль.

2. Отличительные черты КОП

Не смотря на свою относительную молодость — КОП имеет свои особенности, которые регулируют не только особенности языка, но и всю экосистему КОП. К таким отличительным чертам следует отнести:

  1. Чётко выраженную ориентированность на модули. Модуль, является основной структурной единицей. Оказывается, такие понятия, как класс, пространство имён являются плохо продуманным конструкциями в других языках.
  2. Раздельная компиляция модулей. Это приводит к сбережению вычислительных и временных ресурсов, в целом придаёт гибкости всей системе, один и тот же код остаётся в одном месте, а не многократно «размазывается», как это бывает в других языках.
  3. Строгая типизация, как внутри модуля, так и между модулями. Повышает надёжную работу компонентов в целом через сокрытие тех частей, которые не должны участвовать в межмодульном взаимодействии.
  4. Неизбежность динамической сборки мусора. Для компилируемых языков это важный и необычный механизм, который только в самое последнее время начал признаваться сообществом программистов (например, такие популярные языки, как C#, Java, Golang).
  5. Строгое разделение частей модулей, на предназначенные для взаимодействия с другими модулями, и скрытые части только для работы внутри модуля.

3. Отличия от функционального программирования

Как и в функциональном программировании, в компонентно-ориентированном не только возможно, но и приветствуется использование функций и процедур. В обоих видах декомпозиции1) возможно использование функций внутри других функций2), статических и динамических переменных 3), глобальных и локальных структур. В виду явной необходимости в функциональном программировании введены такие абстракции, как например:

Ряд средств в функциональном программировании на самом деле избыточен, но другая часть действительно нужна, но это только усугубляет проблему: из-за различных эффектов характерных для функционального программирования, программисту легко потерять логику рассуждений и совершить какую-нибудь досадную ошибку, на выявление которой может уйти огромная часть времени. На практике, отладка программы и выяснение различных непонятных эффектов уходит львиная доля рабочего времени программиста. Такой язык, как Си вполне соответствует циклу «код-поиск ошибок-отладка». Мощный, как трёхлинейная винтовка, и такой же опасный — можно шутя выстрелить себе в ногу с более чем фатальными последствиями. В целом, про функциональное программирование можно сказать, что оно отлично применимо к несложным проектам, которые должны работать с высочайшей скоростью на относительно простых аппаратных платформах. Конечно, при соответствующей сноровке можно писать и сложные программы на сложном оборудовании, но стоимость такой работы будет относительно дорогой 4). Компонентно-ориентирование программирование стремится всеми доступными средствами к надёжности и гибкости на сколько это возможно. В целом, программа представляет из себя на КОП несколько хорошо изолированных частей. Это приводит к ясной структуре и простым эффективным правилам передачи и обработки информации, что безусловно положительно влияет на надёжность в целом. Сокрытие информации отсеивает лишнюю информацию для программиста, что позволяет ему сконцентрироваться на существенной части задачи. Упрощение языка и отказ от многих сомнительных приёмов программирования вынуждают программиста писать простой для понимания программный код, в котором трудно совершить досадную ошибку. Компилятор языка программирования в духе КОП не позволит создать программу, в которой программистом допускается нарушение отношений типов данных. Можно сказать, что требование к надёжности программ, создаваемых с помощью КОП является главным. В связи с этим, синтаксис языков специально спроектированных для компонентно-ориентированного программирования достаточно прост и выразителен. Едва ли возможна, например, в таком языке как Компонентный Паскаль нечитаемая конструкция, как в уже вышеупомянутом Си или С++. В попытках понять что имел в виду программист на Си можно провести огромную часть рабочего времени. В целом про КОП можно сказать, что он предназначен для крупных программных систем, к которым предъявляются особые требования к надёжности.

4. Отличия от объектно-ориентированного программирования

С точки зрения программиста, придерживающегося компонентно-ориентированного программирования далеко не всё является объектом. Строго говоря, это действительно так. Понятие объект импортировано из логики, где кроме объектов есть ещё и субъекты. Разница между ними заключена, как известно, в том, что субъекты воздействуют на объекты. Но где можно найти формулировку субъекта в объектно-ориентированном программировании(ООП)? Т. е. попросту говоря, в ООП изначально неверно определены категории сущностей, что неизбежно заставляет ввести в ООП такие абстракции как «активный объект» (что на самом деле и является субъектом, хотя субъект не только не производное от объекта, а вообще независимая самостоятельная абстракция в логике).

Приверженцы ООП в качестве важного отличительного свойства этой парадигмы указывают на возможность наследования — отношения двух сущностей, когда одна из сущностей либо полностью, либо частично на основе наследования повторяет поведение и состояние другой сущности, и при этом расширяет (или наоборот ограничивает) поведение сущности-предка. Действительно, такое поведение часто встречается в реальном мире, и не лишено основания. Но, тем не менее, существующие попытки свести всю иерархию сущностей в одно дерево развития на самом деле мало чем оправдано и приводит к тому, что изменив базовую сущность автоматически происходит изменение и всех зависимых сущностей. И это не всегда удобно. Точнее даже, частенько это может грозить катастрофой программному проекту. Кроме того, существует огромное число объектов, для которых привязка к базовому типу совершенно необоснованна. Так например, такой ООП язык программирования, как Java принуждает программиста строить все объекты от встроенных. Из-за подобного жёсткого ограничения пришлось вводить возможность поломать это правило. Разумеется, это приводит к потенциальным нарушениям. В целом, ООП заметно облегчает декомпозицию программы, но в то же время привносит и свои сложности, в ключе понимания логики исполнения программы в целом. Компонентно-ориентированное программирование не принуждает программиста использовать какие-либо сущности. Язык и так достаточно строг, для того, чтобы программист не смог поломать свой код. Понятие сущности существенно совмещено с понятием модуля, и как правило, сущность содержится в одном модуле, что позволяет более полно контролировать логику программы. Впрочем, при явной необходимости сущность может быть распределена между многими модулями (например, при случае объединении в одной сущности многих других меньших сущностей, и отношение «содержится» более поощряется, чем отношение «является»). При этом КОП, в отличии от ООП элегантно и естественно решают проблему хрупкого базового класса — множественное наследование попросту не нужно.

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

> сахар = вода + углекислый_газ + ультрафиолет

Но стало ли понятней, что происходит в таком коде? Давайте взглянем на код написанный в функциональном стиле

> сахар = СинтезироватьСахар(вода, углекислый_газ, ультрафиолет)

Здесь уже вполне видно, что суть сахара не равна сути воды, углекислого газа или ультрафиолета. Между этими понятиями знак равенства слегка неадекватен. В указанном примере идёт явное превращение нескольких свойств в новое свойство, более неприводимое ни к одному из предыдущих. В самом деле, запись

> вода = сахар - (углекислый_газ + ультрафиолет)

в корне не верна и даже эмоционально вызывает чувство протеста. Кроме того, до сих пор идут жаркие споры, а на сколько полиморфизм вообще является частью ООП? Существует достаточное число языков программирования в которых полиморфизм есть, а объектов – нет!) Обратное утверждение также верно: существуют языки с парадигмой ООП, но в них нет перегрузки операторов. Очень спорно назвать такие языки не полностью отвечающими парадигме ООП.

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

5. Контроль во время компиляции и исполнения

Эти две фазы гораздо более тесно связаны, чем в случае других парадигм программирования. Так как, например C++ не гарантирует исполнения кода во всех возможных случаях, в него с неизбежностью введены лексемы, служащие для перехвата исключений. И это, в целом, плохо. Такие лексемы говорят о том, что язык не был сконструирован, а скорее сочинён. Более того, наличие инструкций перехвата управления, вовсе не гарантирует отсутствие таких эффектов как улететь в космос (неправильное исполнение программы, которое может нанести существенный ущерб целостности структур данных). В КОП нет ничего подобного. Подобные лексемы избыточны и в-общем случае – бессмысленны. В то же время, вполне возможна ситуация, когда интерфейс модуля в КОП был изменён. В такой ситуации надстройка над средой исполнения, отвечающая за состыковку различных модулей в режиме исполнения заметит несоответствие типов и запретит передачу данных между несовместимыми модулями. Это позволит своевременно остановить распространение ошибки по всей программе между модулями, которые, возможно, даже не участвовали во взаимодействии двух первых несовместимых модулей. Такое правило контроля во время исполнения приводит к важному следствию: взаимодействие между модулями происходит только с помощью базовых типов (такие как, целые числа, вещественные числа, массив литер). Во время компиляции, все задействованные модули для разработки проверяются на согласованность со вновь создаваемым модулем, а если уже существующий модуль необходимо изменить — создаётся новый (с другими интерфейсами), либо изменяется содержимое существующего модуля (с учётом возможных последствий для зависимых модулей). Так, например, решается проблема «ада динамических библиотек». Кроме того, для контроля правильности исполнения программы, введены специальные средства контроля содержимого переменных на допустимые значения — инварианты (использование таких средств считается хорошим тоном). Инварианты в КОП используются в форме предусловий (в начале участка кода), постусловий (в конце участка кода), условий (в произвольном месте кода).

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

6. Отличительные особенности компонентно-ориентированного программирования

КОП, в чём-то расширяет, а в чём-то ограничивает другие идиомы программирования. Так, поддерживается функциональный стиль, но ограничиваются сторонние эффекты. Как такого в КОП нет понятия класса, но в то же время, структуры данных, и привязанные к ним процедуры в рамках модуля — вполне описывает понятие класса в ООП. Достаточно простые исходные лексемы позволяют строить (и поощряют такое построение) код, который хорошо согласуем, сильно типизирован, может быть расширен (как через исходный код, так и через скомпилированный в машинном виде). КОП, прежде всего, нацелен на устойчивую динамичную среду. Так например, возможна ситуация в ОС Linux, когда с помощью специальных механизмов в ряде случаев можно внести исправления прямо в работающее ядро, в Компонентном Паскале – динамически заменить один работающий модуль на другой не представляет сложности (за исключением тех случаев, когда происходит взаимодействие с внешними ресурсами, которые Компонентный Паскаль контролировать, по понятным причинам, – не может). Также следует упомянуть о том, что в компилируемых языках автоматическая уборка мусора не такая простая вещь. В динамических системах потребность в сборщике мусора обязательна. По сути, операционная система сама и является таким глобальным сборщиком мусора. Обычно, программист сам должен контролировать низкоуровневые операции с памятью, так как языки программирования и операционные системы, обычно, не имеют развитых средств управления памятью (хотя, изредка и попадаются приятные исключения, например операционная система Aos, написанная на прямом родственнике Компонентного ПаскаляАктивном Обероне). И здесь не нарушается принцип абстракции. Используя КОП можно построить систему, в которой уборка мусора будет выполняться автоматически и абсолютно точно. Это можно достигнуть несколькими способами, и самый доступный из них – подсчёт ссылок на структуры данных. Если модуль, который обрабатывает какую-либо структуру выгружен из памяти – очевидно, эти данные уже никто не сможет обработать).

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

Сборщик мусора – это не часть программы. Это часть среды. Именно поэтому, освобождение памяти становится гарантированно правильным. Такой класс уязвимостей ПО, как переполнение стека становится вымирающим видом.

7. Примеры реализации компонентно-ориентированного программирования

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

Oberon
Oberon-SA Oberon-2 Активный Оберон Zonnon
Revised Oberon Компонентный Паскаль
Oberon-07

8. Компонентный Паскаль

Одна из самых известных реализаций КОП. Существует ряд весьма сложных проектов, выполненных на нём. Чего стоит система управления транспортом в Швейцарии, или бортовая система для беспилотного вертолёта. Основной боевой самолёт Евросоюза – Еврофайтер частично оснащён бортовым ПО на Компонентном Паскале. Компонентный Паскаль можно встретить под ОС Windows, Linux, ColibriOS. На родственных языках была написана ОС Oberon, Blubottle (Aos, LinAos, WinAos), ряд других экспериментальных проектов. Существует реализация Компонентного Паскаля для среды .Net, Java, JavaScript, интерпретатор на Golang и конечно GNU GCC.

9. Ссылки

Оглавление | Далее

1)
Декомпозицией называется способ объединения различных составных частей в одном объекте. Также существует и другие способы сочетания различных объектов в одном.
2)
Функцией называют такой участок программы, который может возвращать какой-либо результат. Процедура, в отличии от функции, результат возвращать не может. Во многих языках (в том числе и в Компонентном Паскале) от такого разделения на процедуры и функции – отказались. Любая процедура может возвращать результат.
3)
Статической переменной называется такая переменная, которая создаётся один раз, и до конца выполнения программы. Динамическая переменная создаётся «на ходу». Обычно, динамические переменные используются внутри вызываемых процедур. Такой подход позволяет сократить потребление памяти. Динамические переменные в Компонентном Паскале имеют строгую типизацию
4)
В учебнике Орлова по методам разработки программного обеспечения можно найти сравнительную таблицу, в которой приводятся относительные трудозатраты на создание единицы программы. Ассемблер примерно в 2-3 раза затратней, чем Си. Си примерно в 2 раза затратней С++. Паскаль экономичней Си ещё в 2-4 раза. Модула-2 экономичней Паскаля примерно в 1,5 раза. Компонентный Паскаль, вместе с фреймвороком BlackBox ещё увеличивает возможности программиста. Итого, по сравнению с тем же Си, Компонентный Паскаль вместе с Блэкбоксом дают преимущество примерно в 7…8 раз. Это, конечно, без учёта опыта программиста.