Push/Pull/Events модели#
Регулярно повторяющаяся задача проектирования — выбор способа взаимодействия вызывающего и вызываемого кода.
Pull-модель
— “вам надо, вы и мучаетесь”. Часто реализовывается проще. Вызывающий код в том месте, в котором удобно, запрашивает данные.
Примеры
Все загрузки, всё определение конфигурации и т.п. - размазано по коду и делается ad hoc.
Push-модель
— “то же самое, но вывернутое наизнанку”. Вам приходит callback/event/change propagation “blabla setting changed old->new”. В системе жёстко вшито, когда именно происходит событие, и имеется возможность добавить свою реакцию на него.
Примеры
Все загрузки, конфигурации, управление ресурсами - вынесено наружу, делается более-менее централизованно. Вы регистрируетесь где попало и реагируете на обновления. Зато система без вас знает, когда, как и что делать.
Место определения колбека отделено от момента его вызова, вызывающий код должен понимать контекст, в котором будет вызван колбек — необходимо ли проверять на существование необходимые колбеку ресурсы, доступны ли они или заняты, безопасно ли создавать или удалять какие-либо типы объектов. Один из самых примитивных примеров — инвалидация итератора stl-контейнера в цикле в c++.
Events-модель
В push-моделях можно встретить элементы pull-модели:
- если объём данных для push большой и может быть не нужен клиентам полностью или сразу, система может только уведомить об изменившихся данных (послать сообщение) и предоставить pull-интерфейс для запроса этих данных, когда будет удобно клиенту
- если удобно отложить обработку данных, коллбек может вместо выполнения работы положить сообщение в очередь. Дальше на клиента ложиться задача спуллить сообщения из очереди и обработать их. Удобно, если необходимо обработать все сообщения вместе.
Примеры
The C10K problem — взаимодействие большого количества сообщений в ОС.
Managing Coupling Part 2 — Polling, Callbacks and Events — о push/pull/events модели от дизайнеров движка Stingray
Паттерны/идиомы/узоры#
В различных языках или библиотеках идеи и модели могут быть:прозрачные для пользователя
— доступны в виде примитивов языкавыражаемые формально
— для использования можно переопределить функцию для своего типа/отнаследоваться от языка/написать синтаксический макрос/воспользоваться принятым в языке способомвыражаемые неформально
— для использования необходимо каждый раз повторно реализовывать функционал
Когда-то паттернами считались процедуры (1957, Design Patterns in Dynamic Programming) и классы (1972, Design patterns of 1972). Прозрачные для пользователя практически не обсуждаются (или даже не называются!), формально выраженные также часто принимаются как правило хорошего программирования. Разве что когда кому-нибудь не нравится производительность или ограничения в интерфейсе. В этом случае даже прозрачные идиомы могут быть реифицированны и переделаны. Пример — Runtime-полиморфизм в C++, альтернативный полиморфизм.
Может показаться, что называть что-то очень простое и примитивное паттерном слишком сёрьзно, но это всего лишь ярлыки.
Ideas for a Programming Language Part 4: Reactive Programming — про поиск синтаксиса для того, чтобы ввернуть асинхронное программирование в язык прозрачно для пользователя
Итераторы, колстеки, корутины, файберы, диспетчеры, асинки — revisited#
Две большие статьи выше посвящены, по большому счёту, паттернам работы с коллекциями в различных языках (где-то эти “паттерны” уже вшиты в примитивы языка, где-то их нет и в стандартной библиотеке и приходилось писать самому).
Всё описанное в статьях можно классифицировать по 4 группам:операции с pull-итераторами в синхронных коллекциях
операции с push-итераторами в синхронных коллекциях
операции с pull-итераторами в асинхронных коллекциях
операции с push-итераторами в асинхронных коллекциях
pull-итераторы — c++ stl-итераторы, IEnumerator
push-итераторы — колбеки
синхронные коллекции — контейнеры
асинхронные коллекции — генераторы (могут описывать как space-distributed коллекции, так и time-distributed)
Как и в общем для pull-моделей, pull-итераторы проще для использования/комбинирования — результаты, возвращаемый pull-итератором — стандартные типы языка, с которым учатся работать с самого начала знакомства с языком.
Push-итератор не возвращает ничего, и является отложенным вычислением (замыкания/указатели на функции/функциональные объекты). Отложенные вычисленя тоже можно комбинировать, но вместо привычных прикладному программисту на императивных языках способов требуются привычные математикам.
“Modern” C++ Lamentations — про разницу в синтаксисе и времени компиляции между C++ и C# при работе с лямбдами.
“Transducers” by Rich Hickey — типичные проблемы комбинирования колбеков, как оторвать комбинирование вычислений от коллекций.
Даже просто запись цепочки выполняющихся друг за другом после окончания предыдущего колбеков превращается в анти-паттерн callback hell
(синтаксический сахар async/await - количество скобок уменьшается)
Паттерн Task
(где-то встречается название Future
) является обёрткой для асинхронной работы над примитивом. В статьях не хватает разбора способов асинхронной работы с коллекциями (синхронными и асинхронными).
Обобщения паттерна Task в C#
pull-based работа синхронными коллекциями
— если просто заменить Task<int>
на Task<IEnumerable<int>>
, то можно использовать await
для коллекции, но при этом все элементы коллекции будут собираться синхронно за один вызов, в чём собственно нет особого смыслаpull-based работа с асинхронными коллекциями
— IAsyncEnumerable<T>
и await foreach
позволяет получать элементы коллекции асинхронноpush-based работа с асинхронными коллекциями
— push-вариант работы с асинхронными коллекциями — паттерн Observer
(IObserver<T>
в C#). Если Task
представляет собой одно асинхронное событие, то Observable
— это асинхронная коллекция (источник) событий, на которые могут подписываться Observer
-ы.
Observable/Observer#
Observer
— давно известный паттерн, популяризованный в GoF, и активно используемый в smalltalk-версии паттерна MVC
.
How did MVC get so F’ed up? — деградация MVC в языках с не-observable примитивами в качестве модели, мешает композиции.
Observable
в C# зовёт 3 метода Observer
(продолжение/окончание/ошибка):
Observable
можно представить как обобщение примитивного обхода коллекции (синхронной или асинхронной). При этом, как и для примитивного foreach, так и любых более сложных реализаций, должно соблюдаться простое правило: OnCompleted/OnError — это последний колбек, после которого Observable не будет присылать других.
Паттерн сам по себе не защищен от возможностей кривой композиции, так что периодически появляются теоретические попытки сделать что-то более чистое (но не всегда более простое).
Deprecating the Observer Pattern with Scala.React от Мартина Одерски, умершая либа. Вместо неё сейчас и сам Одерски предлагает JavaRx, надстроенную над observer-ами.
Reactive-подход
Observer имеет те же особенности композиции, что и push-итераторы — близкие математикам или функциональным программистам примитивы, в C# местами завёрнутые в linq-синтаксис.
ReactiveX — библиотека реактивного программирования, набор примитивов для композиции и конвертеров между различными типами итераторов.
Introduction to ReactiveX in C++ (rxcpp) — презентация по С++ версии библиотеки, heavy templates-based.
Introduction to RX.Net — книга по C# версии.
GUI#
Можно разделить GUI-библиотеки на imperative/declarative
и retained/immediate/reactive
.
Imperative/Declarative#
Imperative
— установка состояние контролов описывается в кодеDeclarative
— установка состояния вынесена куда-нибудь в JSON/XML/HTML или в динамический язык (lua/squirrel — бестиповые таблицы + лямбды удобны для описания представления и кода) и загружается с помощью control->loadFromFile
. Из преимуществ — gui можно менять без перекомпиляции, программиста и с помощью тулзы, которая позволяет менять состояние мышкой.
WPF: контролы лишенные внешнего вида и неразрешимая задача выбора конфигурации темплейта — более развернуто про то, что даёт декларативный подход
Разработчики хотят, чтобы библиотека «угадала» их представление о том, как должен выглядеть и как должен работать тот или иной элемент визуального интерфейса. Соответственно, разработчики, как пользователи библиотеки, оказываются не готовы, что вместо того, чтобы искать готовые компоненты, надо научиться создавать их самому в парадигме того инструментария и тех концепций, которые реализованы в библиотеке.
Retained#
Retained
— клиентский код не занимается рендером сам, а заполняет и обновляет модель. Push-модель, очень распространённая в GUI-библиотеках.
Пример, как может выглядеть код:
Минусы — код обновления состояния модели (всего, что вне GUI), нужно синхронизировать с состоянием GUI.
Примеры либ - cegui
Immediate#
Immediate
— Pull-подход (“вам надо, вы и рисуйте”), родившийся в головах гейм-девелоперов. Если “вывернуть” наизнанку логику, и отдать обязанность рисования контролов клиенту, то логика местами станет проще (вместо угадывания потребностей клиента сложность переезжает в клиентский код, и отпадает необходимость синхронизации состояния). Функции рисования не имеют своего состояния, а принимают указатели на данные, которые они могут изменить.
Примеры либ — Unity Imgui, Dear Imgui, Nuklear
.
Reactive#
Если под рукой есть язык или библиотека, который может описать зависимости между данными, то можно было бы устранить недостаток retained gui без перехода к immediate.
Примеры либ — knockout.js, rivets
для js, ReactiveUI для C#, rxqt для C++.
MIX11 Knockout JS Helping you build dynamic JavaScript UIs with MVVM and ASP NET — knockout.js demo
How dependency tracking works — knockout.js how it works
Knockout.js использует тривиальный динамический метод ослеживания зависимостей.
- Когда объявляется observable, вычисляется его initial-значение.
- В ходе вычисления, устанавливается подписка на любые другие observables (включая computed observables), значения которых читаются (язык должен поддерживать хук на чтение значений). Подписка означает, что будет вызвано вычисление этого observable (шаг 1), при этом любые старые подписки удаляются.
- После завершения вычисления, вызываются все подписки о том, что новое значение этого observable доступно.
Так что зависимости не вычисляются при первом запуске и их не нужно объявлять, они просто перерасчитываются каждый раз при обновлении, и могут изменяться динамически. Если эвалуатор observable не читает ни одного другого observable, от которого он зависит, то повторное вычисление никогда не произойдёт, и сам эвалуатор может быть заменён на вычисленное значение. - Декларативные биндинги данных к состоянию контролов — это просто computed observables.
Введение в ReactiveUI: прокачиваем свойства во ViewModel — В C# -> WPF + XAML бекэнд + RX.Net для описания зависимостей + ReactiveUI.Fody для генерации
Reactive Gui на C++ и геймдеве#
Систему реактивного GUI можно разделить на 4 части:
- язык реализации (и, возможно, расширения) системы (C++)
- язык для декларативного описания gui и байндингов контролов к данным
- язык для описания скриптового поведения контролов (если хотим декларативно описывать поведение — в C++ сложно с рефлексией и интерпретацией кода)
- тулза для визуального изменения декларативного описания
Model-View-ViewModel
The Elm Architecture
Примеры Reactive GUI для геймдева встречаются не очень часто, но встречаются.
imvue
imvue — минималистичный проект-пример
- sdl/glew/imgui в качестве бекэнда для рендера gui, плюс кодоген ооп-обёртки вокруг imgui
- lua в качестве скриптового языка и reactive-системы
- libcss — html/css декларативное описание, в стиле шаблонов vue.js
- без визуального редактора, так как игрушечная либа
xui
XUI — еще один демо-пример
- irrlicht в качестве бекэнда
- C++ clang-based парсер для рефлексии, rxcpp и cpplinq для реактивности
- xaml для декларативного описания, порт OmniXaml на C++
noesisengine
noesisengine — коммерческий проект
- свой рендер-бекэнд, рефлексия, редактор (+ экспорт из microsoft blend)
- xaml в качестве декларативного описания
daRg
Dagor Reactive GUI — гайдзиновский reactive gui
- своей рендер-бекэнд
- quirrel (свой порт squirrel) в качестве декларативного описания, рефлексии и скриптового языка
- свой визуальный редактор Dargbox