Ху есть мистер Component?

Мы привычно говорим «Компонента», «Компонентное программирование» и, увы, все мы понимаем эти слова по-разному.

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

Эта заметка написана по следам разговоров с Игорем Беспальчуком. Я попытаюсь описать свое понимание «частей» и «сборки». Надеюсь, что Игорь (и все желающие) добавят к этому свое понимание и критику.

Я не буду углубляться в то, почему эта тема важна, приведу только цитату из книги Роберта Мартина «Чистая архитектура. Искусство разработки программного обеспечения»:

«Внимательно рассмотрев практику программирования компьютеров, вы заметите, что очень немногое изменилось за 50 лет. Языки стали немного лучше. Инструменты стали фантастически лучше. Но основные строительные блоки компьютерных программ остались прежними.»

Мартин сравнивает 1966 и 2016 годы. Любопытно, что даже Илья Муромец сидел на печи меньше, чем ИТ индустрия сидит в ясельках…

Не пора ли нам вылезти из детских пеленок и начинать делать программы по-взрослому? Из строительных блоков побольше?

Вот отсюда и вопрос, что есть такое строительный блок и как из блоков собирать программы.

Игорь в докладе использует термин DevArch, который подчеркивает, что программы делают разработчики (Developers) и архитекторы (Architect). В докладе Игорь хорошо говорит о «странностях» работы архитектора, и о том, что архитекторы «страшно далеки от народа», то есть от разработчиков. Сближение архитекторов и разработчиков и наличие явной, встроенной в программу архитектуры решит множество нерешаемых сейчас задач, но я сейчас не совсем об этом.

Вернусь к строительным блокам, если в разработке программы принимают участие (на разных уровнях) A и D, то возникают вопросы:

  • Они работают с одними и теми же строительными блоками?
  • Они используют одинаковые способы подключения/взаимодействия строительных блоков?

На мой взгляд, ответ НЕТ на оба вопроса. И тогда у нас должны быть A-блоки и D-блоки, и способы их связи.

Замечание: Архитектор и Разработчик — это роль, один и тот же человек может совмещать или чередовать эти роли. То есть я говорю не об ограничениях кого-либо, а о разделении понятий.

Архитектор собирает программу из A-блоков. Часть A-блоков он берет готовыми (reuse), а часть ставит болванками (прототипами). Разработчик начинает работать с уже готовой программой, в которой часть блоков болванки, а часть взаимодействий не реализовано.

Разработчик собирает A-блок из A-блоков или D-блоков, и вставляет D-блоки взаимодействия. Я уже писал о таких промежуточных блоках и называл их «клеем».

Так же, как архитектор, разработчик может брать готовые D-блоки разной степени универсальности или программировать новые D-блоки.

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

Примеры A-блоков общего использования – UI блоки, текстовый редактор. Примеры D-блоков общего использования – математические функции, контейнеры, шрифты, http, ….

31 комментарий


  1. Попробую поспорить. На мой взгляд, принципиальной разницы между «блоками», в которых мыслят и с которыми работают разработчики и архитекторы — нет. В посте утвкрждается обратное, и даны некоторые примеры, но я пока не уловил принципа различения A- и D- блоков. Более того, на мой взгляд, какой-нибудь готовый UI-блок «текстовый редактор с подсветкой синтаксиса» может быть как элементом архитектурного проекта, так и предметом тщательной до-раз-работки (т.к. нужно заставить его работать, выяснить ограничения опытным путём, кастомизировать в точках гибкости и т.п.). Известно, что невозможно провести четкую границу и назвать строгий критерий разделееия области ответственности архитектора и разработчика — эта граница определяется в основном проектной ситуацией, уровнем компетенции разработчика, и уровнем доверия к нему со стороны архитектора (и поэтому, очевидно, области ответствености динамичны на ЖЦ проекта/системы). В остальном они выполняют очень схожую работу в очень схожем, многоуровневом материале — дизайне. Нижние уровни дизайна описываются кодом, верхние — нет, но по-хорошему граница A-D не должна определяться _этой_ разницей. В тех ситуациях, когда разработчик совсем слаб, архитектор опускает границу достаточно сильно и выдает разработчику готовые интерфейсы классов (в ООП), сигнатуры функций, образцы для типового кода, контролирует использование тех или иных паттернов, включая достаточно низкоуровневые (типа GoF).

    Ответить

    1. На мой взгляд, пока мы не введем принципиальную разницу между A и D блоками, мы не продвинемся. Основная беда программирования — это понятийная каша.

      Технологически граница будет не четкая, так как:
      1) Любой D-блок может быть обернут и использован, как A-блок.
      2) Как я уже упоминал, архитектор — это роль. Мы все немного архитекторы и немного разработчики.

      Возможно, что если мы заменим Architect на Assembler (Сборщик) в аббревиатуре A-блок станет понятней.

      Программа собирается (ассемблируется) из A-блоков, для этого не надо «программировать» на языке программирования. Сам А-блок может или программироваться или собираться из более мелких. Соединения A-блоков всегда «разъемные».

      А вот D-блоки всегда программируются, и взаимодействие D-блоков программируется — условно, это скорее вызов, чем «разъем».

      Еще раз: Сейчас нет принципиальной разницы между A и D блоками, просто потому что A-блоков нет (точнее они есть только в «Вире»).

      Ответить

      1. Да, возможно мое непонимание как раз проистекает из того, что «А-блоков не существует, кроме как в Вире». Но вот точно — не стоит их ассоциировать с работой архитектора. Давайте, правда, Assembiled/Developed. То есть технология работы с ними различна, и разница именно в этом, независимо от того, кто эту работу по соответствующей технологии выполняет — архитектор из своей роли или разработчик из своей роли.

        Тут я дальше утыкаюсь в то, что недостаточно хорошо понимаю (что неудивительно, ведь этого пока нигде нет!), что же такое «сборочный компонент», как именно с ним идет работа, и чем технология этой работы («сборки») отличается. Чем «ассемблирование» A-блоков отличается от обычного использования готового D-модуля из репозитория типа Npm? Я говорю, где мне нужен компонент, какие у него будут настройки, снабжаю зависимостями, размещаю где-то в составе другого, более крупного D-компонента… Видимо, я пока не схватил идею сборочности из Вира и вообще.

        Ответить

        1. Думаю, можно тут отметить, что мы пообсуждали этот вопрос более детально, и вроде пришли к следующему. А- и D- блоки отличаются между собой не то, чтобы по своей природе, или механике, или языку, или возможностям. Просто на некоторых уровнях система, которая строится по принципам компонентного дизайна, как кажется, вообще не требует наличия императивного кода, а нужны только компоненты нижележащих уровней и средства их коммутации. По мере движения вглубь, к деталям устройства, могут начать появляться компоненты, у которых внутри (как реализация их интерфейсов) присутствует как императивный код (отдельные функции/процедуры/методы, как ни назови), так и взаимодействие с другими целыми компонентами, внешними, или дочерними. И наконец, в самом низу, в корнях каждого компонента, конечно лежит императивный код (ну, или, может, функциональный), уже без внутренней _компонентной_ структуры. Четкой границы нет, но разница в крайних проявлениях очевидна. Наличие императивного кода в компонентах верхнего уровня — возможно, будет являться признаком плохого дизайна. Но это неточно. Без большого опыта промышленной разработки в компонентном подходе на подобающих средствах (которых пока нет) все это — гипотезы.

          Ответить

  2. Эммм…. Алексей, идеи Ваши мне понятны и близки))
    Но в моём сознании при чтении текста всплывает только одна фраза — Компонентный Паскаль. Я понимаю, что Вы вводите понятие «клея» (схема) и он намазывается на блоки архитектором, но вот что касается «компонент» — это уже готовый к употреблению блок, который в Компонентном Паскале на намазывается отдельно созданным клеем, а собирается как схема через ровно такой же компонент верхнего уровня.

    Что касается обоих вопросов выше — уже как-то написал, что Компонентный Паскаль на сегодняшний день — для меня является наиболее совершенным воплощением того языка программирования, на котором я бы хотел писать ещё вчера — -поэтому исходя из моего понимания Вашего текста, с позиции КП — ответ на оба вопроса «да».

    Или я что-то откровенно недопонимаю в Вашем подходе, либо что-то ещё)) Ну, если дадите ссылку на подробненькую статью для тупых — скажу спасибо))

    Ответить

    1. Компонентный Паскаль имеет очень слабое отношение к той технологии программирования, которую я продвигаю.

      Вот здесь http://алексейнедоря.рф/?p=269 об этом я говорил.

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

      «Компоненты», точнее А-блоки должны быть связаны внешним соединениями, а не (внутренним) импортом. В этом основное отличие. Кирпич (или микросхема) не связаны статически с соседними кирпичами (микросхемами). Это в гораздо большей степени независимые части, чем модули.

      Ответить

  3. ««Компоненты», точнее А-блоки должны быть связаны внешним соединениями, а не (внутренним) импортом»

    Вроде бы, импорт — это ссылка на интерфейс. Если хочется явно повыставлять связи, он не мешает. Разве нет? Если же на интерфейс не ссылаться, то его надо каждый раз воспроизводить, что накладно и есть большая вероятность составить его так, что несмотря на выполнение нужной роли компоненты не будет стыковаться и тогда их нужно будет связывать через бесконечные переходники, что снова накладно. Если же интерфейс воспроизводят всё время как надо, значит, его всё равно импортируют, но в уме.

    Можете прояснить эту часть?

    Ответить

    1. Проще всего объяснить, используя аналогию с электроникой. На печатной плате стоят несколько микросхем и они связаны проводниками. Проводники — это внешняя связь по отношению к микросхемам. При проектировании/изготовлении микросхемы не задан «статически» набор других микросхем, которые обязательно должны обязательно быть, чтобы микросхема работала. Для микросхем задан интерфейс — спецификация входные/выходных разъемов, но подключить к разъему можно что угодно, лишь бы спецификация выполнялась.

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

      Естественно, средства борьбы с жесткостью всем известны от callbacks и DCOM до динамически загружаемых классов. Но почти всегда мы считаем, что статическая связь — это хорошо, а динамическая нужна, но иногда.

      Впрочем, некоторые системы постоянно используют динамическую сборку например, в big-data или data-flow системах постоянно используется динамическая сборка на уровне крупных блоков, через docker/kubernetes/очереди сообщений и тому подобное. То есть на уровне больших распределенных систем динамика и сборка уже вошли в практику.

      То, что я пытаюсь сделать — это перевернуть привычную технологию программирования, и поставить её с головы на ноги. Зачем и почему — я уже много раз писал.

      Программы надо собирать из блоков, используя условно динамические связи. «Условно», потому что проводники на печатной плате вполне статические во время работы и даже во время проектирования печатной платы, но динамические (не фиксированные) во время проектирования/изготовления микросхемы.

      Точно так же в программной системе надо уметь переходить по уровням динамика/статика, а уровней может быть много, так как программная среда гибче физической.

      На мой взгляд, при изготовлении программной системы мы должны начинать с динамики. Программная система собирается из А-блоков и «проводников», соединяющих разъемы блоков. На этом уровне гибкость предельная, что упрощает прототипирование. Далее по мере «стабилизации» архитектуры можно
      оптимизировать часть взаимодействий, уменьшая динамику до нужного уровня, вплоть до вызова функции (static dispatch), как самого жесткого вида связи.

      И последнее, что очень важно: мы должны уметь проходить не только вниз — к уменьшению динамики, но и вверх. Разборка программной системы не менее важна, чем сборка. Как мы знаем, работа над программной системой не завершается, до тех пор, пока система используется.

      Программная система — это как молодая женщина из известного анекдота: дитя, девочка, девушка, молодая женщина, молодая женщина, …. молодая женщина, бабушка умерла.

      Технология разработки программная система должна обеспечить возможность сборки, оптимизации, «разборки» на составные части, замены частей и пересборки.

      Ответить

      1. «При проектировании/изготовлении микросхемы не задан «статически» набор других микросхем, которые обязательно должны обязательно быть, чтобы микросхема работала»

        Современные языки программирования для проектирования микросхем используют импорт для того же, что и ЯП для процессоров. Дело не в эффективности исполнения, а в эффективности написания — сослаться на что-то проще, чем воспроизводить, пусть это и только интерфейс. Если хотите, могу попытать у коллег — кодеров на Verilog. Не наворачиваете ли Вы очередной виток на спирали полноты по Тьюрингу?

        «В программировании импорт — это именно статическая связь.»

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

        Ответить

    2. Коллеги, позвольте я этот момент прокомментирую со своей позиции.

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

      Проще всего схватить разницу на материальных примерах: программные модули — это как чертежи, например, поршня в цилиндре и корпуса двигателя внутреннего сгорания. Один чертеж поршня, один чертеж корпуса. Это то, что в design-time. Это программный код. Компоненты в работающей программе (в run-time) — это как конкретный физический корпус какого-то двигателя и несколько (!!!) физических поршней в нем и сам этот двигатель в сборе как компонент верхнего уровня. Отношение «программный компонент» — «программный модуль» соответствует отношению «объект»-«класс» в class-based ООЯП. Очень хорошо видна разница между компонентами и модулями, когда мы смотрим на отношение кратности: модули корпуса и поршня 1:1, а компоненты в каждом двигателе 1:4 или 1:6 или еще как-то. Ну и отношение модуля и компонентов всегда один-ко-многим, по двум чертежам мы штампуем тысячи корпусов и поршней, а из одного программного модуля (класса ООП) рождаются миллионы программных компонент (объектов ООП).

      Про отношения зависимости и использования.
      «Чертеж» двигателя может напрямую «импортировать» чертеж поршня — ссылаться на него: «а сюда потом вставлять поршень, произведенный по чертежу A-123/5.45.67ы». А может не ссылаться на чертеж поршня, а ссылаться только на описание требований к нему («интерфейс»): «сюда вставлять поршень типоразмера 16×27, соответстввующий ГОСТ 12345-87». В ООЯП этому соответствует зависимость класса-потребителя не напрямую от класса-провайдера, а через интерфейс (ну, то есть, полная независимость от модуля-провайдера, а вместо этого — зависимость от модуля с описанием интерфейса взаимодействия). Именно такой стиль ООП-программ за последние лет 15 как раз и стал mainstream’ным (благодаря принципам SOLID и Роберту Мартину, конкретно — принципу IoC, Inversion of Control), и уже давно широко принят индустрией, наработаны многочисленные фреймворки (типа Spring в мире Java) и библиотеки (IoC-контейнеры), позволяющие таким образом развязывать зависимости уровня модулей, связывая компоненты (!) в runtime только через предоставляемые другими компонентами интерфейсы. В каком-то смысле программы при этом действительно «собираются». При этом связанные компоненты уже не «зависят» (depend) друг от друга (резервируем это слово для модулей), а _используют _ (use) друг друга, причем, через некоторый интерфейс, не зная точно, что за реализация за ним. Примерно как процессор _использует_ память, не зная, что за планка вставлена в материнскую плату, но зная интерфейс.

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

      Я уверен, что если мы начнем использовать вот такой, более различающий словарь, то понимание начнет приходить быстрее.

      Смотрите:
      ««Компоненты», точнее А-блоки должны быть связаны внешним соединениями, а не (внутренним) импортом»

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

      Если я правильно это проинтерпретировал (и, надеюсь, видно, что терминологическое различение помогает разобраться), то это формулировка принципа Inversion of Control.

      «Вроде бы, импорт — это ссылка на интерфейс.»

      Иногда — на интерфейс, а иногда — прямо на реализацию, что, конечно, связывает модули очень сильно.

      Ответить

      1. «Проще всего схватить разницу на материальных примерах: модули — это как чертежи»
        Ну это уж Вы так определили, а вот МКС состоит из модулей, которые совсем не чертежи. Проблема в том, что программисты заимствуют термины, назначая им новые, хотя и похожие смыслы. Лучше бы придумывали свои.

        «При этом связанные компоненты уже не «зависят»…, а _используют _ (use) друг друга»
        Нельзя использовать что-то, но не зависеть от него. При этом зависимость через логическое ИЛИ перечислением или косвенной зависимостью — это всё равно зависимость. Это может быть важно для понимания, что даже в выделенными Вами терминами «модуля» и «компонента» может и не быть строгой границы.

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

        Ответить

        1. Как-то я совсем потерялся. О чем вообще идет спор?

          Чистая теория мне не интересна. Для меня вопрос простой: какая нужна поддержка в языке программирования для того, чтобы
          1) делать А-блоки
          2) определять связи между А-блоками

          Связи между А-блоками должны позволять взаимодействие часть А-блоков расположенных в другом процессе или на другом устройстве. Возможно, что не все А-блоки должны быть настолько «переносимыми» (см. например Dart isolates).

          Вопрос о зависимостях (import/use/include) очевидно вторичен. Ограничения на использование должно вытекать из требований.

          Ответить

          1. @AN
            * какая нужна поддержка в языке программирования для того, чтобы
            1) делать А-блоки
            2) определять связи между А-блоками

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

            Но, для того, чтобы это непонимание прояснить, я хочу спросить вас так: а что сейчас вам мешает, (в любом современном ЯП), делать А-блоки и определять связи между ними?

            Я бы сказал, что единственная поддержка, которая для этого нужна — это понятие интерфейса, который может быть скомпилирован отдельно от реализациии. Ну, может, еще второе — возможность динамически загрузить скомпилированный модуль, просмотреть имеющиеся в нем типы и их интерфейсы, и создать объект по типу с приведением к нужному интерфейсу (Reflection, RTTI). Эти два пункта вроде везде или почти везде есть.

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


          2. «Для меня вопрос простой: какая нужна поддержка в языке»

            Тогда, может, попрограммируем? В естественном языке общения трудно достичь точности в определении общих понятий и будет постоянное недопонимание. А так — напишем немного компонентов, посоединяем их и посмотрим, что из этого получится. Будет понятней, как решается или не решается задача.

            Для простоты — всё в тексте, но без псевдокода в том смысле, что код потенциально должен быть исполняем машиной, пусть на данный момент и нет готовых инструментов. Код желателен простой, но чтобы решал задачу без излишних условностей, чтобы при желании всё можно было помоделировать-потестировать на других языках.


          3. «Тогда, может, попрограммируем?»
            Мысль хорошая, но я пока не могу.

            Во-первых, в разрабатываемом языке мы еще не дошли непосредственно до работы над компонентами, а думать плотно над другими темами (не теми, над которыми идет работа), это замедлить скорость разработки.

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

            В третьих, делать что-то «условно» исполняемым мне кажется неверным. Если уж делать что-то, то это что-то должно работать. Именно так я экспериментировал с Виром.


          4. @comdiv
            * Тогда, может, попрограммируем?

            У А.Н. много интересных концптуальных наработок типа Вира, но его правда сложно сопоставлять с промышленными ЯП. А новый ЯП с компонентами пока не готов.

            Я в свободное время рисую схемки и пишу псевдокод для консольной шахматной программки.
            https://github.com/iamhere2/HLCD-Researches/tree/master/ChessApp
            Времени свободного, правда, маловато, только на выходных получается позаниматься.

            А без псевдокода получится плохо — ведь в современных ЯП нет поддержки компонентной модели. Ну, то есть, можно на кажый компонент сделать по классу и положить их все просто рядом, как обычно и делается, но какой в этом тогда будет смысл?

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

            Можете, если вам интересно на реальном ЯП написать соответствующую реализацию без компонент, на любом ЯП, присылайте MR, наберем подборку и сможем сравнить подходы и возможности, пообсуждать предметно, на примере. ИМХО, задачка как раз что надо — не слишком простая и не слишком сложная — консольная программка для игры в шахматы. Движок, который бы играл за компьютер, при этом можно не делать или сделать «совсем дураком», в данном случае это не важно.


        2. @comdiv, отличные замечания, спасибо!

          * Ну это уж Вы так определили, а вот МКС состоит из модулей, которые совсем не чертежи.

          Совершенно верно, и в первых сладйах своего доклада (они исключительно важны для глубокого понимания остальной части) я говорю об этой разнице значений для термина «модуль» из «железной» системной инженерии, и термина «модуль» для software engineering. Поэтому в своем комментарии выше я снечала использовал уточняющие прилагательные: «программный модуль», «программный компонент». Действительно, если мы возьмем точный смысл «модулей МКС», и попытаемся перетащить его в сферу ПО, то обнаружим, что таких явлений в сфере ПО просто нет! Ну нет у нас в ПО таких логистических единиц из design-time, которые становятся полноценными частями воплощения системы в run-time. Ну, или они настолько спрятаны, размазаны и скрыты на низких уровнях, что никому уже не интересны вообще никогда. А значит, и понятие такое не нужно, и термин. Вместо этого термин «модуль» в SE приобрел совсем другое значение, означает другое понятие, и я с вами согласен, что это не здорово, но он уже закрепился за 60-70 лет достаточно прочно, никуда от него уже не деться. Поздно сетовать, что в 1960-х пионеры-практики взяли это слово. Поэтому я считаю, что лучшее что можно сделать — это ввести уточняющий квалификатор: «программный», и четко описать в чем разница в значениях. Что я и попытался сделать на первых слайдах.

          Ответить

        3. @comdiv
          * Нельзя использовать что-то, но не зависеть от него. … даже в выделенными Вами терминами «модуля» и «компонента» может и не быть строгой границы.

          Напротив, я утверждаю, что как только вы у себя в мышлении разлепите понятия (программного) компонента и (программного) модуля, вы увидите эту разницу. ну, кстати, я тут не пионер, различение этих двух отношений («use»/»depend on») рассмотрено, например, в «Documenting Software Architecture». Да и в UML проявлено, если не ошибаюсь, разными типами стрелочек, см. например, package diagram vs component diagram.

          Смотрите, кофеварка «использует» источник энергии в момент работы, и без него работать не сможет, и надо ей предоставить этот источник. Но чертеж кофеварки не «зависит» от чертежа источника энергии, а «зависит» только от интерфейса — евро-вилка 220V 50Hz с дополнительным контрактом (не выраженным в интерфейсе), что там будет 1A.

          Да, слова естественного языка немножко отличаются, мы используем как синонимы «зависит», «использует», «требует», «потребляет», «подключается», «хочет», и т.п. Но если мы хотим немножко разобраться в сложных вопросах, нам нужно добавлять в язык строгости, и где нужно — ограничивать значения и четче разклеивать разные термины-ярлыки на разные явления/предметы.

          Ответить

        4. @comdiv
          * Даже когда автор не подразумевал любой другой реализации, он всё равно ссылается на интерфейс,

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

          Давайте сперва про последнее. Да, _в_принипе_ заменить, конечно, можно. Но если у вас код зависит (в смысле «не собирается без») от модуля с реализацией, то вам потребуется править этот модель с реализацией или править модуль, который зависит, перенаправляя на другой модуль с реализацией. И то, и другое потребует вмешательства в существующзий код, который работает, а это повышает вероятность ошибки и усложняет конфигурационное управление (версии, и т.п.). Поэтому все и носятся как со святым Граалем с тем, чтобы как-нибудь перестыковывать между собой модули, не меняя их. Это Open/Closed принцип из SOLID, если не ошибаюсь.

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

          И еще одно, про «полный интерфейс»:
          * полный интерфейс — это не столько привычная сигнатура условной функции, но ещё и пред- и пост- условия.

          Есть отличное слово — «контракт». И это не то же самое, что интерфейс. Интерфейс — это когда вы в принципе можете подключиться. А контракт — это при каких условиях вы будете по этому интерфейсу обслужены. Сигнатура функции — это интерфейс. А то, что, например, там есть предусловие, что dateFrom < dateTo — это контракт. Отличное различение, на мой взгляд, и термины хорошие.

          Ответить

          1. * кофеварка «использует» источник энергии … Но чертеж кофеварки не «зависит» от чертежа источника энергии, а «зависит» только от интерфейса

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

            Точно также и класс в ЯП — это лишь объект на более высоком уровне абстракции. В таком же отношении находятся Ваши модуль и компонент, как я Вас понял.

            Я не спорю, что нельзя использовать Вашу терминологию, просто доношу свой взгляд. Так-то можно объявить всё по разном. Главное — со всеми договориться.

            * в языках со строгой типизацией обычно если вы используете имя типа, то … означает «теперь мы связаны с тем модулем жестко, и нас нельзя пересобрать без него, и при его изменениях нас тоже надо будет пересобрать»

            Я вижу это как вопрос реализации. Даже жёсткое авторское видение можно переиграть. Радикальный, но распространённый пример — программа, которая жёстко завязана на использование всего компьютера превращается в изолированный компонент системами виртуализации. Явное выражение компонентности — нужная вещь, но, зачастую, она не противоречит другим подходам. Далеко не всегда нужны такие жёсткие меры, это просто пример, что в мире ПО всё можно подменить, даже железо. А это значит, что завязка идёт, всё-таки, на интерфейс даже если автор этого не знает.

            * Есть отличное слово — «контракт». И это не то же самое, что интерфейс

            Могу предложить другое отличное слово — «разъём».
            А контракт — это всего лишь формализация интерфейса отдельно от исполнимого кода. Документация — это разновидность контракта. Но интерфейс существует вне зависимости от его формализации, а за неимением иного, зачастую, формализмом исполнимого кода выступает сам код(делай как я).
            Например, USB интерфейс останется интерфейсом даже если мы разом потеряем всю документацию(контракты) для него. Более того, для него мы используем разные разъёмы и это всё равно один интерфейс, а для их соединения нам достаточно простых переходников. В обратном случае ничего не получится — другой интерфейс, засунутый в разъём USB, не будет работать с USB устройствами, хотя разъёмы будут соединяться отлично.

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

            Так как в ПО возможен бесконечный уровень косвенности, то что в одном случае выступает интерфейсом, на другом уровне уже может выступать разъёмом(транспортный уровень, туннелизация и пр.). Это и может приводить к путанице с терминами.


          2. @Comdiv

            * С моей точки зрения — чертёж кофеварки и сама кофеварка — это одно и то же с точностью до уровня абстрагирования.

            Это немного странно, т.к. слишком уж это разные вещи. Разве вы можете сварить кофе с помощью более или менее абстрактного чертежа? Реальная кофеварка — это физический 4D-объект, а её модель (представленная чертежом) — абстракция. В большинстве upper ontologies эти вещи различают в самом корне как принципиально разное. Конечно, можно сказать, что модель «абстрагирует» физический объект (лучше сказать — «моделирует»), но все-таки надо помнить, что наша цель, как инженеров — порождать не только модели прекрасного будущего, но и что-то, что будет менять физический мир. Работающая (в конкретное время, на конкретном оборудовании) программа — это тоже физический 4D-объект/процесс (любое вычисление — это физический процесс). И нас в сфере ПО интересует в конечном итоге получение работающих (исполняющихся) программ. Поэтому все-таки я бы очень рекомендовал различать вот эти конечные предметы нашего интереса (которые 4D), и все многочисленные нематериальные концепты (и их материальные носители, конечно), с которыми мы имеем дело на пути к цели. Никакая диаграмма и никакой программный код не смогут вам рассчитать зарплату, это сможет сделать только работающая программа. Именно это различие я беру как то основное, что отличает модуль от компонента. Прграммный Компонент — это существующий в runtime фрагмент физической реальности. Программный Модуль — это модель (выраженная в ЯП) будущего компонента, возможно, хранимая на каком-то носителе, и как модель (!) он действительно может быть более или менее абстрактным. Компонент — предельно конкретен, сегодня он обычно воплощен в 4D обычно в состоянии и поведении pn-переходов полупроводниковых схем. Хотя, конечно, для того, чтобы «увидеть» программный компонент, мы прибегаем к помощи других программ и некоторой интерпретации происходящего. Но если мы им верим на 100%, также как физик верит тоннельному микроскопу и ускорителю частиц, то мы признаем эту измеренную приборами интерпретацию за реальность: «здесь действительно есть атом урана 235». Сравните: «на этом компьютере действительно запущен процесс #1256». Ну, вот такая опосредованно-воспринимаемая физическая/программная реальность. Но вот концепция атома или план эксперимента на ускорителе или чертеж кофеварки или диаграмма классов — это вещи совсем другой природы, это концепты (иногда с носителем), и у концептов нет места во времени и в пространстве (где находится корень из двух? Гарри Поттер? понятие окружности? алгоритм QuickSort?). Носитель («описание»), конечно, локализуется в 4D, но он никогда не может выступить заменой самой целевой системы, которую моделирует концепт.

            Прошу прощение за длинный текст.

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

            * Точно также и класс в ЯП — это лишь объект на более высоком уровне абстракции. В таком же отношении находятся Ваши модуль и компонент, как я Вас понял.

            Вроде объяснил, почему считаю, что отношение совсем другое.

            * Я не спорю, что нельзя использовать Вашу терминологию, просто доношу свой взгляд. Так-то можно объявить всё по разном. Главное — со всеми договориться.

            Абсолютно верно. Термины для этих понятий могут быть любыми.

            ** в языках со строгой типизацией обычно если вы используете имя типа, то … означает «теперь мы связаны с тем модулем жестко, и нас нельзя пересобрать без него, и при его изменениях нас тоже надо будет пересобрать»

            * Я вижу это как вопрос реализации.

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

            * А это значит, что завязка идёт, всё-таки, на интерфейс даже если автор этого не знает.

            Я это вижу очень просто — мы ставим эксперимент. Удалось получить работающую программу, заменив реализацию без доступа к исходникам — значит, «завязки на реализацию не было». Не удалось — значит, была. Понятно, что это звисит от ЯП.

            * Могу предложить другое отличное слово — «разъём».
            А контракт — это всего лишь формализация интерфейса отдельно от исполнимого кода. Документация — это разновидность контракта. Но интерфейс существует вне зависимости от его формализации, а за неимением иного, зачастую, формализмом исполнимого кода выступает сам код(делай как я).

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

            Когда я говорил контракт, я не говорил про документ. Я говорил про концепт — условия, при которых компоненты, соединенные через интерфейс, смогут провзаимодействовать как ожидется исходя из их назначений.

            Уточнив наличие этого, второго очень важного различения, я хочу переспросить — что вы называете «разъемом»? Мне не до конца понятно.

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

            В данном случае нужно различать физические интерфейсы (разных форматов), электрические, и «логические» (информационные). Это все три — интерфейсы, и, если не ошибаюсь, такая терминология закреплена в культуре. Например, если физический интерфейс соблюден, но мы подали на проводники 220V, то вряд ли можно скзаать, что «наш прибор предоставляет интерфейс USB» (и мы тут еще не говорим про контракты!). В части информационного интерфейса все становится еще тоньше, и, наверное, я сам пока тут плаваю в различении интерфейса, контракта и протокола (протокол — это синоним кого-то из первых двух или нет? пока не знаю).

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

            А вот тут вы используете эти три термина, но я как раз и уверен, что понимаю верно. Возможно, ваш термин «разъем» как раз и соответствует моему термину «интерфейс» (или «тип интерфейса»). А «контракт» в моем смысле — вовсе не значит «исчерпывающий и 100% что-то гарантирующий контракт», он может быть и частичным, просто это то, что не обеспечивается интерфейсом «физически»/»статически», и нужно обговорить отдельно, и будет проверено только в динамике взаимодействия.

            * Простым и надёжным способом является номинальная стыковка, когда соответствие интерфейсов указывается ссылкой на один источник, а это — тот самый импорт, явный или неявный.

            Вот «импорт» — я бы оставил это слово для связи модулей, а к вашей реплике я бы сказал, что наверное да, просто указание на другой _компонент_ является самой примитивной формой интерфейса. «У меня есть имя, и этого достаточно чтобы начать взаимодействовать». Да, указание имени, или адреса, или почтового ящика, что-то такое. Кстати, у А.Н. в Вире так и работает — указывается относительный путь адресата и это все, максимально широкий информационный интерфейс. Максимально гибкий. Ну, впрочем, в JavaScript и прочих совсем-динамических языках (SmallTalk? не знаю про него почти ничего) примерно то же самое, только все-же «ссылку» ты должен получить откуда-то.

            Обратите внимание: вы один раз делаете импорт модуля, а потом связываетесь с десятью разными компонентами (экземплярами, объектами) того «вида» (чтобы не говоить «типа»), который был описан в этом модуле. Ну, или просто похожими по поведению, если вы завязывались только на интерфейс или только на имя. Но все же разница в отношениях кратности 1:1 и 1:10 должна быть заметна. Вам все же обычно недостаточно имени _типа_, вам нужна еще и _ссылка_ (или имя экземпляра), чтобы начать взаимодействие!

            Еще раз сорри за длинные тексты, но боюсь, что короче не смогу выразить важные смыслы.


  4. Поясните, что вы имеете ввиду, говоря слово «воспроизводить»?

    «Дело не в эффективности исполнения, а в эффективности написания — сослаться на что-то проще, чем воспроизводить, пусть это и только интерфейс.»

    Возможно, что наше взаимное непонимание — это следствие терминологической путаницы.

    Ответить

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

      Ответить

      1. Обращение к ним происходит по определённому интерфейсу -> Обращение к ним происходит по определённым интерфейсам

        Без этой правки может быть неясно, что слова «определить» и «воспроизвести» относятся к интерфейсам

        Ответить

  5. @Игорь

    * Вместо этого термин «модуль» в SE приобрел совсем другое значение, означает другое понятие, и я с вами согласен, что это не здорово, но он уже закрепился за 60-70 лет достаточно прочно, никуда от него уже не деться. Поздно сетовать, что в 1960-х пионеры-практики взяли это слово.

    Вроде бы, как раз в те времена пионеры вполне придерживались устоявшейся инженерной философии.

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

    * (из смежного раздела «Архитекторы и разработчики») К сожалению, я плохо знаком с наработками в мире Оберона и Компонентного Паскаля, глядел только по диагонали, но то, что я видел, уверяет меня в обратном. Компоненты в том смысле, в котором ввожу это понятие я, там тоже отсутствуют почти полностью, как и в остальном мире ООП — то есть они есть!, но только «одноуровневые». Отношение «часть-целое» отсутствует, удобных способов вкладывать типы и компоненты на несколько уровней — нету. Я был бы рад ошибиться в этом, и был бы признателен за пример кода, показывающий обратное.

    На примере Паскаля как раз хорошо видна суть SE, задуманная в те года пионерами. Ввели модульность — повторно используемые функции выделяются в отдельные модули, обозначаемые как unit, готовое изделие — program:
    https://wiki.freepascal.org/Unit

    Модуль декларирует интерфейс — «предлагаемые» функции (с сопутствующим определением типов данных, констант), язык предусматривает правила их использования. Модуль может использовать (uses) другие модули (для реализации функций). Программа (program) задаёт непосредственную модель конечного изделия — содержательный текст программы (как и текст реализации функций/процедур) по сути есть модель композиции функциональных компонентов (в т.ч. иерархическая) дискретной системы в форме управляющих конструкций.
    В program можно уточнить/выбрать конкретный вариант модуля для сборки программы через спецификатор «in» вида:
    » uses SomeModule in ‘filename’ »

    Затем в языке в некотором смысле увеличилась гранулярность модульности — функции начали объединять в привязке к типам — возникла «объектность» (с расширением правил использования функций для динамической диспетчеризации и пр.).

    Развитие Оберона, в частности по линии Активного Оберона, представлена в этой презентации:
    https://oberoncore.ru/_media/library/friedrich_presentation_at_workshop_on_system-on-chip_design_21-23_10_2013.pdf

    Особое внимание обращает на себя такая форма функциональных компонент как «активные ячейки». Объекты типа «cell» декларируют входные/выходные порты и содержат процедурное тело. Сборочные программы в виде особого модуля/типа «cellnet» являются алгоритмами композиции «вычислительных ячеек» (создание ячеек и соединение портов) — построение сети (в т.ч. и иерархической композиции) компонент (аля data-flow на FPGA — «процессоры», соединенные «проводами»).

    В презентации также представлены некие эксперименты с концепцией «ячеек» для C#. Есть почва хоть немного оценить потенциальную перспективу, хотя бы представить, насколько утомительно или нет, к примеру.

    Вопрос «правильной» организации компонент, конечно же, далеко не простой, особенно в динамике системы, с учётом текущей алгоритмической ситуации, динамической сменой модулей в runtime.

    Однако, полвека назад пионеры в SE, вроде бы, как раз были в рамках «системного мышления».

    Ответить

    1. @PSV100

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

      Теперь смотрите ключевое: в сфере ПО то, что называют модулем — НИКОГДА не выполняет НИКАКИХ функций во время работы системы. Потому что то, что было модулем, еще несколько раз преобразуется, сливается/разделяется, перетранслируется/перемешивается, и реально работающая вычислительная машина выполняет что-то «очень свое». Вот той «полной по-атомной идентичности» того, что купили как модуль (насос) и того, что работает как насос (в конкретный момент времени) — ее нету. Например, вы написали функцию, а JIT ее взял и за-inline’ил! Ну и вообще — то, что вы купили (текст на ЯП) совсем не похоже на то, что исполняется. Итого: «Программный Модуль» в сфере ПО как понятие — сохраняет унаследованный от «железного модуля» смысл логистической/сборочной единицы, но утрачивает смысл «исполнителя тех или иных функций», т.к. реально этот исходный «программный модуль» диссипирует по дороге к реальному исполнителю. Впрочем, на практике, нас это не очень волнует, т.к. есть некоторые сильные гарантии того, что то, что исполняется, будет ЭКВИВАЛЕНТНО тому, что было написано в модуле. Программный модуль оказывается только моделью/инструкцией для автоматической генерации того реально исполняющегося машкода (может, смешанного из разных модулей), которого уже никто не увидит. При этом, что интересно, средства отладки позволяют ОТНЕСТИ то, что исполняется, к текстам исходных модулей (но до некоторых пределов, и далеко не всегда, и неспроста
      только при наличии дополнительной отладочной информации).

      Ответить

    2. @PSV100

      * На примере Паскаля как раз хорошо видна суть SE, задуманная в те года пионерами. Ввели модульность — повторно используемые функции выделяются в отдельные модули, обозначаемые как unit.

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

      На мой взгляд, для полноценной работы в парадигме компонент нужно обеспечить возможность их многоуровневой вложенности с обеспечением герметичности. Т.е. если компонент А1 — это внутренняя деталь компонента А, не предназначенная для использования нигде, кроме как внутри А — то и не может никто до этого А1 добраться.

      Когда вы в процедурном ЯП делаете вложенные процедуры/функции — это настоящая компонентность с хорошей герметичностью. Вы во вложенной процедуре можете сделать еще три вложенные процедуры, и внутри них — еще. И свойство герметичности будет повторяться сверху-вниз точно как надо! И это — принцип, необходимый для управления сложностью в масштабе. Собственно, Дейкстра его декларировал — любые программные конструкции могут вкладываться друг в друга, и при этом не будут доступны извне.

      Но даже в процедурных языках было бы просто НЕУДОБНО вкладывать процедуры друг в друга на три уровня вглубь. НИГДЕ не делали (ну, или я не знаю) возможности вынести эти, в истинном смысле дочерние под-модули, в отдельные файлы, но при этом по-прежнему сохранить гарантии того, что к ним никто не доберется, кто не должен.

      Модули и их зависимости — совсем не решают эту проблему. Для вашего компонента А вы делаете какой-то модуль U (с процедурой, или с классом), а для дочернего компонента — вы делаете отдельный модуль U1, с отдельной процедурой/классом A1, U импортирует U1, А использует A1 (может, даже _инстанцирует_ объект, если это был класс). Все раьботает. Bingo! Вот только вы полностью утеряли качество герметичности. И теперь любой проходимец B может точно также заиспользовать это компонент А1, который, вообще-то должен был быть только внутренней частью А! Гарантии, которые дает язык на эти взаимодействия — очень слабые!

      Процедуры еще хотя бы кое-как можно вложить друг в друга на несколько уровней (хотя придется терпеть сильное неудобство из-за невозможности разложить по отдельным файлам). Класс со скрипом можно вложить в другой класс (тоже сильное неудобство и partial classes, к примеру из C#, помогают, но очень слабо). Но вы ВООБЩЕ не можете вложить внутрь класса ВСЁ то разнообразие, которое у вас есть снаружи — любые типы/функции/классы/интерфейсы (интерфейсы — тоже очень важно!), чтобы организовать внутри некоторого компонента настолько же богатый и сложный мир, полностью скрытый от посторонних посягательств. По крайней мере в тех ЯП, про которые я что-то знаю. (Ниже поговорим про ActiveCells в АктивномОбероне).

      Ответить

    3. @PSV100

      Огромное вам спасибо за эту презентацию и наводку на «ActiveCells». Мои краткие набеги на материалы по Oberon не выводили меня на эту штуку. Кроме презентации легко находится и работа конкретно про ActiveCells:
      http://people.inf.ethz.ch/felixf/pdfs/2012_ActiveCells.pdf
      с текстовым описанием.

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

      А вот про ActiveCells… Они ОЧЕНЬ похожи на самые настоящие компоненты с хорошей герметичностью и с хорошими гарантиями. Это прямо замечательно, и картинки про них — тоже очень хорошие, типичные компонентные UML-диаграммы, которые и я рисую в своих набросках.

      Интересно при этом, что на уровне реализации иерархия CellNets превращается (flattening) обратно в плоскую структуру (как современные ООП-программы), но это совершенно не важно, т.к. язык позволяет выразить иерархию вложенности, отношение часть/целое, и существуют гарантии того, что эти границы не будут нарушены при последующих преобразованиях, а значит, все, происходящее в runtime можно ОТНЕСТИ обратно к исходной иерархической структуре вложенных CellNets.

      При этом, надо сказать, что для ActiveCells выбраны достаточно ограниченные возможности по взаимодействию, и, на мой взгляд, это уже дополнительные, не всегда нужные ограничения. Например, их обмен только асинхронный (как в «ООП Алана Кея» — объекты, обменивающиеся сообщениями), и, как я понял, они не могут вызывать синхронные команды, ожидая ответа. Но иногда синхронные вызовы может быть вполне адекватным решением, так что если говорить про языки общего назначения, я бы там ожидал и синхронных, и асинхронных взаимодействий.

      Очень прикольно выглядит описание CellNet «Calculator» или «Game» — ни да, ни взять, тот самый А-компонент, о котором А.Н. писал выше. Все, что делает — создает дочерние компоненты и связывает их между собой коннекторами. Вот только я думаю, что если такого будет много, то нужно делать специальный синтаксис, который был бы легче, с массой соглашений и умолчаний, чтобы в самых типичных 90% случаев все создавалось и соединялось «само». В остальном — очень похоже на то, что я описываю на псевдокоде в своей фантазии ChessApp (https://github.com/iamhere2/HLCD-Researches/tree/master/ChessApp).

      Что касается их варианта на основе C# — ActiveCells, то он выглядит как «нашлепка сверху», а хотелось бы, конечно, чтобы components/cells были более плотно в языке, переиспользовали существующие концепции (того же интерфейса, например), и могли использоваться в бОльшем числе ситуаций (в т.ч. пресловутые синхронные обращения).

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

      Ответить

    4. @Игорь

      * Теперь смотрите ключевое: в сфере ПО то, что называют модулем — НИКОГДА не выполняет НИКАКИХ функций во время работы системы. Потому что то, что было модулем, еще несколько раз преобразуется, сливается/разделяется, перетранслируется/перемешивается, и реально работающая вычислительная машина выполняет что-то «очень свое». Вот той «полной по-атомной идентичности» того, что купили как модуль (насос) и того, что работает как насос (в конкретный момент времени) — ее нету.

      * Что интересно, существенная часть презентации как раз наглядно показывает то, что я писал выше: от программного модуля как такового во время исполнения может ничего не остаться. Тут это доведено до прекрасной крайности: на основе этого модуля с кодом на ЯП высокого уровня генерится реализация в виде FPGA! Вот уж точно — ничего не осталось и целевую функцию выполняет явно не тот-самый-модуль, который мы написали, а кто-то совсем-совсем другой…. Ну да ладно, это все добавка к предыдущему тезису…

      Например, в тех же Оберонах существуют платформы в том числе с динамической загрузкой/выгрузкой модулей (аля DLL, не на FPGA и модули не вида «cellnet»). Т.е. понятие модуля (или ссылки на него) может быть и частью модели программы.

      Однако проблематика в ином. Видимо, необходимо уточнить, что есть «производство в сфере ПО».

      Компьютер как информационно-техническая система есть способ реализации природного «кибернетического гомеостаза» («кибернетический цикл», атрибут именно информационных систем, фактически, их суть), можно уточнить по Глушкову — двух автоматов: управляющего и операционного.
      Вот здесь есть хорошая картинка для «высокоуровневого» представления иерархии уровней или слоёв, страт, выделяемых в системе «компьютер»:
      https://panchul.livejournal.com/469643.html?thread=19173003

      На каждом уровне существуют свои языки моделирования (понятия тезауруса языка основываются на семантических свёртках понятий на уровнях ниже). Вот здесь есть пример диалекта языка «логических элементов», в данном случае в основе песок или сыпучие элементы:
      http://zhurnalko.net/=nauka-i-tehnika/tehnika-molodezhi/2005-07—num30

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

      Причём в производстве ПО также существуют множество уровней со своими языками.
      Ниже пример на базе методики вновь от пионеров полувековой давности, от инженерной школы упомянутого Глушкова — см. пример про факториал:
      https://alterozoom.com/ru/documents/18177.html

      Для построения программы расчёта факториала выделено несколько этапов проектирования — от постановки задачи до конечной программы через ряд семантических уровней (со своим языком). Форма моделей может быть не только в виде графсхемы (Р-схемы выше — это альтернатива блок-схемам, UML и т.п., по сути — трансформация графов переходов до упорядоченных графических структур, задающих последовательное, параллельное и вложенное соединение). Причём модели каждого этапа являются модулем. Здесь подробнее:
      http://emag.iis.ru/arc/infosoc/emag.nsf/BPA/e72abd849fe68a7dc32576eb0034c090

      Технология предполагала множество видов моделей (от документации до программ с техническими чертежами). Модель оформляется как модуль (стандартизированной структуры), модули компонуются иерархически (системы раскладываются на подсистемы и т.д.). А также кроме иерархии модули могут быть связаны горизонтально по «контурам» (аналог уровней/слоёв в модели компьютера выше, выделяемых в подсистемах).

      Так, например, пусть создаётся проект упомянутой выше МКС — целевая система, которая должна содержать такие-то компоненты (такие-то блоки). Как только аспект рассмотрения системы приобретает технологический характер — возникают модули, пусть даже на самом верхнем уровне будут модели на естественном языке (вводное описание).
      Компонент системы любой природы — функциональная роль, как только возникает рассмотрение кандидатов реализации роли — возникает модуль. Даже вводное описание может быть различным, т.е. несколько альтернатив модуля (к слову, Р-технология предусматривала версионность и полное управление проектами уже в те года, когда ещё не было Git-ов с Redmine-ами. Свидетели Р-комплексов утверждали, что даже экономисты на предприятии оперировали графиками работ и своими расчётами, в предусмотренных для них АРМ).

      Ответить

    5. * Но даже в процедурных языках было бы просто НЕУДОБНО вкладывать процедуры друг в друга на три уровня вглубь. НИГДЕ не делали (ну, или я не знаю) возможности вынести эти, в истинном смысле дочерние под-модули, в отдельные файлы, но при этом по-прежнему сохранить гарантии того, что к ним никто не доберется, кто не должен.

      Можно пройтись по Паскалевской линии. В Паскале модули защищены в рамках секции implementation (где разработчик — полный хозяин в модуле). Позже в Delphi/FreePascal добавился более жёсткий доступ в разрезе типа, но это больше удобство для IDE (чтобы в автокомплите не вываливались лишние приватные элементы при реализации модуля, если курсор находится вне кода процедуры, связанного с типом). При желании вынести в отдельный файл ряд элементов можно использовать директиву include (в интерфейсе или реализации модуля), решение на уровне препроцессора, однако.

      В Modula 3 интерфейсы и модули были разделены, гибкое решение (в т.ч. и для решения задач полиморфизма в типах). Вроде бы, partial classes в C# — заимствование артефактов из Modula.

      В Ada есть непосредственно дочерние модули, которые могут друг друга ограниченно импортировать.

      Для Оберона были даже такие идеи — расширение или встраивание модуля, причём новый модуль мог одновременно и импортировать исходный:
      http://www1.chapman.edu/~radenski/research/papers/module.pdf

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

      * Класс со скрипом можно вложить в другой класс (тоже сильное неудобство и partial classes, к примеру из C#, помогают, но очень слабо). Но вы ВООБЩЕ не можете вложить внутрь класса ВСЁ то разнообразие, которое у вас есть снаружи — любые типы/функции/классы/интерфейсы (интерфейсы — тоже очень важно!)

      По линии Си-производного мейнстрима не так гладко с модульностью, часто есть отождествление с понятием классификации типов и объектов (тезаурус языка для программ предполагает в т.ч. тестирование, приведение типа и т.п.).
      Особенно концептуальная каша с этими так называемыми интерфейсами. Пример — условно новый в промышленности (т.е. современный, причём с Оберон-влиянием) Go, на котором делает акцент А.Н. как шаг вперёд в модульности/сборке. Вот здесь представлен случай концептуальной неразберихи, в результате утиная типизация срабатывает некорректно:
      https://github.com/golang/go/issues/34996

      Или вот «запечатанный» интерфейс (с непубличными элементами), в общем-то, и непонятно, как же в целом интерпретировать сам концепт (с названием «интерфейс») в разрезе модульности (понятие модуля имеется в языке — пакет).
      https://blog.chewxy.com/2018/03/18/golang-interfaces/#sealed-interfaces

      (видимо, таковы результаты попытки предельно уменьшить тезаурус языка).

      Ответить

    6. * При этом, надо сказать, что для ActiveCells выбраны достаточно ограниченные возможности по взаимодействию, и, на мой взгляд, это уже дополнительные, не всегда нужные ограничения. Например, их обмен только асинхронный (как в «ООП Алана Кея» — объекты, обменивающиеся сообщениями), и, как я понял, они не могут вызывать синхронные команды, ожидая ответа.

      Вроде бы, блокирующее ожидание есть, также как и тестирование портов на наличие данных.

      * Очень прикольно выглядит описание CellNet «Calculator» или «Game» — ни да, ни взять, тот самый А-компонент, о котором А.Н. писал выше. Все, что делает — создает дочерние компоненты и связывает их между собой коннекторами. Вот только я думаю, что если такого будет много, то нужно делать специальный синтаксис, который был бы легче, с массой соглашений и умолчаний, чтобы в самых типичных 90% случаев все создавалось и соединялось «само».

      Альтернатива — форма в виде системы уравнений. Пример иного dataflow — Lustre/Lucid Synchrone (модель вычислений несколько иная, чем в ActiveCells):
      https://cavale.enseeiht.fr/CAFEIN/IMG/pdf/wp1-lucid-synchrone.pdf

      Lucid Synchrone — диалект ML с конструкциями от Lustre, однако императива нет — он внешний, аля FFI и вне верификации системы (это модели для safety critical systems). Уравнения задают иерархическую структуру, все связи выстраиваются на основе переменных/параметров. Возможна графическая форма со всей полюсной структурой или как диаграмма состояний и т.п.
      Начальные значения задаются как «статические» параметры (не изменяются, они не являются транзактами), в виде (первый аргумент — статический):
      let node integr (static dt) x0 dx = …

      * В остальном — очень похоже на то, что я описываю на псевдокоде в своей фантазии ChessApp

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

      Ответить

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *