Если Вы хотите поделиться опытом на примерах из прошлого — это тоже очень интересно. На современных, думаю, просто будет проще. И в смысле общепринятой терминологии, которая, вероятно, эволюционировала за эти годы. И технологий, которые многие из присутствующих "трогали руками" и сразу представляют, о чём идёт речь. И потому, что память о современном опыте гораздо свежее, чем о том, что делал два-три-четыре десятка лет назад.
В данном примере всё начинается с кнопки. То есть, пользователь инициирует обработку, нажимая кнопку. Вьюшка, вероятно, избавлена от ответственности диспетчирования экшенов — она дёргает делегат. А сама просто подписана на обновление состояния.
Далее, я вижу, что инфраструктурный адаптер ничего не знает о состоянии приложения и работает как сервис: отвечает, когда его дёргают, при этом никаких экшенов не диспетчирует.
Некий сервисный слой координирует обработку событий от пользователя и взаимодействие с инфраструктурой.
В этом небольшом примере не видно, к сожалению, как решено взаимодействие с инфраструктурой, которое инициируется не пользователем, а, например, пришедшим push-сообщением, попаданием в определённую геозону или встряхиванием. Или если произошла ситуация, когда OAuth access token оказался недействительным, и от пользователя требуется выполнить повторный логин.
Подозреваю, что связующим и координирующим звеном при этом выступает сервисный слой, которому поступают события и от пользователя, и от инфраструктурных адаптеров. При данном подходе, вероятно, приемлемо, когда состояние одной вьюшки может измениться по причине экшенов от разных юз-кейсов, но при этом требуется, чтобы экшн только порождал обновление состояния приложения (однократное), но не приводил к появлению новых экшенов в процессе обработки. Получается, что ответственность сервисного слоя — управление юз-кейсами отдельно взятых взаимодействий.
Есть такой момент, который вызывает у меня некоторые вопросы.
У нас есть вьюшки, который отвечают за взаимодействие с пользователем. Вьюшка подписана на обновления какой-то части дерева состояния. При наступлении нового состояния вьюшка перерисовывается. Действия пользователя с элементами управления вьюшки транслируются в экшены, которые диспетчируются в хранилище состояния.
Однако, кроме взаимодействия человека и вьюшки есть также инфраструктурные компоненты: они через сеть общаются с бэкендом, работают с сенсорами, хранилищем ключей, файловой системой. В браузере может быть взаимодействие с сервис-воркерами, локал-стораджем и т.д.
Следуя принципу SRP (который в конечном счёте сводится к римской максиме Divide et Impera), мы не хотим давать вью знать детали работы инфраструктурных адаптеров. И поэтому вместо их вызова напрямую общаемся через хранилище состояния. Это значит, что, скажем, API-адаптер тоже подписан на хранилище состояний и инициируется именно им. Когда запрос выполнен — он диспетчирует действие в духе TodoListReady(items: ...).
Возникает ситуация, когда поток "экшн" — "стэйт" — "экшн" — "стэйт"… начинает напоминать символ бесконечности, в центре которого находится хранилище состояний. То есть, разные компоненты, взаимодействующие с хранилищем, влияют друг на друга через него. Экшн от вью инициирует API, экшн от API приводит к изменению вью.
И тут есть выбор, кто должен знать обо всех этих экшенах и стэйтах.
Вариант 1. Вью диспетчирует экшэны, которые обновляют стэйт API-адаптера; адаптер диспетчирует экшены, которые обновляют стэйт вью. Вью и API-адаптер знают друг о друге (об экшенах и стэйтах друг друга).
Вариант 2. Вью диспетчирует только специфичные для себя экшены, но API-адаптер подписан на стэйт этой вью и знает, когда нужно делать запрос. После этого он диспетчирует экшн, который обновляет стэйт именно этой вью. Получаем, что вью не знает об API, но API-адаптер знает о вью.
Вариант 3. У вью и у API-адаптера есть свои непересекающиеся наборы экшенов и поддеревья состояния, на которые они подписаны. API и вью друг о друге не знают. Однако, редьюсеры знают, что такой-то экшен должен обновить состояние одновременно и вью, и API. Или, по-другому, что такой-то экшен вью должен не только обновить состояние этой вью, но ещё и диспетчировать экшн API-адаптера. А ответный экшен API-адаптера должен также диспетчировать экшн вью с результатом запроса.
Вот здесь, на мой взгляд, как раз и кроются различные трейд-оффы этой архитектуры. С чем удобнее работать, что проще отлаживать, какие специальные приёмы для документирования и отладки, особенно если это происходит в команде? Где в данном случае те самые точки опоры, которые позволят применить SRP по адресу и перевернуть Землю?
Основная проблема максимы Divide et Impera в мире IT заключается в том, что она была хорошо применима, когда составные части были географически и политически обособлены и имели high cohesion "из коробки". Таким образом, принцип работал на обеспечение low coupling. Однако, в программировании у нас нет такой роскоши, как high cohesion из коробки. И часто, добавляя желаемую обособленность составляющих частей, мы также и нарушаем cohesion, не желая этого. С этим мы боремся с помощью автоустаревающей документации, информации о типах, облегчающих перемещение в IDE, общения у маркерной доски...
Вот интересно, кстати, существует ли какой-то наглядный способ представлять эти action-state flow в виде диаграмм, который себя оправдывает в реальном проекте?
Druu, фротнэнд в том виде, в котором мы его сегодня знаем, появился не 50 лет назад. Меня интересует опыт применения этого подхода именно в контексте современного фронтэнда: веб и особенно iOS c Android-ом.
Речь идёт о том, что при реализации всегда можно выбрать между двухсторонним связыванием a-la AngularJS, всякими FRP-имплементациями вроде ReactiveX и подходом, который используют Elm, Redux и прочие. Вот именно причины этого выбора и сделанные выводы мне интересны.
Меня этот подход интересует с точки зрения применения во фронтэнде несколько иного рода: в iOS и Android. Пока что мне нравится (ReSwift), но я ещё не наступил на все положенные грабли.
Кстати, тот самый главный редьюсер — он ведь действительно выполняет только одну задачу: роутинг экшенов в соответствующие специфические редьюсеры, которые уже и обновят хранилище состояния. В этом смысле его роль ничем не отличается от роли роутера в рельсоподобных фрэймворках.
Опять же, я предпочитаю делать редьюсеры чистыми функциями. То есть, туда попадают, по сути, не экшены в семантическом смысле этого слова, а уже принятые решения об изменении состояния. Задача же принятия решения и осуществления необходимых для этого взаимодействий с инфраструктурой — это ещё одна отдельная ответственность, которой должен заниматься другой код. Не уверен, что все во фронтэнд-сообществе придерживаются такого подхода.
Правда, я встречался и с мнением, что UDF (именно в контексте статически типизированных языков) затрудняет прослеживание логики выполнения программы. С другой стороны, архитектура — это не про Священный Грааль, а про разумный выбор альтернативы. Поэтому просто нужно знать, что мы выигрываем, а что проигрываем при принятии определённых архитектурных решений.
Поэтому и интересно мнение людей с опытом. Кто, как ни такие люди, могут рассказать о том, что и на что они променяли, выбрав такую архитектуру. И оправдался ли их выбор.
VladVR Владимир, спасибо за статью!
Расскажите, пожалуйста, подробнее, что Вы всё-таки думаете даже не о Redux в частности, а о unidirectional data flow вообще? Аргументы "за" и "против", юз-кейсы, когда это стоит использовать, а когда — нет. Какие альтернативы? Очень интересно услышать Ваше мнение.
Почему возможности опасны? Это один из базовых принципов построения архитектуры: ограничивать спектр возможностей, но не исключая их, оставляя выбор конкретных решений разработчикам этих решений, следующих данной архитектуре.
Ведь в конце концов, если вернуться к истокам этой аббревиатуры, REST — это архитектурный стиль. То бишь, поименованный набор архитектурных ограничений (см. диссертацию Филдинга). У этого архитектурного стиля есть определённый и хорошо описанный контекст, набор достоинств и отстоинств (в той же диссертации).
GraphQL — это же не архитектурный стиль. Почему мы их сравниваем? (Или я ошибаюсь?)
На мой взгляд, у REST есть ещё один замечательный юз-кейс в современных микросервисных системах, который на момент написания диссертации ещё не существовал. Если ввести дополнительное ограничение и оставить только безопасные и идемпотентные методы — тогда он отлично будет применим в CQRS/ES для моделей для чтения.
В данном случае задачу можно разделить на вычисления и эффекты. Для реализации необходимых эффектов мы просто используем соответствующую монаду. Или их комбинацию.
Что мы получаем:
ключевая бизнес-логика процесса явно читается из кода
вычисления соответствуют принципу Single Responsibility, легко тестируются и распараллеливаются
каждый из эффектов реализован и оттестирован в одном месте — соответствующей монаде.
Если принять, что мы при этом использовали подход Domain Driven Design, то часть сложных эффектов явно выходит за рамки нашего Bounded Context и у нас сводится к публикации соответствующего Business Event.
Как-то так. Кстати, если мы про реальный мир, то там не обойтись без обработки ошибок. Добавим:
пирог
.map(УпакованныйПирог(пирог))
.recover{
case Подгорел() => ....
case Непропёкся() => ...
case ОтключилиЭлектричество() =>...
case НахамилНачальник() => ...
default => ...
}
Ну, вы понимаете. Опять же, логика читается из кода. Я за это очень ценю такой подход.
Оно, конечно, да. Однако, суть ФП не в программировании без эффектов, а в отделении эффектов от чистых вычислений. Они выполняют принципиально разные задачи, поэтому по отдельности с ними работать гораздо проще. Реюзать, тестировать, делать код ревью...
Не так давно я как раз подбирал подходящую аналогию, чтобы объяснить отличие императивного подхода от функционального. И нашёл её в школьном курсе математики.
Все мы с вами знаем, кто такие синус и косинус. И нам не приходит даже в голову пытаться их применять через определение. Мы просто знаем и пользуемся. Но в школе вхождение в эту тему имело очень ненулевой порог для многих.
Так и в функциональном программировании: однажды въехав в тему (например, монада State), мы начинаем её просто использовать везде, где она нужна.
С применением императивного же подхода нам гораздо легче начать, оперируя базовыми конструкциями, но при этом мы каждый раз воспроизводим эти конструкции от той самой базы.
Правда, в какой-то момент Буран пропал из виду.
Автоматика должна была принимать решение, по какой траектории заходить на посадочную полосу. Траектория строилась по стенке вертикального цилиндра, и Буран принял решение, которое в силу его малой вероятности народ на ВПП не рассматривал. Плюс низкая облачность.
А потом он «как большой утюг» бесшумно (потому что при посадке он — планер) вывалился из-под облаков буквально над группой ожидающих, с противоположного предполагаемому торца ВПП.
Автоматика посадила его настолько мягко, что тормозной парашют, который должен был сработать от обжатия стойки шасси, не сработал.
Безусловно, это пример успешной работы автоматики в автономном режиме. Но случись внештатная ситуация — и всё было бы иначе.
Кроме того, 737 и прочие садятся на автопилоте по глиссадному лучу, то есть, при взаимодействии с наземными системами. Кстати говоря, не знаю, как автопилот отрабатывает посадку при сильном боковом ветре. О посадке на реку Гудзон в автоматическом режиме можно даже не думать.
В истории космических полётов автоматически садился Буран, но в серийное производство система не пошла. К тому же, на случай внештатной ситуации его встречали пара истребителей. Да и живых людей на борту не было.
"Любой ночной поезд" — это тот единственный, который уходит из Таллинна днём и к полуночи прибывает в Питер?
Если Вы хотите поделиться опытом на примерах из прошлого — это тоже очень интересно. На современных, думаю, просто будет проще. И в смысле общепринятой терминологии, которая, вероятно, эволюционировала за эти годы. И технологий, которые многие из присутствующих "трогали руками" и сразу представляют, о чём идёт речь. И потому, что память о современном опыте гораздо свежее, чем о том, что делал два-три-четыре десятка лет назад.
В любом случае — опыт интересен.
В данном примере всё начинается с кнопки. То есть, пользователь инициирует обработку, нажимая кнопку. Вьюшка, вероятно, избавлена от ответственности диспетчирования экшенов — она дёргает делегат. А сама просто подписана на обновление состояния.
Далее, я вижу, что инфраструктурный адаптер ничего не знает о состоянии приложения и работает как сервис: отвечает, когда его дёргают, при этом никаких экшенов не диспетчирует.
Некий сервисный слой координирует обработку событий от пользователя и взаимодействие с инфраструктурой.
В этом небольшом примере не видно, к сожалению, как решено взаимодействие с инфраструктурой, которое инициируется не пользователем, а, например, пришедшим push-сообщением, попаданием в определённую геозону или встряхиванием. Или если произошла ситуация, когда OAuth access token оказался недействительным, и от пользователя требуется выполнить повторный логин.
Подозреваю, что связующим и координирующим звеном при этом выступает сервисный слой, которому поступают события и от пользователя, и от инфраструктурных адаптеров. При данном подходе, вероятно, приемлемо, когда состояние одной вьюшки может измениться по причине экшенов от разных юз-кейсов, но при этом требуется, чтобы экшн только порождал обновление состояния приложения (однократное), но не приводил к появлению новых экшенов в процессе обработки. Получается, что ответственность сервисного слоя — управление юз-кейсами отдельно взятых взаимодействий.
Есть такой момент, который вызывает у меня некоторые вопросы.
У нас есть вьюшки, который отвечают за взаимодействие с пользователем. Вьюшка подписана на обновления какой-то части дерева состояния. При наступлении нового состояния вьюшка перерисовывается. Действия пользователя с элементами управления вьюшки транслируются в экшены, которые диспетчируются в хранилище состояния.
Однако, кроме взаимодействия человека и вьюшки есть также инфраструктурные компоненты: они через сеть общаются с бэкендом, работают с сенсорами, хранилищем ключей, файловой системой. В браузере может быть взаимодействие с сервис-воркерами, локал-стораджем и т.д.
Следуя принципу SRP (который в конечном счёте сводится к римской максиме Divide et Impera), мы не хотим давать вью знать детали работы инфраструктурных адаптеров. И поэтому вместо их вызова напрямую общаемся через хранилище состояния. Это значит, что, скажем, API-адаптер тоже подписан на хранилище состояний и инициируется именно им. Когда запрос выполнен — он диспетчирует действие в духе TodoListReady(items: ...).
Возникает ситуация, когда поток "экшн" — "стэйт" — "экшн" — "стэйт"… начинает напоминать символ бесконечности, в центре которого находится хранилище состояний. То есть, разные компоненты, взаимодействующие с хранилищем, влияют друг на друга через него. Экшн от вью инициирует API, экшн от API приводит к изменению вью.
И тут есть выбор, кто должен знать обо всех этих экшенах и стэйтах.
Вариант 1. Вью диспетчирует экшэны, которые обновляют стэйт API-адаптера; адаптер диспетчирует экшены, которые обновляют стэйт вью. Вью и API-адаптер знают друг о друге (об экшенах и стэйтах друг друга).
Вариант 2. Вью диспетчирует только специфичные для себя экшены, но API-адаптер подписан на стэйт этой вью и знает, когда нужно делать запрос. После этого он диспетчирует экшн, который обновляет стэйт именно этой вью. Получаем, что вью не знает об API, но API-адаптер знает о вью.
Вариант 3. У вью и у API-адаптера есть свои непересекающиеся наборы экшенов и поддеревья состояния, на которые они подписаны. API и вью друг о друге не знают. Однако, редьюсеры знают, что такой-то экшен должен обновить состояние одновременно и вью, и API. Или, по-другому, что такой-то экшен вью должен не только обновить состояние этой вью, но ещё и диспетчировать экшн API-адаптера. А ответный экшен API-адаптера должен также диспетчировать экшн вью с результатом запроса.
Вот здесь, на мой взгляд, как раз и кроются различные трейд-оффы этой архитектуры. С чем удобнее работать, что проще отлаживать, какие специальные приёмы для документирования и отладки, особенно если это происходит в команде? Где в данном случае те самые точки опоры, которые позволят применить SRP по адресу и перевернуть Землю?
Основная проблема максимы Divide et Impera в мире IT заключается в том, что она была хорошо применима, когда составные части были географически и политически обособлены и имели high cohesion "из коробки". Таким образом, принцип работал на обеспечение low coupling. Однако, в программировании у нас нет такой роскоши, как high cohesion из коробки. И часто, добавляя желаемую обособленность составляющих частей, мы также и нарушаем cohesion, не желая этого. С этим мы боремся с помощью автоустаревающей документации, информации о типах, облегчающих перемещение в IDE, общения у маркерной доски...
Вот интересно, кстати, существует ли какой-то наглядный способ представлять эти action-state flow в виде диаграмм, который себя оправдывает в реальном проекте?
Druu, фротнэнд в том виде, в котором мы его сегодня знаем, появился не 50 лет назад. Меня интересует опыт применения этого подхода именно в контексте современного фронтэнда: веб и особенно iOS c Android-ом.
Речь идёт о том, что при реализации всегда можно выбрать между двухсторонним связыванием a-la AngularJS, всякими FRP-имплементациями вроде ReactiveX и подходом, который используют Elm, Redux и прочие. Вот именно причины этого выбора и сделанные выводы мне интересны.
Меня этот подход интересует с точки зрения применения во фронтэнде несколько иного рода: в iOS и Android. Пока что мне нравится (ReSwift), но я ещё не наступил на все положенные грабли.
Кстати, тот самый главный редьюсер — он ведь действительно выполняет только одну задачу: роутинг экшенов в соответствующие специфические редьюсеры, которые уже и обновят хранилище состояния. В этом смысле его роль ничем не отличается от роли роутера в рельсоподобных фрэймворках.
Опять же, я предпочитаю делать редьюсеры чистыми функциями. То есть, туда попадают, по сути, не экшены в семантическом смысле этого слова, а уже принятые решения об изменении состояния. Задача же принятия решения и осуществления необходимых для этого взаимодействий с инфраструктурой — это ещё одна отдельная ответственность, которой должен заниматься другой код. Не уверен, что все во фронтэнд-сообществе придерживаются такого подхода.
Правда, я встречался и с мнением, что UDF (именно в контексте статически типизированных языков) затрудняет прослеживание логики выполнения программы. С другой стороны, архитектура — это не про Священный Грааль, а про разумный выбор альтернативы. Поэтому просто нужно знать, что мы выигрываем, а что проигрываем при принятии определённых архитектурных решений.
Поэтому и интересно мнение людей с опытом. Кто, как ни такие люди, могут рассказать о том, что и на что они променяли, выбрав такую архитектуру. И оправдался ли их выбор.
VladVR Владимир, спасибо за статью!
Расскажите, пожалуйста, подробнее, что Вы всё-таки думаете даже не о Redux в частности, а о unidirectional data flow вообще? Аргументы "за" и "против", юз-кейсы, когда это стоит использовать, а когда — нет. Какие альтернативы? Очень интересно услышать Ваше мнение.
Почему возможности опасны? Это один из базовых принципов построения архитектуры: ограничивать спектр возможностей, но не исключая их, оставляя выбор конкретных решений разработчикам этих решений, следующих данной архитектуре.
Ведь в конце концов, если вернуться к истокам этой аббревиатуры, REST — это архитектурный стиль. То бишь, поименованный набор архитектурных ограничений (см. диссертацию Филдинга). У этого архитектурного стиля есть определённый и хорошо описанный контекст, набор достоинств и отстоинств (в той же диссертации).
GraphQL — это же не архитектурный стиль. Почему мы их сравниваем? (Или я ошибаюсь?)
На мой взгляд, у REST есть ещё один замечательный юз-кейс в современных микросервисных системах, который на момент написания диссертации ещё не существовал. Если ввести дополнительное ограничение и оставить только безопасные и идемпотентные методы — тогда он отлично будет применим в CQRS/ES для моделей для чтения.
В данном случае задачу можно разделить на вычисления и эффекты. Для реализации необходимых эффектов мы просто используем соответствующую монаду. Или их комбинацию.
Что мы получаем:
Если принять, что мы при этом использовали подход Domain Driven Design, то часть сложных эффектов явно выходит за рамки нашего Bounded Context и у нас сводится к публикации соответствующего Business Event.
Как-то так. Кстати, если мы про реальный мир, то там не обойтись без обработки ошибок. Добавим:
Ну, вы понимаете. Опять же, логика читается из кода. Я за это очень ценю такой подход.
Оно, конечно, да. Однако, суть ФП не в программировании без эффектов, а в отделении эффектов от чистых вычислений. Они выполняют принципиально разные задачи, поэтому по отдельности с ними работать гораздо проще. Реюзать, тестировать, делать код ревью...
Не так давно я как раз подбирал подходящую аналогию, чтобы объяснить отличие императивного подхода от функционального. И нашёл её в школьном курсе математики.
Все мы с вами знаем, кто такие синус и косинус. И нам не приходит даже в голову пытаться их применять через определение. Мы просто знаем и пользуемся. Но в школе вхождение в эту тему имело очень ненулевой порог для многих.
Так и в функциональном программировании: однажды въехав в тему (например, монада State), мы начинаем её просто использовать везде, где она нужна.
С применением императивного же подхода нам гораздо легче начать, оперируя базовыми конструкциями, но при этом мы каждый раз воспроизводим эти конструкции от той самой базы.
Итак, начнём.
Сложно?
Автоматика должна была принимать решение, по какой траектории заходить на посадочную полосу. Траектория строилась по стенке вертикального цилиндра, и Буран принял решение, которое в силу его малой вероятности народ на ВПП не рассматривал. Плюс низкая облачность.
А потом он «как большой утюг» бесшумно (потому что при посадке он — планер) вывалился из-под облаков буквально над группой ожидающих, с противоположного предполагаемому торца ВПП.
Автоматика посадила его настолько мягко, что тормозной парашют, который должен был сработать от обжатия стойки шасси, не сработал.
Безусловно, это пример успешной работы автоматики в автономном режиме. Но случись внештатная ситуация — и всё было бы иначе.
В истории космических полётов автоматически садился Буран, но в серийное производство система не пошла. К тому же, на случай внештатной ситуации его встречали пара истребителей. Да и живых людей на борту не было.
С удовольствием воспользуюсь одним инвайтом! Адрес: kreslavsky at gmail.com
Спасибо!