Monday, October 29, 2012

Компонентная философия и реализация

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

  Так как компонентно-ориентированное программирование(далее КОП) это развитие идей объектно-ориентированного программирования, именно его я и буду использовать для сравнения. Я предполагаю, что вы понимаете основные концепции "чистого" и "грязного" ООП, и имеете некоторый опыт его использования на практике.
  К сожалению, пока вы не сможете "потрогать руками", то о чём здесь написано, так как на сегодня не существует ни одно реализации компилятора, но попробуйте оценить "на глаз". Первые реализации(прототипы) компилятора вероятно будут создавать JVM классы (я ещё не копал этот вопрос глубоко, возможно .NET будет лучшим выбором), но в будущем я бы хотел сделать также натив(x86) версию. 
  Разумеется, предлагаемое здесь не является "серебряной пулей", однако, я надеюсь, это поможет сделать программирование лучше!
Далее вас ожидает
Современное ООП - это отстой!
  Несколько "притянутых за уши" аргументов против ООП.
Компонентная философия
  Много разных мыслей не о чём конкретном.
How it works
  Собственно описание двухколёсного экипажа...
Погружаемся глубже
  ...с блэк-джеком и девочками.
Примеры
  Наиболее извращённый способ сделать простые вещи сложными.
Современное ООП - это отстой!
  Здесь я немного напишу об ООП, каковы его достоинства и недостатки, чтобы в дальнейшем вы понимали, от чего я отталкивался при разработке КОП.
  Думаю, ООП это одна из наиболее мощных(а может быть и самая мощная) парадигма, позволяющая решать огромный круг задач программирования. Косвенным подтверждением мощности ООП можно считать его популярность, оно есть практически во всех майстримных ЯП. ООП смогло получить столь широкое распространение благодаря тому что сочетало сокращение количества работы программиста(да, программисты весьма ленивы:)) с привычным и понятным процедурным программирование(вспомните класс это всего лишь усовершенствованная структура). ООП это инструмент конструирования ПО, предоставляющий программисту(собственно выполняющему конструирование) модель мышления(саму ООП модель "всё объекты обменивающиеся сообщениями..." и набор средств/приёмов для преобразования реальных объектов в объекты этой модели), а также средства переноса сконструированного в уме в компьютер(это собственно язык, ИДЕ и т.п.).
  Но!
ООП это сложно
  В ВУЗе, где я учусь, ООП-сенсей говорил о разработке ПО: "Всё просто, посоздавали объекты, повызывали их методы - программа готова!". Но, из пяти человек моей группы(не считая меня) заданное сделал только один, и то на помолам с ООП-сенсеем. Четверо из этих пяти относительно легко осилили процедурное программирование(C, Pascal) и трое матан(дифференцирование, интегрирование), то есть глупыми я бы их не назвал. Как и я в своё время, они не могли понять фундаментальные концепции ООП("всё объекты...", наследование etc.), и как следствие задавались вопросами вроде "Зачем создавать экземпляр чтобы вызвать метод?", "Чем объект отличается от класса?", "Зачем это ООП вообще нужно?" и т.п.
  Думаю причина сложности понимания даже "чистого" ООП в том что:
1.IRL люди очень редко используют такие высоко-абстрактные понятия как "объект", "класс" etc., т.к. больше взаимодействуют с конкретными вещами. Из-за чего им сложно представить объект как собственно "объект" а не как нечто конкретное.
2.IRL людям редко приходится абстрагировать/классифицировать/обобщать, в большинстве случаев используются готовые классификации и абстракции.
3.IRL люди не очень часто используют композицию-декомпозицию, а это ключевые навыки для любого конструирования(не только программ).
4.IRL люди не часто пользуются воображением, дающим возможность представить нечто одно как нечто другое, например, представить габариты здания как структуру из трёх переменных.
Все эти(а может быть и не только эти) скиллы требуют серьёзной прокачки только для того чтобы человек смог собрать что ни будь из набора готовых ООП объектов(как например в QT конструкторе можно складывать простенькие приложения), ну а осилить магию превращения объекта реальности в конструкцию из методов и свойств, та ещё задача.
  Но это как говорится ещё цветочки, настоящие проблемы начинаются когда вы попробуете разобраться в хитросплетении иерархий классов и обращений к полям, какого ни будь сложного приложения, особенно плохо документированного и "запущенного". 
Реализации ООП ужасны
  Не смотря на всю красоту и лаконичность ООП-концепции, её майстримные реализации представляют из себя свалку фичь.
ООП способствует деградации программ
  Да, ООП в своё время позволило поднять уровень сложности программ, однако сегодня ООП (старые реализации) исчерпали свои возможности. Сложные проекты превращаются в горы классов с дикой структурой, и хитросплетением зависимостей. Даже хорошо спроектированные проекты с течением времени превращаются в кошмар, и требуют трудоёмкого рефакторинга.
Компонентная философия
  В этой главе я опишу несколько основных, философских концепций, лёгших в основу этого проекта, а также некоторые фантазии, без какой либо конкретики. Если вас больше интересует как конкретно это устроено можете пропустить эту главу. Также, здесь я почти не буду затрагивать экономический аспект компонентного программирования, на это тему уже и так много написано, в частности рекомендую: Компонентное программное обеспечение © 1997, Cuno Pfister (Oberon Microsystems, Inc.).
  Компоненты, это объекты с упорядоченными связями!
Компонентное мышление
  Занимаясь разработкой ЯП, я вскоре понял, что программирование, оно не в компьютере, оно в голове, иначе говоря не достаточно просто сделать язык программирования. С тех пор задача была сформулирована так: Во первых, необходимо разработать модель мышления(парадигму, образно можно представить её как приложение "инсталлируемое" в мозг), использование которой программистом, облегчило бы работу по созданию программы. Во вторых необходимо разработать программный инструмент, который бы облегчил программисту перенос создаваемой им программы из голов в компьютер, и наоборот.  
  Многие разработчики создают ЯП так, чтобы как можно больше упростить написание программ, моя-же цель сделать написание вообще не нужным. То есть, в идеале, прикладные программисты не будут писать программы, они будут их конструировать/собирать из компонентов. Иначе говоря я хочу избавить программистов от этапа кодирования(в последовательности: дизайн -> разработка -> кодирование) и как следствие от всех связанных с этим проблем(необходимость изучения ЯП, отладка, тестирование, рефакторинги и т.п.). Каким конкретно образом будет собираться программа не важно, это могут быть графические редакторы или DSL'и заточенные под конкретную предметную область, или даже какие то автоматические системы построения/развёртывания/адаптированния. 
  Разумеется, эта идея не нова, и раньше предпринимались попытки создать компонентные средства разработки, например Delphi, .NET, BlackBox и прочее. Однако все они предлагают компонентное программирование как надстройку над ООП, что требует от прикладных программистов знания и КОП и ООП, иначе говоря, программисты должны знать/понимать как устроены компоненты, как их создавать и т.д. Я предлагаю "разделить" программистов на тех кото разрабатывает компоненты, и на тех кто их использует(прикладных программистов), причём последние могут абсолютно ни чего не знать о внутреннем устройстве компонентов(к стати на заре электроники, разработчики сами создавали схемы из примитивных, универсальных элементов(что можно сравнить с сегодняшним подходом к разработке программ при помощи ЯОП), со временем электронщики разделились на тех кто разрабатывает микросхемы, для некоторой области применения, и на тех, кто конструирует готовую аппаратуру из микросхем).
  Прикладному программисту будет достаточно знать что-то вроде: есть некоторый компонент, который ведёт себя некоторым образом, что к этому компоненту можно "прикрутить" другой, а к тому третий, и вместе они будут составлять систему, решающую некоторою задачу. При этом он может совсем не знать, что на самом деле представляет из себя компонент, а просто "двигать кубики" в IDE. Или даже вообще не знать о компонентах, а просто описывать решение своей задачи на некотором DSL. То есть, это позволит программисту мыслить на более высоком абстрактном уровне, а значит разрабатывать/работать с более сложными системами. Повышение абстрактного уровня достигается за счёт более строй инкапсуляции и автоматизации взаимодействия компонентов.
  Более строгая инкапсуляция также облегчит жизнь разработчику компонентов, ему не нужно будет знать всю систему(всё приложение например, или группу приложений для которых он создаёт компонент) и всю предметную область, а достаточно знать некоторою небольшую проблему предметной области, для решения которой он должен создать компонент, и граничащие с ней решения-компоненты, с которыми его компонент будет состыковываться.
  По существу компонент(как и ООП-объект), является универсальной сущностью, которую можно "превратить" в отражение любого реального объекта(не только физического). Образно можно представить компонент, например, как кусок глины, из которого можно вылепить любую фигуру. По сравнению с ООП-объектами, на взаимодействие компонентов накладываются некоторые дополнительные ограничения(подробней в след. главе), однако они являются необходимыми, для правильного функционирования компонентных программ.
  По идее компоненты, отражающие некоторую предметную область, будут объединятся в библиотеки компонентов(как сейчас это делается с библиотеками функций и классов). Каждый компонент в библиотеке является решением какой то одной, маленькой подзадачи предметной области, и может соединятся с другими компонентами(как из этой так и из других библиотек). Затем уже эти библиотеки будут использоваться прикладными программистами для сборки программ решающих конкретные, прикладные задачи. Библиотека может быть снабжена визуальным конструктором и/или DSL'ем(о чём ниже), что значительно упростит работу с ней(подобный подход уже успешно используется например, для разработки ПО программируемых логических контроллеров). Иначе говоря, КОП очень хорошо подходит для реализации подхода "Domain Driven Design".
  КОП предполагает опосредственный подход к программированию("программирование от третьего лица"), то есть программист, как бы лишь наблюдает за работой программы и вносит в неё изменения в случае необходимости, но сам не "учувствует" в её работе. Иначе говоря вам следует представлять программы и компоненты как активные сущности, обладающей поведением и состоянием, а не как к пассивные, неизменяющиеся объекту("объект изменится только если я его явно изменю..."). Например, при написании программы, вместо того чтобы думать "сейчас я создам окно, на нём помещу кнопку...", вам лучше думать "программой будет создано окно, на котором разместится кнопка...". Что, например, автоматически приведёт вас от "если появится ошибка я её исправлю...", к "когда появится ошибка, программа её исправит/обойдёт...". Переход к такому мышлению довольно сложен, однако оно того стоит. Вы сможете создавать действительно "самостоятельные" и стабильные программы и компоненты, требующие минимума поддержки, и вообще какого либо вмешательства в свою работу.
  Разработка компонентов более сложна чем разработка например классов или функций, так как требует системного, "четырёхмерного" мышления. Необходимо не только представлять/понимать каков будет компонент, но и то как он будет изменятся, с течением времени, под влиянием внешних событий и внутренних процессов. Но, этого не стоит боятся, так как дополнительные затраты с лихвой окупятся при конструировании из компонентов. Даже если и возникнет необходимость в разработке новых компонентов, то во первых, их количество будет небольшим(по сравнению готовыми компонентами, использованными в приложении), во вторых, у вас(как разработчика компонента) уже будет "окружение" из готовых компонентов, т.е. вам будет на что опереться/отчего оттолкнуться(иначе говоря будет достаточно просто создать компонент связывающий в одно приложение уже готовые части).  
КОП и ПОЯ
  DSL, это язык программирования, который остро заточен под некоторою предметную область. С одной стороны, DSL удобен в рамках своей предметной области, с другой стороны, он ограничивает программиста теми же рамками предметной области и своей продуманностью. Ограничения приводят к необходимости использовать несколько DSL и/или язык общего назначения в одной программе(данный подход называется "языково-ориентированное программирование"). Что, в свою очередь порождает проблему совместимости языков.
  Сегодня, DSL'и как правило транслируются в языки общего назначения(хост-язык), либо сразу компилируются в бинарный/байт код выполняющей платформы(что можно представить как трансляцию в ассемблер), так же иногда DSL'и интерпретируются(но сейчас не об этом). То есть фактически происходит преобразование одного ЯП в другой. Если в программе несколько ЯП то все они, в конечном итоге, "сливаются" в один. Именно во время слияния и возникает проблема совместимости. Беда в том что элементы ЯП(переменные, операторы, функции, классы etc.) слишком малы и универсальны, потому транслятор склеивающего языка(даже если ЯП статически типизируемый) не может обнаружить ошибки, на подобии случайного затирания значения в переменной или вызова метода с некорректными параметрами.
  КОП предполагает, что DSL будет транслироваться не в хост-язык, а в конструкцию из компонентов. Такой подход не только устраняет проблему совместимости нескольких DSL в одной программе, но и даёт несколько весьма весомых преимуществ.
  Компоненты всегда специализированы, т.е. каждый компонент заточен под выполнение только какой-то одной, конкретной работы. Взаимодействие компонентов строго определено, т.е. одному компоненту будет крайне сложно сделать "что-то не то" с другим, так чтобы это осталось не замеченным. К тому же, компоненты, являясь активными сущностями, могут сообщить программисту о не адекватных действиях с ними. Иначе говоря, если разработчик компонента допустил ошибку, способную привести к не совместимости, программа просто не соберётся.
  В КОП, DSL-фреймворк представляет из себя библиотеку компонентов, отражающую предметную область, плюс компоненты-плагины для компилятора и IDE, и/или компонент-интерпретатор позволяющие сделать программирование на DSL динамичным(о чём ниже). В одном приложении может быть сколько угодно взаимодействующих DSL-фреймворков, причём их интеграция не будет проблемой, так как всё это не более чем компоненты. Пользователь(прикладной программист) DSL-фреймворков, может и не знать ни о каких компонентах, а просто писать код, даже методом "научного тыка", остальное сделает "шайтан-машина". Думаю, такой подход позволит объединить достоинства DSL и библиотек.
  Тем не менее, я считаю, что создавать DSL на каждый чих как минимум бессмысленно. Только когда будет накоплена достаточная, устоявшаяся база компонентов, можно снабдить её DSL'ем для более удобной работы, или когда библиотека компонентов изначально разрабатывается с учётом поддержки DSL.
Концепция "Let it crash"
  Даже если программный код хорошо отлажен, протестирован, и казалось бы не содержит ошибок, нет никаких гарантий что он не даст сбой. Так как, во первых, в хоть сколько-то сложной программе не возможно за разумную цену отловить все ошибки. Во вторых некоторая доля ошибок являются следствием аппаратных проблем(например, высоко-энергетическая частица может изменить состояние ячейки ОЗУ, что повредит программу).
  Программируя в КОП стиле, вам не следует считать ошибки чем-то страшным и не поправимым, относитесь к этому как к неприятному но неизбежному явлению, думайте не "что делать если случится ошибка", а "что делать когда она случится".
  В КОП принята концепция "let it crash"(как в Erlang). Когда внутри компонента что-то идёт не так и он не способен самостоятельно это исправить(когда появляется не обрабатываемое исключение), компонент разрушается. При этом компоненты, которые в этот момент были связаны с разрушившимся, получат соответствующее уведомление. Разрушение одного компонента или даже кластера компонентов не приводит к завершению работы приложения. Компоненты(или один компонент, выполняющий роль супервизора) получившие уведомление должны устранить проблему и восстановить(регенерировать) разрушившеюся часть приложения. В теории, благодаря такому подходу можно создавать высокостабильные программы, работающие десятки-сотни лет без перезапуска.
  Негативной стороной такого подхода для программиста является необходимость всегда быть готовым к тому что "что-то пойдёт не так".
Динамичное программирование
  Как проходи ваш пограммёрский день? Уверен, обычно так: придя на работу первым делом вы проверяете почту/соц. сети/форум, отвечаете на интересные сообщения, затем завариваете себе чай/кофе, собираетесь с духом и приступаете к кодированию, от сюда и до обеда :). Сам процесс состоит из унылого, однообразного цикла: пишем код -> компилируем код -> тестируем код -> пишем код -> компилируем код -> тестируем код -> пишем код -> компилируем код -> тестируем код -> пишем код -> компилируем код -> тестируем код -> пишем код -> ... ЧОООООРТ, зачем я стал программистом! Но что поделать, так уж принято, ещё со времён появления первого ЯП.
  А теперь представьте что программирование из скучного и однообразного занятия превратилось в увлекательный квэст или шутер! Каждый день новое свершение, новое приключение! Думаете это не возможно!? :))
  По работе мне иногда приходится конструировать разные детали и механизмы. Обычно они не бывают сложными, потому проектирование можно выполнить полностью в уме или, в крайнем случае, на бумаге. Так было пока я не столкнулся с задачей превосходящей мои возможности. Потратив две недели и гору материала на безуспешные попытки, я таки решился посмотреть в сторону CAD'а. В те времена я мало что знал о них, считал что максимум на что они способны - сделать черчение проще и по ГОСТу. Ох, как я был не прав! Современные CAD'ы это мощнейшие инструменты, обеспечивающие полный цикл разработки, от идеи до пакета чертежов, от отдельных деталей до интерактивной сборки и тестирования устройства целиком. Именно последнее - возможность увидеть устройство собранным, работающим и возможность вносить изменения в реальном времени, впечатлили меня больше всего. Почему этого нет в программировании?! - подумал я.
  КОП позволяет сделать программирование именно таким - интерактивным и динамичным. Вам следует представлять КОП приложение как динамичную(меняющуюся во времени) сущность, с которой вы можете экспериментировать в реальном времени, пробовать разные подходы и сценарии, не останавливая её работу. Можете сразу видеть, к каким изменениям приводят ваши действия. Тем самым совмещая этапы разработки и тестирования, а также исключая лишние слои абстракции и компилирование. Образно говоря: теперь вы можете просто открыть капот и "покапаться" в работающем движке, вместо: смотрим/рисуем/правим чертежи -> заказываем новый экземпляр авто(на автозаводе) -> наблюдаем за работой -> возвращаемся к чертежам". 
  Особенно мощь динамичного программирования проявляется при использовании совместно с DSL'ным и/или графическим представлением(последнее более наглядно, но графику сложнее редактировать и потому, она пригодна только для визуализации небольших программ или их частей). Благодаря такому подходу, прикладной программист будет видеть и редактировать понятное ему описание и/или изображение модели программы, а не абстрактную конструкцию из компонентов. Представьте: вы конструирует программу в реальном времени, например для бух. учёта. Вы вводите сточку кода "add nameField on MyForm" и нажимаете ввод. Эта сточка сразу же превращается(интерпретируется) в конструкцию из пары компонентов, которые встраивается прямо в работающую программу. Первый из них добавляет на форму поле ввода и содержит логику работы поля, второй организует логику преобразования введённых данных к нужному виду и прочие действия с ними. При этом новое поле сразу же появляется на открытой форме, готовым к работе. Если что-то пошло не так, новая часть программы разрушается, а код её конструирования в редакторе отмечается как не правильный. Так-же весь этот процесс (компонентная конструкция программы) может быть визуализирован, чтобы дать программисту лучшее представление о том что сейчас происходи внутри. 
  Этот подход также открывает широкие перспективы для автоматизации развёртывания и адаптации приложения под текущие потребности пользователя. Например при запуске приложение может проанализировать окружение(что за система, какие приложения установлены, какие запущены в данный момент etc.), и выбрать наиболее оптимальную начальную конфигурацию(набор загруженных компонентов, положение и набор окон, etc.). Может загружать компоненты когда они нужны и выгружать когда нет(например, подгружать компонент проверки орфографии только тогда кода он нужен). Может подгружать и/или обновлять компоненты из интернета, прямо во время работы. И так далее и тому подобное.
  Назначив один(или несколько) компонент супервизором, можно при необходимости в ручную(например из командной строки) управлять приложением и/или изменять его конструкцию. К примеру, делать "кровавый патчинг": например, при эксплуатации нагруженного сервера было обнаружено, что некоторые виды запросов приводят к разрушению компонента-обработчика и потери данных. В качестве временной меры(пока проблема не будет исправлена) можно быстро написать компонент фильтр, отсеивающий запросы приводящие к краху, и включить его между компонентом принимающий запросы и компонентом-обработчиком, не останавливая работу сервера.
  Можно автоматизировать тестирование новых компонентов. Например, приложение, скачав новый компонент, сначала подключит его к компоненту-тестеру, который прогоняет серию тестов, или даже "скармливает" новому компоненту реальные данные и, если это обновление, сверяет результат с предыдущей версией. Если всё в порядке, компонент встраивается.
Эволюционная разработка
  Суть этого подхода в том что, программа разрабатывается в цикле с небольшими итерациями, в каждой итерации становясь всё ближе к поставленным требованиям, причём сами требования на каждой итерации уточняются(т.е. в принципе процесс может быть бесконечным). Эволюционная разработка доказала свою полезность на практике, в проектах среднего размера с фиксированными требованиями, и является неизбежной в проектах с плавающими и неопределёнными(например, исследовательских) требованиями. К сожалению этот подход не свободен от недостатков, главный из которых - постепенное возрастание энтропии кода(разрушение структуры программы), т.е. превращение кода в жуткую кашу, которую легче переписать с нуля чем разобраться в ней. Этот недостаток ограничивает применение эв. подхода средними проектами с непродолжительным сроком жизни.
  Наиболее эффективный и экономически выгодный способ сдержать разрушение структуры это постоянный рефакторинг(ещё, например, можно просто не допускать деградированния, для чего на каждой итерации полностью верифицировать всю программу, но это дороже). Однако современные ЯП(в частности поддерживающие ООП), склоняют программиста к созданию сложных и запутанных программ с огромным количеством беспорядочных, неуправляемых связей и как следствие зависимостей между объектами(возможно, потому что эти ЯП проектировались под каскадный подход), что в свою очередь делает рефакторинг очень сложным и дорогим.
  Вероятно, дороговизна рефакторинга была и есть одной из главных причин появления и развития компонентного подхода. Сегодня существует довольно много компонентных ЯП и технологий, например Oberon, COM, JavaBeans и прочие). Однако эти реализации не способны в полной мере раскрыть мощь компонентного подхода, так как являются всего лишь надстройками над ООП, т.е. всего лишь вносят дополнительную сложность.
  Переход к чистому компонентному программированию сделает эволюционную разработку ПО более простой и быстрой, а также снимет ограничение на размер и срок жизни приложений. Потому что:
1)В компонентном приложении все взаимосвязи всех компонентов упорядочены в интерфейсы и управляемы(об этом ниже). Это означает что ни один из компонентов не может бесконтрольно обращаться к другим(только через строго определённые интерфейсы), а также что  каждый компонент знает с "кем" он в данный момент связан и может свободно управлять своими связями. Что даёт возможность свободно добавлять/заменять/удалять компоненты(части приложения), иногда даже не останавливая его работу.
2)Представление приложения как конструкции из компонентов(а не как простыни кода) "развяжет руки" разработчикам, в том смысле что позволит избавится от мыслей вроде "чооорт, если мы добавим эту фичу нам придётся переписать 100500 строк кода!". Это сделает простым и естественным добавление новых и удаление старых(рудиментарных) частей программы, позволит свободно экспериментировать. Иначе говоря это ускорит эволюцию программы, и как следствие уменьшит затраты на разработку.
3)Рефакторинг проще и дешевле! Можно просто заменять/удалять компоненты(или даже целые кластеры компонентов) целиком не затрагивая остальные компоненты и, заботясь лишь о совместимости важных интерфейсов(об интерфейсах ниже). 
4)Компоненты "изолированы" друг от друга. Это означает что можно легко распределить работу над программой между несколькими разработчиками(или командами), при этом каждому из них не нужно будет изучать всю программу целиком и тщательно следить за тем что делают другие. Что позволит разработчику сконцентрироваться именно на своей части работы и, сделать её быстрее и качественнее.
  В крайнем случае, в целях экономии средств, можно вообще забить на рефакторинг и значительно ослабить контроль над разработкой, как бы позволив программе "жить своей жизнью". В это случае в команде будет отсутствовать архитектор/главный разработчик(т.е. тот кто знает весь проект целиком), дизайнер, тестеры etc., а разработка будет вестись методом "скотча и суперклея"(разумеется это очень плохой подход, но он может быть использован только, например, в условия острого дефицита бюджета(маленькая фирма) или в опенсорс проектах). Даже в таких условиях КОП значительно продлит жизнь программы, и позволит сравнительно не дорого привести проект в "адекватное" состояние.
How it works
  Эта глава посвящена собственно компонентно-ориентированной парадигме, а точнее её версии, разработанной в рамках проекта RS/C. Эта версия несколько отличается от остальных(известных мне) и наиболее похожа на MS COM (да-да, фатальный недостаток в COM был исправлен :). Также сильное влияние оказали такие ЯП как Scala, Erlang, Python, и в некоторой степени Pascal. Я бы хотел чтобы вы воспринимали КОП как усовершенствованное ООП, чем оно по сути и является. Так как КОП разрабатывалось как замена ООП, области их применения практически полностью совпадает.   
  Далее о собственно парадигме, в виде списка понятий и идей, это правила(возможности/ограничения) по которым "строятся" компонентные приложения. В главе "примеры" ниже, вы найдёте несколько примеров архитектур программ построенных с использованием КОП.
Компоненты
  Компонент (как и объект в ООП) является основной единицей(квантом) для построения программы, т.е. программа в КОП есть не что иное как конструкция из компонентов. Каждый компонент составляющий программу имеет следующие характерные свойства:
*.Все компоненты создаются рантайм(из прототипов), в отличии от например грязного ООП, где есть статик классы. Соответственно у каждого компонента может быть функция-конструктор и/или функция-деструктор.
*.Компонент это чёрный ящик, с точки зрения использующего его программиста. Это означает что компоненты следует разрабатывать так чтобы у его пользователей не возникала необходимость заглядывать в исходный код.
*.Компонент обладает поведением и состоянием, т.е. он реагирует на внешние воздействия и эта реакция зависит от его внутреннего состояния. При необходимости, может быть реализован компонент, обладающий только состоянием или только поведением.
*.Компоненты являются активными сущностями, это не просто пассивные ООП-объекты, они могут содержать внутренние потоки выполнения, этим они похожи на полноценные программы. Компоненты сами определяют своё поведение в зависимости от окружающих условий, например, могут создавать другие компоненты и управлять ими или, например, могут самоуничтожатся, в случае если "сочтут" это необходимым.
*.Компоненты имеют срок жизни, и могут в любой момент "умереть". Об этом следует помнить при проектировании компонентов и программ из них.
*.Компоненты не имеют типов/классов, типы есть у интерфейсов компонентов.
*.Компонент, нельзя передавать другому компоненту(в отличии от ООП-объекта) в функцию, помещать в переменную и т.п.(всё это можно делать с хендлом(см. ниже) компонента). Компоненты как бы размещаются на одном уровне, т.е. не могут быть вкладываемыми или содержать другие компоненты(это могут сборки, описанные ниже).
*.Компонент имеет один или более отдельных интерфейсов. Интерфейс это единственный "легальный" способ взаимодействия компонентов. Вторая задача интерфейса - скрыть реализацию(внутреннее устройство) компонента. Через интерфейсы компоненты обмениваются значениями(более подробно об этом в следующей заметке).
  Образно можно представить компоненты как цифровые микросхемы, обменивающиеся сигналами(значениями):
  Но как и ООП-объекты, компоненты это фактически не более чем структура в оперативной памяти(в куче).
Интерфейсы
  Вы все наверняка пользуетесь бытовой техникой, а многие, также водят какое либо транспортное средство. У всех этих(и не только) устройств есть интерфейс, от простого, из одной кнопки как у чайника, до сложного, как у персонального компьютера. Весь интерфейс устройства может быть разделён на несколько меньших интерфейсов, по признаку специализации, например у компьютера, есть интерфейс пользователя и интерфейсы для подключения других устройств.
  Также можно рассматривать интерфейс как контракт(договорённость), описывающий взаимодействия двух компонентов. Более подробно этот подход раскрыт в книге "Компонентное программное обеспечение", в главе 3.2.
  В КОП, каждый компонент может иметь один или несколько интерфейсов, через который будет взаимодействовать с другими компонентами. Каждый интерфейс имеет имя, которое необходимо знать или получить рантайм, чтобы использовать этот интерфейс.
  Образное представление мобильного телефона в виде компонента с именем "MyPhone":
Компонент "MyPhone" имеет четыре интерфейса: IUI(интерфейс пользователя), IPower(интерфейс для подключения зарядного устройства), IHeadphones(для подключения наушников) и IDock(для подключения дополнительных устройств).
  Интерфейс многих устройств, предполагает двухсторонний обмен, например, у интерфейса пользователя компьютера есть монитор и колонки для вывода информации и, клавиатура и мышь для ввода. Также, интерфейсы часто состоят из многих, более мелких, частей, например, клавиатура компьютера состоит из отдельных кнопок.
  В КОП интерфейс состоит из полей - методов и свойств(как интерфейс класса в ООП), и разделён на две части: секция экспорта и секция импорта. В первой перечислены методы и свойства которые будут доступны другим компонентам для вызова и чтения соответственно. Во второй поля, которые может вызывать и читать компонент. Интерфейсные классы из ООП от части похожи на КОП интерфейсы, это вложенные классы которые имею доступ к членам внешнего класса, ссылка(в КОП имя) на объект такого класса передаётся другому объекту и он может вызвать методы интерфейсного класса, аналог этого в КОП, секция экспорта, содержащая методы которые будет вызывать компонент подключившийся к интерфейсу. В отличии от ООП в КОП это всё строго упорядочено и управляемо, а потому более безопасно.
  Образное представление интерфейса пользователя мобильного телефона:
Красным - импортируемые: свойство inSound - входящий аудио поток, "читаемый" телефоном, и метод vibration - виброзвонок "вызываемый" телефоном. Синим - экспортируемые: методы кнопки(pushCallButton, pushEndButton, pushNumbersButton, pushNavigationButton, pushExtendedButton), "вызываемые" пользователем, и свойства outSound - исходящий аудио поток и screen - вывод на экран, "читаемый" пользователем.
  Интерфейсы в КОП всегда имеют тип. Тип это описание полей интерфейса. Типы интерфейсов размещаются в библиотеке типов, которая при необходимости может быть отмечена GUID(как в .NET), что сделает типы статистически уникальными. Интерфейс может быть полиморфным(несколько интерфейсов с одинаковым именем но разным типом). Типы без GUID могут быть использованы только в пределах видимости компилятора, т.е. там где возможна проверка компилтайм(в пределах пакета). С типом интерфейса связывается описывающая интерфейс документация и/или тестирующий компонент.
  Чтобы взаимодействовать между собой, компоненты должны иметь комплементарные интерфейсы. Это два интерфейса определённых при помощи одного типа интерфейса, но у второго секции импорта и экспорта меняются местами. Т.е. то, что экспортирует один интерфейс, импортирует второй, и наоборот.
  Образное представление мобильного телефона и подключённого к нему сетевого адаптера:
IPower - подключённый интерфейс; IACoutlet - подключённый полиморфный интерфейс.
  Прежде чем компонент сможет обратится к какому либо из полей секции импорта, комплементарные интерфейсы необходимо подключить(объединить), это выполняется рантайм (оператором "connect"). Попытка обратится к полю импорта, не подключённого интерфейса, приведёт к исключению. При этом к секции экспорта можно обращаться в любой момент, так как они размещаются "на стороне" компонента. При подключении интерфейсов, не проверяемых компилтайм, выполняет проверка(сравнение) сигнатур(включающих описание типов и GUID библиотеки типов), в случае не совпадения - исключение. Также для полиморфных интерфейсов при подключении выполняется выбор интерфейса с комплементарным типом(если оба интерфейса полиморфные, подключаются все комплементарные). По завершении взаимодействия компонентам необходимо разорвать соединение(оператором "disconnect"). Соединение может быть прервано и автоматически, например в случае возникновения не обрабатываемого исключения(приводит к разрушению компонента) в подключённом компоненте. Похожий механизм есть в Qt, там рантайм можно подключать слот к сигналу.
  К каждому интерфейсу может быть прикреплены функции-обработчики подключения и/или отключения. Обработчик подключения позволит компоненту, например выполнить какую либо подготовку, или отказать в подключении(к примеру, из-за не соответствия версии). Обработчик отключения позволит, например, очисть более не нужные ресурсы.  
  Тип интерфейса всегда фиксированный(как в MS COM), т.е. будучи однажды объявленным он не должен изменяться, нельзя добавлять и тем более изменять/удалять поля интерфейса. Несомненно, плохо, что нельзя расширять интерфейс, тем не менее это решение позволяет:
*.Упростить программную модель: рассмотрение интерфейсов идёт на уровне собственно интерфейсов, т.е. не важно что внутри интерфейса(например: это старый интерфейс, это новый интерфейс(2 шт.)), а не на уровне полей(например: это старые поля, это новые поля(N+N шт.)).
*.Как угодно редактировать интерфейсы, без оглядки на обратную совместимость. Создавая новый интерфейс, можно изменить вообще все поля, выкинуть устаревшие и т.п., решение проблем совместимости ложится на компонент-адаптер, имеющий старый интерфейс (который также может реализовывать устаревший функционал, выброшенный из основного компонента). Спустя какое-то время(необходимое пользователям для перехода на новый интерфейс), адаптер можно выкинуть. Тогда как при подходе на уровне методов необходимо оставлять все старые методы, если начать их изменять/выкидывать это потребует введение контроля версий интерфейса, и сильно усложнит жизнь его пользователям.
  Образное представление обновления телефона до мобильного телефона:
  Интерфейсы не могут вкладываться друг в друга. В принципе, технически это довольно просто реализуется, но я решил что это слишком усложнит модель при минимальном выигрыше, потому отказался от этого. К тому-же вкладываемые интерфейсы сильно усложнят графическое представление, что не есть хорошо.
  В коде интерфейсы будут выглядеть примерно так((!) черновая версия синтаксиса, без сахара):
  /**** Type library ******************************************************************/
    ...
    Foo FUNC Int :> Bool                                                               //Тип функции.
    Sruct UNION{myFoo Foo, myInt Int}                                                  //Тип структуры.
    TInterfase INTERFACE Sruct | myBool Bool                                           //Тип интерфейса.
    ...
  /**** Component A *******************************************************************/
    ...
    IInterfase(im-ex) TInterfase{,true}                                                //Определение интерфейса
    ...                                                                                //в прототипе компонента "A".
    try:
      connect IInterfase to B                                                          //Подключение к компоненту
    except:                                                                            //"B" обёрнутое обработчиком
      /*some do*/                                                                      //исключений.
    s = IInterfase.Sruct.myFoo(123)                                                    //Вызов импр. метода.
    s := IInterfase.Sruct.myInt                                                        //Чтение импр. свойства.
    ...
  /**** Component B *******************************************************************/
    ...
    IInterfase(ex-im, connection connecpProc) TInterfase{{{ar)/*some code*/(rz},100},} //Опре. интерфейса в "B",
    ...                                                                                //содержащее тело метода.
    connecpProc Connection{{ar)                                                        //Функция-обработчик
      if IInterfase.myBool == true:                                                    //подключения, если "myBool"
        rz := true                                                                     //не "true" - отвергнуть.
      else:
        rz := false
    (rz}}
    ...
  /************************************************************************************/
Bool, Int, Connection - базовые типы; FUNC, UNION, INTERFACE – супертипы; im-ex, ex-im, connection - параметры.
(!)В этом примере вы вероятно найдёте много не понятного, но я привёл его, чтобы показать, как это будет выглядеть в общем, детальнее об этом в следующей заметке.
  Возможно, вы зададитесь вопросом: к чему все эти пляски с бубном интерфейсами и коннектами? Не лучше ли сделать как в чистом ООП или модели акторов "только объекты и сообщения", и оставить возможность обращаться к полям без подключения? Не лучше! Так как:
*.Благодаря такому подходу организуется управление и упорядочивание взаимодействий компонентов. Если в ООП любой объект может послать сообщение любому другому, то тут прежде чем начать взаимодействовать с другим, компонент должен подключится к этому другому, а по завершении отключится, что даёт: Во первых, возможность другому компоненту подготовится к взаимодействию или даже отказаться от него. Во вторых, строгая типизированность и GUID интерфейса практически исключает вероятность случайного вызова "не того" метода. В третьих, даёт возможность другому компоненту узнать, когда взаимодействие будет завершено, и выполнить какие то действия. И в четвёртых это позволяет сделать компоненты гораздо более "инкапсулированными", нежели объекты, что увеличивает уровень абстракции.
*.Абсолютно точно известно когда компонент станет не нужен, можно утилизировать его сразу в этот момент, т.е. сборщик мусора не нужен (об это подробней с следующей главе, ниже).
*.Использование обработчиков событий интерфейсов позволяет связанным компонентам адекватно и своевременно реагировать на "внезапную смерть" партнёра.
*.Такая архитектура легко масштабируется и рефакторится, и также делает возможным "горячую" замену компонента.
*.Приложение может быть относительно легко масштабировано на насколько процессов или компьютеров, а также это значительно упрощается параллельное(многопоточное) программирование.
*.Эта арх. позволит автоматизировать взаимодействие компонентов, т.е., например, прикладному программисту будет достаточно написать "connect to new", а далее компоненты сами соединятся, настроятся, и начну взаимодействовать.
Сборки
  Задача сборки инкапсулировать некоторою конструкцию из компонентов/сборок. Сборка "с наружи" выглядит как обычный компонент, также имеет интерфейсы, создаётся/разрушается рантайм и т.п. Создание сборок это уже работа на уровне компонентов, а не на уровне их внутреннего устройства, т.е. оно сводится к созданию необходимого набора компонентов и конфигурации их подключений(проще говоря - композиция компонентов). КОП-композиция несколько отличается от ООП, здесь пользователь сборки ничего не значит о её внутреннем устройстве, обращаясь с ней как с обычным (атомарным) компонентом. Интерфейсы сборки это некоторые (специально назначенные) из интерфейсов составляющих компонентов, причём доступны только эти интерфейсы, попытка обратится к составляющим компонентам не через них приведёт к исключению.
  Образное представление, внутреннего устройства мобильного телефона:
  Определение(прототип) сборки мобильного телефона, в коде, будет выглядеть примерно так((!) черновая версия синтаксиса, без сахара):
  /**** Assembly *******************************************************************/
    MobailPhone ASSEMBLY(                                                           //Прототип сборки,
      UI/IUI(root),                                                                 //и список внешних
      ChargingController/IPower,                                                    //интерфейсов.
      SoundBlaster/IHeadphones,
      CPU/IDock
    ){                                                                              //Далее последовательность
     new CPU name CPU                                                               //конструирования сборки.
     new Charget name Charget
     connect ICharget of Charget to CPU
     new Video name Video
     connect IVideo of Video to CPU
     new Kaybord name Kaybord
     connect IKaybord of Kaybord to CPU
     new Audio name Audio
     connect IAudio of Audio to CPU
     new Radio name Radio
     connect IRadio of Radio to CPU
     new UI name UI
     connect IVideoStream of UI to Video 
     connect IDirection of UI to Kaybord
     connect IAudioStream of UI to Audio
     new Battery name Battery
     connect IBattery of Battery to Charget
     connect IPower of Battery to UI
     connect IPower of Battery to Video 
     connect IPower of Battery to CPU
     connect IPower of Battery to Kaybord
     connect IPower of Battery to Audio
     connect IPower of Battery to Radio
    }
  /**** Use(somewhere in component "User") *****************************************/
    ...
    phone = new MobailPhone                                   
    adapter = connect ILine of ACoutlet tonew Adapter
    connect IPower of adapter to phone
    ...
  /*********************************************************************************/
(!)В этом примере использовано определение прототипа и работа с хендлами, описанные ниже.
Прототипы
  Прототип есть не что иное, как текстовое описание компонента или сборки. Я использовал этот термин, так как хочу обратить ваше внимание на то что конструируется/описывается именно компонент, а не класс или тип компонента, и экземпляры создаваемые рантайм будут как бы копией созданного вами компонента-прототипа. Иначе говоря, прототипы в КОП гораздо менее важны, чем классы в ООП, и как бы отступают на второй план. Прототипы поддерживают наследование реализации(будет похоже на наследование в Scala'е), в том числе (в некоторой степени) и множественное.
  Прототип это как бы чертёж(описание) компонента:
  Как и ООП класс, в общем случае прототип выглядит как структура(но не является ею), в коде он будет выглядеть примерно так((!) черновая версия синтаксиса, без сахара):
  /**** Prototype ****************************************/
    CompB(import MyLib/_) COMPONENT(extends CompA){       //Заглавие.
      Constructor{{)/*construction self*/(}}              //Конструктор.
      d Deconstructor{}                                   //Деконструктор.
      IInterfase(im-ex, root) TInterfase{}                //Корневой интерфейс.                   
      IToInterfase(im-ex) TInterfase{}                    //Интерфейс.
      h Doer{{)/*some do*/(}}                             //Вызывается после завершения
      Doer{{)                                             //конструирования.
        d := {)/*deconstruction self*/(} Deconstructor    //Присваивание деконструктора.
        f := {a,b) r := a + b (r} Foo                     //Присваивание функции.
        s = f(100 Double,i)                               //Вызов функции.
        h()                                               //То-же
      (}}
      f Foo{}                                             //Пара обычных
      i Int{123}                                          //переменных(контейнеров).
    }
  /**** Use **********************************************/
    ...
    C = new CompB
    connect IToInterfase to C
    ...
  /*******************************************************/
Constructor, Deconstructor, Doer, Double, Int - базовые типы; COMPONENT - супер тип; TInterfase, Foo - пользовательские типы(из MyLib); import, extends, im-ex, root - параметры.
  Прототипы объединяются в пакеты, как и классы в Java и .NET(по началу, я, вероятно, буду использовать Java пакеты). Если компонент это квант разработки, то пакет - это квант компиляции и распространения. Приложение переставляет из себя: набор пакетов, рантайм(необходимые для запуска DLL и EXE) и файл конфигурации, кроме прочего содержащий последовательность конструирования приложения(такая же как у сборок). Более подробно о прототипах в следующей заметке.
Погружаемся глубже
  Здесь более подробно описаны некоторые аспекты реализации, потому если вас не сильно заинтересовало КОП, можете пропустить это главу. Я не буду слишком углубляться в внутреннее устройство компонентов, пока считайте это некой магией (я, чесно-чесно, хотел описать это здесь, однако, за время подготовки материала, заметка так разбухла что стала похожа на небольшую книгу, потому я решил разбить её на две части).
  В отличие от ООП, где всё объекты, КОП разделена как бы на два уровня: верхний уровень это собственно компоненты, составляющие программу, и нижний это значения, составляющие компоненты(на самом деле всё несколько сложнее, но сейчас это не важно). Значения это не компоненты и не объекты, это обычные данные(числа, строки etc.) и лямбда функции(обычные ФП функции). Значения могут как угодно "перемещаются" внутри компонента и за его пределы(исключая функции завязанные на контекст компонента, например обращающиеся к его интерфейсам, такие функции не могут "покинуть" компонент) через интерфейсы. Внутри компонентов всё строго типизировано, рантайм проверяется только выход за пределы массивов при обращении по индексу(в случаях когда не возможно проверить компилтайм). Такой подход позволит сделать компоненты более быстрыми и компактными, хотя и несколько усложнит их разработку. В принципе ничего не мешает использовать внутри компонента ООП-объекты(как в MS COM), а не простые значений, но я счёл что, это чрезмерно усложнит программную модель и отказался от такого подхода.
  В целом система будет построена на подобии .NET(возможно даже я использую его в качестве бэкэнда), т.е. исходный текст(на одном или нескольких ЯП) транслируется в байт-код, который затем, на машине пользователя, компилируется бинарный код.
  Можно условно выделить три уровня:
1)Фронтэнда - уровень исходного текста, который собственно пишет программист. Исходный текст транслируется в байт-код(ассемблер). Байт-код затем компилируется в бинарный код целевой платформы(бэкэнда).
2)КОП-платформа(называется "Skidbladnir") - это абстрактный слой, скрывающий аппаратную или программную платформу, т.е. в идеале программист работает только с компонентами, как например Java-программист работает только с объектами. Для обеспечения работы платформы будет использоваться небольшой рантайм, являющийся не более чем библиотекой с функциями(а не виртуальной машиной).
3)Бекэнд - собственно платформа на которой выполняется программа(натив, JVM etc.).
Динамические и статические компоненты
  Несмотря на то, что все компоненты создаются рантайм, они делятся на динамические и статические. Эти два вида компонентов отличаются только способом создания. Статические это те что создаются при старте приложения/сборки, т.е. при его начальном конструировании, они существуют(доступны) всё время работы приложения(разрушение хотя-бы одного статического компонента == разрушение всего приложения). Динамические компоненты создаются/разрушаются по мере необходимости уже во время работы программы. Образно говоря стат. компоненты это "скелет" приложения, динам. компоненты это "мясо". Статические компоненты дополнительно имеют имена(динамические только хендлы), что позволяет работать с ними более удобно.
  Такой подход позволит несколько упростить разработку. При разработке архитектуры приложения, можно будет сначала выделить базовые, не изменяющиеся компоненты, собрав из них как бы каркас приложения. А далее, на этом каркасе, продолжить динамически собирать приложение, т.с. "доводить до ума".  
  Статические компоненты также используются внутри сборок(описанных выше), образуя внутренний каркас. В этом плане сборка отличается от приложения лишь тем что имеет интерфейсы "выведенные" во вне, и тем что разрушение статического компонента приводит только к разрушению сборки, а не всего приложения(разумеется если сборка не была статическим компонентом приложения). Похожесть сборок внутри на приложения, а снаружи на обычные компоненты, делает их удобным решением для реализации плагинной подсистемы приложения. 
  Пример архитектуры простого MVVM приложения:
Чёрным - статические компоненты; зелёным - динамические. Frame - компонент управляющий главным окном и размещёнными на нём элементами; TabController - компонент управляющий панелью вкладок и компонентами вкладок; Tab - компонент управляющий одной вкладкой; Mediator - компонент-посредник, подготавливает данные для отображения на вкладке, а также обновляет изменённые данные в базе; MediatorController - компонент управляющий посредниками, его задача после создания вкладки создать и сконфигурировать посредника; Database - компонент-база данных.
Корневой интерфейс
  Один из интерфейсов компонента должен быть отмечен параметром "root", это интерфейс называется корневым или основным. Он был введён в модель КОП для решения проблемы управления памятью(описано ниже), и для того чтобы сделать КОП программирование более похожим на ООП.
  Часто при создании компонента(оператором "new"), его конструктору необходимо передать какие-то данные для инициализации. В ООП для этого используются параметры конструктора, однако в КОП, где все взаимодействия выполняются через интерфейсы, это выглядело бы как костыль. Корневой интерфейс подключается ещё до выполнения конструктора, что позволяет создаваемому компоненту взаимодействовать с создающим(или каким-то третьим компонентом) уже на этапе конструирования.
  В коде, это будет выглядеть примерно так((!) черновая версия синтаксиса, без сахара):
  /**** Type library **********************************************************/
    ...
    Sruct UNION{myInt Int, myBool Bool}                                        //Тип структуры.
    TInterfase INTERFACE Sruct |                                               //Тип интерфейса.
    ...
  /**** Component A ***********************************************************/
    ...
    IRootInterfase(im-ex, root) TInterfase{}                                   //Определение корневого интерфейса.
    Constructor{{)                                                             //Конструктор.
      if IRootInterfase.myInt != 100:                                          //Если "myInt" не 100,
        rz := false                                                            //то отменить создание компонента.
    (rz}}                                              
    ...                                                                               
  /**** Component B ***********************************************************/
    ...
    IRootInterfase(ex-im) TInterfase{}                                         //Интерфейс, комплементарный 
    ...                                                                        //корневому из "A".

    IRootInterfase.myInt := 100                                                //Установка свойства.
    IRootInterfase.myBool := true                                              //Установка свойства.
    С = new A                                                                  //Создание компонента.
    ...
    ...
    C = new A(100, true)                                                       //Вариант с сахаром(комплементарный 
    ...                                                                        //добавится автоматически компилятором).
  /****************************************************************************/
Множественные подключения
  К интерфейсу компонента может быть подключено несколько комплементарных интерфейсов других компонентов, это и есть множественное подключение. Такое может пригодится, например, в случае если несколько компонентов требуют однотипного взаимодействия. Чтобы разрешить(по умолчанию запрещены, из соображений безопасности) м. подключения необходимо отметить интерфейс параметром "multiple".
  Кусочек кода из простого MVVM приложения, описанного выше((!) черновая версия синтаксиса, без сахара):
  /**** Component TabController *********************************************/
    ...
    ITabController(im-ex, multiple) TTabController{}                         //Интерфейс, для м. подключений.
    ...
    new Tab                                                                  //Новая вкладка.
    new Tab                                                                  //Ещё одна.
    S = new Tab                                                              //Третья(с получение хендла).
    ...                                                                               
    ITabController.show("Hi!")                                               //Метод "show" будет вызван
    ...                                                                      //у всех трёх вкладок.
    S.ITabController.show("Bye!")                                            //Метод "show" будет вызван
    ...                                                                      //только у последней.               
  /**** Component Tab *******************************************************/
    ...
    ITabController(ex-im, root) ITabController{{S) print(S) (}}              //Интерфейс, с реализацией "show"
    ...                                                                               
  /**************************************************************************/
Хендл компонента
  Хендлы это скорей костыль, созданный для случаев когда необходимо сделать известным один компонент внутри другого. В ООП в таких случаях передаётся ссылка. Я использовал термин "хендл" так как он больше подходит(хендл это как-бы "представитель" компонента), чем термин "ссылка"("нечто ссылающееся"). Предположительно, хендлы будут использоваться гораздо-гораздо реже чем ссылки в ООП.
  Хендл это значение, т.е. как и обычные значения(например типа Int) хендлы можно помещать в переменные, списки, передавать через интерфейсы и т.п. Хендлы похожи на структуры, и содержат список доступных(через посредство хендла) интерфейсов.
  Образное представление хендлов как списков интерфейсов, связанных с компонентом:
  В качестве типа хендла(например для определения переменной или приведения типа) может быть использовано имя прототипа, в этом случае через хендл будут доступны все интерфейсы имеющиеся у компонентов созданных с этого прототипа. Так-же можно определить тип хендла при помощи супертипа "HANDLE", включив только необходимый набор доступных интерфейсов.
  Хендлы могут безопасно приводится с "понижением" типа(это когда тип к которому приводится хендл не содержит некоторых интерфейсов), и не безопасно с повышением типа(когда тип к которому приводится хендл содержит хоть один новый интерфейс), в последнем случае выполняется рантайм проверка наличия у компонентов необходимых интерфейсов, которая может закончится исключением. В целом эти операции эквивалентны таковым в ООП, но вместо набора полей, набор интерфейсов.
  После разрушения компонента его хендлы продолжат существовать до тех пор пока не разрушатся "естественным" образом(как разрушаются прочие значения(затирание, разрушение с контекстом вызова etc.)), обращение через такой, теперь уже не связанный, хендл приведёт к исключению.
  Пример с хендлами((!) черновая версия синтаксиса, без сахара):
  /**** Type library ******************************************************************/
    ...
    TIOne INTERFACE Int | met Foo                                                      //Типы интерфейса.
    TIToo INTERFACE Bool | Char                                                         
    THLow HANDLE{I1 TIOne}                                                             //Типы хендлов.
    THHigh HANDLE{I1 TIOne, I2 TIOne, I3 TIToo}
    ...
  /**** Component A *******************************************************************/
    ...
    I1 TIOne{}                                                                         //Компонент с 4 интерфейсами.
    I2 TIOne{}
    I3 TIToo{}
    I4 TIToo{}                                   
    ...                                                                               
  /**** Component B *******************************************************************/
    ...
    С = new A                                                                          //Создание компонента.
    S = C THHigh                                                                       //Копирование с понижением типа.
    S := S THLow                                                                       //Понижение
    try:
      S := S THHigh                                                                    //Не безопасное повышение типа.
    ...
    try:
      S := S A                                                                         //Повышение до полного типа.
    ...
    connect I2 to S                                                                    //Исп. хендла для подключения.
    S.I2.met()                                                                         //Исп. хендла для вызова метода.
  /************************************************************************************/
Управление памятью
  С самого начала одной из задач было избавится от "мусора" и как следствие от сборщика мусора(концепция "экологического" программирования: "чисто не там где убирают..."). Так как, использование сборщика и сам "мусор" ведут к не оправданной растрате ресурсов(память, процессорное время), и прочим. Не смотря на то что, мне так и не удалось, пока, достичь ООП-шного "создал-забыл", некоторый прогресс есть.
  Основная идея - когда компонент перестаёт быть нужным, он разрушается. Проблема в том как "словить" момент не нужности. В ООП для этого выполняется отслеживание ссылок, когда ни одной из них не остаётся - объект разрушается. Я решил отслеживать подключения интерфейсов, то есть когда ни один из интерфейсов компонента не подключён, компонент считается не нужным и разрушается. Однако такой подход не решает проблему перекрёстных подключений(как проблема перекрёстных ссылок в ООП), когда два или более компонента связаны друг с другом, но уже не используются в программе. Решением этой проблемы стало разделение компонентов на статические и динамические, и введение корневого интерфейса.
  Статические компоненты существуют всё время работы программы/жизни сборки и являются как бы её скелетом. Статические компоненты могут создавать динамические, подключая их к себе корневыми интерфейсами.
Динамические компоненты, в свою очередь, также могут создавать другие динамические, подключая их корневые интерфейсы к себе(или к другим статическим/динамическим компонентам). После подключения корневой интерфейс не может быть отключён, и к нему не могут быть подключены дополнительный интерфейсы(не поддерживает множественные подключения). Таким образом, динамические компоненты, своими корневыми интерфейсами самоорганизуются в дерево, без перекрёстных ссылок, корнем которого является статический компонент.
 
Чёрным - статические компоненты; зелёным - динамические; тёмно-синим - корневые интерфейсы. Вверху справа - разрушающаяся ветка из 3-х компонентов.
  Отключение корневого интерфейса приводит к разрушению компонента(и наоборот, разрушение компонента (по исключению или саморазрушение) приводит к отключению корневого интерфейса), что в свою очередь приводит к отключению других интерфейсов компонента. Если к компоненту были подключены корневые интерфейсы других компонентов, это приводи к их разрушению, и так далее, т.е. вся ветка дерева компонентов "отмирает".
  В общем, по сравнению с ООП программисту нужно прикладывать несколько больше усилий для управления памятью. В частности проектировать компоненты так чтобы они сами знали когда становятся не нужны и саморазрушались, и/или не забывать отключать интерфейсы после их использования(последнее в прочем может быть автоматизировано, например как в ООП: когда поток выполнения покидает зону видимости, в которой интерфейс был подключён, выполняется автоматическое отключение). Однако думаю, эти усилия окупятся более стабильной и экономичной работой приложения.
Шлюзы
  Компоненты не могут "существовать в вакууме", так как в этом случае от них не будет ни какой пользы, т.е. они должны как-то взаимодействовать с окружением. Образно можно представить компонентное приложение как конструкцию из компонентов находящуюся внутри коробки - Win-процесса. Кроме конструкции из компонентов в коробке есть ещё системные и пользовательские DLL-библиотеки функций, и возможно, какие то специальные регионы данных. Также там находится КОП-рантайм, обеспечивающий работу компонентов. Кроме того если приложение на JVM/.NET, там находится собственно виртуальная машина или JIT, и классы/типы. Всё это и есть окружение компонентов (с их "точки зрения").
 
  Компоненты взаимодействуют со средой через шлюзы. Шлюз это специальная функций, с точки зрения использования она не отличается от обычных функций, однако определяется иначе. Шлюз, в отличии от обычной функции, имеет несколько тел, каждое из которых является реализацией шлюза для одной из платформ(или нескольких похожих), на которых предполагается работа приложения, это похоже на используемые в Си макросы, подставляющие кусок кода в зависимости от того под какую платформу собирается программа. По сравнению с обычными функциями, в шлюзе дополнительно возможно получить прямой доступ к адресному пространству процесса, использовать ассемблер и т.п. Так-же можно получать адрес точки входа этого или другого шлюза(в пределах области видимости имён), для реализации callback процедур. Так как шлюзы являются опасными элементами языка, потенциально способный нарушить работу других компонентов и всего приложения, их нельзя определять где попало, а необходимо выносить в библиотеки, отмеченные параметром "unsafe".
  Образное представление шлюза, для доступа к сотовой сети:
  По моему мнению, такой подход лучше чем использование виртуальной машины(изолированной среды, как в Java например), так как проще отладить несколько небольших кусочков кода отдельно для каждой платформы, чем весь код при переходе на другую реализацию ВМ.
Примеры
  Здесь я опишу несколько примеров использования КОП в некоторых типичных задачах. К сожалению пока нет возможности подкрепить их кодом.
Веб-сервер
  Пример архитектуры простого HTTP-сервера, с сессиями:
Светло-зелёным - динамические компоненты, фиолетовым - статические, чёрным - коневые интерфейсы.
  Компонент Connections принимает запросы на подключение(слушает порт) и управляет подключениями, получив запрос он создаёт новый экземпляр компонента Connection и передаёт ему через IConnection данные из запроса, необходимые для установки соединения.
  Если Connection удалось установить соединение, он получает данные GET-запроса и передаёт их через IParser компоненту GETParser, иначе разрушается.
  GETParser разбирает GET-запрос, выясняя что необходимо передать в ответ(например, HTML документ, изображение etc.). Определив что требуется, GETParser создаёт соответствующий экземпляр компонента-генератора и подключает(IData-IDoc) его к Connection. Если необходимо GETParser выделяет из GET-запроса дополнительную информацию(например, параметры для PHP) и передаёт её созданному компоненту через IParameters. Если GETParser'у не удаётся разобрать запрос или возникает какая-то другая ошибка, он создаёт экземпляр компонента Message, и передаёт ему код ошибки через IParameters. Этот компонент генерирует HTML страницы со стандартными сообщениями. 
  Компонент-генератор подготавливает данные для ответа сервера, используя в случае необходимости базу данных(компонент Database), по завершении данные передаются компоненту Connection через IData-IDoc, который в свою очередь передаёт их клиенту. В случае неудачи компонент-генератор может использовать компонент Message (создав и подключив его к Connection) для генерации сообщения или сделать это самостоятельно. По завершении работы компонент-генератор разрушается.
  Соединение (компонент Connection) продолжает существовать до разрыва или таймаута, затем происходит отключение и компонент разрушается. В случае поступления нового GET-запроса до отключения, он отправляется компоненту GETParser, который обрабатывает его также как и первый.
  Если GETParser определяет что GET-запрос содержит данные идентификации пользователя, то дополнительно отправляет эти данные компоненту Session через IMenage. Session управляет сессиями(экземплярами компонента User). Получив данные, Session создаёт экземпляр User, подключает его к Connection(через IData) и передаёт ему данные идентификации через ISession.
  User подключается к Database и проверяет идентификатор пользователя, тем временем GETParser ожидает. В случае ошибки User генерирует страницу с соответствующим сообщением, отправляет её клиенту через Connection и разрушается, Session сообщает GETParser о неудаче и тот прекращает разбор запроса. Если авторизация прошла успешно GETParser продолжает разбор GET-запроса как обычно, но в этот раз подключает компонент-генератор к User а не к Connection.
  Компонент-генератор передаёт данные ответа компоненту User, который, в случае необходимости, добавляет информацию об/для авторизованного пользователя(в частности идентификатор сессии). Далее данные ответа как обычно отправляются клиенту через Connection. Компонент User разрушается только когда пользователь завершит сессию или по таймауту.
  Если GETParser определяет что GET-запрос содержит идентификатор сессии, то передаёт его компоненту Session, хранящему список текущих сессий. Если идентификатор корректный Session подключает соответствующий компонент User к Connection и сообщает GETParser об удаче, тот продолжает разбор GET-запрос, так-же как при создании новой сессии. В случае неудачи GETParser прекращает разбор GET-запрос и генерирует сообщение об ошибке(при помощи компонента Message, например). 
Окна
  На примере win-калькулятора(так как там много окон и практически нет логики), для видов "Standart" и "Programmer"(для простаты я описал только базовые возможности):
  В этом примере я использовал распределённую архитектуру, т.е. когда нет центрального/главного компонента, а логика/функционал "размазан" по нескольким компонентам.
  При старте приложения выполняется код начальной инициализации, конструирующий статические компоненты. Первым создаётся компонент Form, это главное окно, изначально скрытое. Затем создаётся Pahel, это компонент-контейнер и подключается к Form при помощи IWidgets-IWidget(этот интерфейс используется для организации компонентов-окон в дерево и обмена служебной информаций(например, win-хендлы, размеры, пармтры etc.)). За ним, аналогично создаётся ButtonsPanel, это компонент, унаследованный от Panel и дополненный интерфейсом IButtons, а также изменённой логикой размещения дочерних окон. Затем создаются компоненты Registry и Math, для работы с реестром и для математики соответственно. Последим, создаётся CalcMenu, и подключается к Form. На этом выполнение кода начально инициализации завершается.
  CalcMenu наследует компонент Menu(обычное win-меню), дополняя его интерфейсами IForm, IRegistry, IView, и логикой управления видом калькулятора, а также функциями чтения/сохранения параметров. Сразу по создании CalcMenu посредством компонента Registry, работающего с win-реестром, читает параметры сохранённые при прошлом запуске(вид и положение окна на рабочем столе). Допустим вид будет "Standart", а координаты 0х0. CalcMenu устанавливает координаты главного окна через IForm(интерфейс управления главным окном). Затем создаёт компонент StandartViev.
  StandartViev инкапсулирует логику вычисления(собственно калькулятор) и алгоритм конструирования вида "Standart". По создании StandartViev создаёт компонент StndartScoreboard и подключает его к Panel. Этот компонент, наследует компонент Edit(обычное win-edit), дополняя его интерфейсом IScoreboard и изменяя логику вывода текста. Затем, StandartViev подключается к Math и ButtonsPanel и, передаёт последнему массив с описанием клавиатуры(количество, расположение, название и индексы кнопок). ButtonsPanel создаёт и размещает необходимое количество компонентов Button, после пересчитывает свой размер/положение и передаёт его компоненту Panel, который в сою очередь пересчитывает свои размеры/положение(и передаёт их Form) и размер/положение дочерних окон. По завершении всех приготовлений StandartViev извещает CalcMenu, который в свою очередь делает главное окно видимым. Программа готова к работе.
  Если пользователь выбирает пункт меню "About Calculator", CalcMenu создаёт компонент About и подключит его к Form. Этот компонент, наследует компонент Window(независимое окно с рамкой и кнопкой закрыть) и дополняет его интерфейсами IImage и ILabel. About создаёт компоненты Panel, Image, Label и подключает интерфейсы IImage, ILabel через которые загружает информацию для отображения. Закончив инициализацию, About отображает себя.
  В этот момент конструкция из компонентов выглядит примерно так:
Светло-зелёным - динамические компоненты, фиолетовым - статические, чёрным - коневые интерфейсы, жирная линия - иерархия окон.
  Когда пользователь нажимает кнопку "Close", компонент About скрывает себя и саморазрушается, что приводит к отключению от Form и разрушению компонента Panel, который в свою очередь разрушает Image и Label.
  Когда пользователь нажимает какую ни будь из кнопок клавиатуры калькулятора, компонент Button сообщает об этом ButtonsPanel через IButton, то в свою очередь передаёт индекс нажатой кнопки StandartViev через IButtons. Получив индекс, StandartViev выполняет соответствующие действия и, при необходимости, обновляет отображаемое на StndartScoreboard значение, через интерфейс IScoreboard.  
  Когда пользователь переключает вид с "Standart" на "Programmet" компонент CalcMenu разрушает компонент StandartViev, что приводит к отключению интерфейсов IView, IMath и IButtons. Разрушаясь StandartViev также разрушает компонент StndartScoreboard. Дождавшись освобождения ресурсов CalcMenu, создаёт компонент ProgrammerView.
  ProgrammerView как и StandartViev инкапсулирует логику вычисления и алгоритм конструирования вида. При создании, ProgrammerView подключает интерфейсы IMath и IButtons, загружая через последний описание клавиатуры. Создаёт пару компонентов CheckBoxForm(первый для выбора системы исчисления, второй для выбор длинны значения), загружет описание чекбоксов через ICheckBoxSys и ICheckBoxLong, которые в свою очередь создают необходимое количество компонентов Check. В конце создаёт компоненты ProgScoreboard и BinWeav(отображает значение в двоичном виде) для отображения. Программа готова к работе.
  В этот момент конструкция из компонентов выглядит примерно так:
Светло-зелёным - динамические компоненты, фиолетовым - статические, чёрным - коневые интерфейсы, жирная линия - иерархия окон.
  ProgrammerView получает информацию о действиях пользователя через интерфейсы IBottons, ICheckBoxLong и ICheckBoxSys, и выводит результат свой работы через IScoreboard и IBinView.
  Когда пользователь нажимает кнопку "Close" на главном окне, компонент Form сообщает об этом CalcMenu через интерфейс IForm. CalcMenu разрушает компонент ProgrammerView, и как следствие, все созданные им компоненты. Затем CalcMenu получает координаты главного окна через IForm и сохраняет их вместе с текущим видом, посредством компонента Registry. В конце разрушает компонент Form, который в свою очередь разрушает Panel(разрушающий ButtonsPanel(разрушающий все Button)) и CalcMenu. Разрушение статического компонента CalcMenu(или другого статического), приводит к разрушению всего приложения, т.е. разрушаются(вызываются деконструкторы) все существующие в приложении компоненты и процесс завершается.
Виртуальная реальность
  На примере простой 2D стрелялки с реалистичной физикой. Суть игры проста - ходить в ограниченном (win-окном) двухмерном пространстве и, стрелять по движущимся мишеням. В геймплейе есть четыре типа объектов: игрок(точнее управляемый игроком аватар), мишени, пули и ограничивающие стенки. Все объекты должны мочь перемещаться в любом направлении в 2D пространстве(а стены ещё и изменять свою длину), и некоторым образом взаимодействовать.
  Для простоты из физического мира будут эмулироваться только следующие свойства и явления: пространство, геометрическая форма, масса, скорость, инерция, трение и упругие соударения.
  Виртуальное пространство условно разбито на кванты расстояния, они одинаковы для всех объектов. Виртуальные объекты размещаются в пространстве и представляют из себя массивы точек(думаю, для большей точности и производительности можно было-бы использовать массивы кривых/прямых линий, но это усложнит пример), определяющие геометрическую форму объектов, в их системе (декартовых) координат, для простоты начало координат совпадает с центром масс объекта. Например, для треугольника требуется три точки, для пятиугольника - пять, а для круга столько, сколько необходимо, чтобы заполнить точками окружность, на расстоянии в один квант друг от друга:
  Если объект не взаимодействует с другими, то двигается прямолинейно и/или вращается вокруг центра масс с некоторой скоростью, или же находится в покое. По мере движения скорость уменьшается(из-за трения) и конечном итоге становится нулевой. Двигаясь, объекты будут ударяться друг о друга и, об ограничивающие стенки, если сила удара превысить не который максимум - объекты разрушаются(кроме стенок, являющихся не разрушимыми). Соударяясь объекты сообщают друг другу некоторый импульс, зависящей от угла удара, скорости и массы, изменяющий траекторию движения объектов.
  Исходя из выше написанного, архитектура игры будет иметь примерно такой вид:
Светло-зелёным - динамические компоненты, фиолетовым - статические, чёрным - коневые интерфейсы, жирная линия - иерархия окон.
  Компонент Space эмулирует пространство, его задача отслеживать все виртуальные объекты и, сообщать им о случаях соприкосновения их между собой. При создании, виртуальный объект подключается(регистрируется) к Space при помощи интерфейса ISpace, с этого момента он начинает "существовать". Внутри Space постоянно крутится цикл, в котором для всех виртуальных объектов вызывается метод tick (импортированный через ISpace), чтобы движение виртуальных объектов выглядели на экране линейными, цикл синхронизирован с системными часами(например одна итерация каждые 10ms). Каждый объект в методе tick пересчитывает координаты своих точек(в своей координатной сетке), например когда объект вращается или его геометрия изменяется. За один тик объект может сдвинуть точки не более чем на один квант расстояния(это максимальная скорость движения, "скорость света"). Метод tick возвращает массив точек(если он изменился с прошлого тика) и линейную скорость(в т.ч. вектор) объекта. Используя данные из tick компонент Space рассчитывает координаты объектов в абсолютной системе координатах(сами объекты ничего "не знают" о своих абсолютных координатах) и проверяет отсутствие взаимно-пересечений границ объектов(отсутствие столкновений). Для столкнувшихся объектов Space подключает интерфейс ISynergy.  
  После подключения ISynergy, объект получает у Space (через ISpace) координаты взаимодействующего объекта(точки начала его системы координат) в своей системе координат, т.е. его положение относительно себя. Затем, через ISynergy, получает у взаимодействующего объекта массив координат описывающих его точек, его скорость и прочие требуемые свойства. Все эти данные используются для расчёта удара, по завершении которых объект некоторым образом изменяет свои свойства. Интерфейс остаётся подключённым до тех пор, пока виртуальные объекты не "разойдутся", т.е. пока будет выполняться взаимодействие.
  Компонент Camera примерно 25 раз за секунду обращается к Space через ILayout, и получает список и расположение(абсолютные координаты) всех виртуальных объектов. Затем, обходя список, Camera опрашивать все объекты(через ICamera)проверяя, если геометрия и/или текстура объекта изменилась - обновлять свой буфер. Используя полученные данные Camera строит(если необходимо/перестраивает предыдущий) очередной кадр изображения объектов, и выводит его на окно компонента View, через IView.
  Все прототипы виртуальных объектов наследуют прототип CObject, содержащий реализацию интерфейсов ISpace,ICamera и определение ISynergy, а также набор вспомогательных функций. При создании, по завершении инициализации, виртуальный объект подключает интерфейсы ISpace, ICamera, т.е. "появляется" в виртуальном пространстве, в некоторых начальных координатах(определяемых через IControl(по умолчанию ) или самим новым объектом, относительно каково ни будь уже существующего объекта). Когда виртуальный объект разрушается, он разрывает соединение через ISpace и ICamera.
  Компонент Arena управляет четырьмя виртуальными объектами Wall, представляющими из себя ограждение игрового поля(чтоб другие объекты не покидали его).
  Компонент Joystick получает клавиатурные сообщения от главного окна(Form, через IForm), и управляет виртуальным объектом(ми) Avatar. Когда игрок нажимает пробел компонент Avatar создаёт экземпляр объекта Bullet, и через интерфейс IBullet сообщает ему начальные параметры. Когда игрок нажимает "Pause", Joystick приостанавливает работу компонентов Space и TargetGen, через IControl и IGen соответственно.
  Компонент TargetGen создаёт виртуальные объекты Target с рандомными начальными параметрами.
  Компонент Observer подсчитывает очки игрока и количество уничтоженных мишеней, и выводи их по средством компонента StatusBar(полоска внизу главного окна).

No comments:

Post a Comment