Исторический экскурс

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

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

Если мне задать вопрос — хорошо или плохо, что взаимодействие полностью динамическое, мой ответ будет: Да.

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

Но есть мысль, которая меня греет — переход от динамики к статике — это оптимизация. Недаром же стало модным gradual typing.

Вернусь к Виру. Все взаимодействие между инструментами в Вире идет через верстаки. Есть всего два вида взаимодействия — разослать уведомление и выполнить запрос. В привычном коде это выглядело бы примерно так:

  • верстак.разослать уведомление(«имя», …)
  • ответ := верстак.выполнить запрос(«имя», …)

где «…» обозначает дополнительные аргументы.

Инструмент всегда обращается к конкретному верстаку с неким именем, верстак проверяет, подключена ли к нему команда с таким именем. Если не подключена, то верстак игнорирует уведомление, а для запроса возвращает «ложь», как ответ на запрос. А если подключена — то передает управление тому инструменту, который подключился, и, для запроса возвращает ответ от инструмента.

  • Уведомление: Инструмент вызывает Верстак вызывает Инструмент
  • Запрос: Инструмент вызывает Верстак вызывает Инструмент возвращает ответ (через верстак)

На этапе подготовке к взаимодействию (как правило, при инициализации инструмента), команды инструмента подключаются к верстаку или верстакам. К каким верстакам, какие команды и под какими именами — все это задано в схеме  — в той части, которая соответствует инструменту.

Приведу пример. Есть текстовый редактор, который при любом изменении текста рассылает уведомление «Текст изменился» на некоторый верстак. К этому же верстаку могут быть подключены разные инструменты, например, инструмент «Проверка орфографии», у которого команда «Проверить орфографию» подключена к уведомлению «Текст изменился». А у другого инструмента к этому же уведомлению подключена другая команда, и он зажигает иконку «нужна запись». Уведомление рассылается всем подключенным инструментам, в отличие от запроса.

Как с этим работает конструктор (человек)? Ставит инструмент и задает в его настройках (в схеме),  необходимые «правила» взаимодействия. Далее запускает и смотрит — если не работает, включает «наблюдателя» (а это тоже инструмент), который показывает, как идут уведомления и запросы, куда доходят, а куда не доходят.

Что в этом подходе принципиально важно — разделение ответственности:

  • Верстак отвечает за взаимодействие. Это «роутер», настраиваемый схемой, то есть код всех верстаков одинаковый, настройка разная.
  • Инструмент отвечает за выполнение действий, но не за взаимодействие — вся настройка взаимодействия является внешней по отношению к нему.
  • Схема отвечает за структуру: иерархию верстаков и инструментов, и задает настройки взаимодействия. То есть, можно сказать, что есть структурная схема и схема настройки каждого верстака и инструмента.

Разделение ответственности дает гибкость, и это то, что нужно сохранить. Одновременно, хочется добиться большего контроля, в том числе, статического. То есть, все, что можно проверить во время разработки, должно быть проверено, в первую очередь: наличие инструментов и совместимость сигнатур команд. А все, что нельзя проверить в статике должно быть проверено в динамике.

Прерываюсь на этой очевидной мысли. Кажется, что надо начинать прототипировать, «иначе такое занятие будет пустою забавою».

 

 

2 комментария


  1. Добрый день.

    Интересная информация про Верстаки в Вире, не знал. Мне казалось, там было все несколько более прямолинейно связано.

    Но не является ли динамическая модель с посредником легко реализуемой добавкой к статической модели?

    Например, если мы используем один общий интерфейс типа
    interface IDynamic {
    call_action(actionName: String, Box parameters): Result<Box, Error>
    }

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

    С верстаками — я может не так слегка понял, но, опять-таки, в моем представлении это похоже на некоторый _паттерн_ устройства А-компоненты, которая содержит:
    — дочерний компонент Верстак, предоставляющий (provide) IDynamic с какой-то логикой динамической диспетчеризации
    — дочерние компоненты-деятели, требующие (consume) интерфейс IDynamic и подлюченные к Верстаку

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

    Если взять ваш пример, то вроде вы (конструируя компоненту Приложение) ведь сразу знаете, что именно компонент Редактор может породить событие «Текст изменился», а не какой-то другой. И мы знаем что именно компоненту «Проверка орфографии» это событие нужно как триггер, а также ему, вообще-то нужен доступ к тексту, чтобы его проверить. То есть соединять надо эти две компоненты, и зачем тут Верстак?

    Если ПроверкаОрфографии — универсальный компонент, то там будет какая-то развязка через АдаптерПроверкиОрфографии, а если не универсальный, то прям ПроверкеОрфографии вроде как нужен интерфейс «ИсточникТекста» типа такого:
    Интерфейс ИсточникТекста {
    Запрос Текст: Строка;
    Событие ТекстИзменился;
    }

    Но, может я чего-то недопонимаю. Главный тезис наверное в другом — вроде как на статике легко реализуется динамика, просто средствами самой статики + синтаксический сахар или макросы. А вот наоборот — на динамике сделать статику — кажется сложнее.

    Ответить

    1. Динамика и статика (кто более матери-истории ценен?), возможно, мы говорим о разном (несогласованность терминологии). Для меня главное — противопоставление динамики и монолитных программ. Монолитную программу практически невозможно разобрать на части (и переиспользовать части). Предельный случай — это С++ (по крайней мере, до модулей, которые появились только в С++20), но проблема есть и на модульных языках тоже, включая Тривиль. В динамических языках (типа JavaScript) проблемы разборки нет, но есть проблема связанная с тем, что выделенная часть будет корректно работать, только в окружении, которое неявно задано.

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

      Статическое же связывание (то есть прямой вызов или вызов без посредников между компонентами), это частный случай, которые не всегда возможен (компоненты работают на разных устройствах или компонента может быть удалена/замена в процессе выполнения), и более того, не всегда полезен. Посмотрите, например, на строку 46 в про.tri: //к.отладка := истина

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

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

      Ответить

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

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