Как стать автором
Поиск
Написать публикацию
Обновить
0
0
kreslavsky @itollu

Пользователь

Отправить сообщение

"Любой ночной поезд" — это тот единственный, который уходит из Таллинна днём и к полуночи прибывает в Питер?

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


В любом случае — опыт интересен.

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


Далее, я вижу, что инфраструктурный адаптер ничего не знает о состоянии приложения и работает как сервис: отвечает, когда его дёргают, при этом никаких экшенов не диспетчирует.


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


В этом небольшом примере не видно, к сожалению, как решено взаимодействие с инфраструктурой, которое инициируется не пользователем, а, например, пришедшим 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), мы начинаем её просто использовать везде, где она нужна.
С применением императивного же подхода нам гораздо легче начать, оперируя базовыми конструкциями, но при этом мы каждый раз воспроизводим эти конструкции от той самой базы.


Итак, начнём.


def разогретьДуховку: (Духовка, T) => Future(РазогретаяДуховка)

def подготовитьПротивень: (Противень, Мука) => Future(ПодготовленныйПротивень)

def подготовитьОсновуДляТеста: (Мука, Сода, Соль) => Future(ОсноваДляТеста)

def приготовитьЗаправку: (Масло, Сахар, КоричневыйСахар, Яйца, Бананы) => Future(Заправка)

def приготовитьТесто: (ОсноваДляТеста, Заправка, Кефир, Орехи) => Future(Тесто)

def выпечь: (ПодготовленныйПротивень, РазогретаяДуховка, Тесто, Время) => Future(ГорячийПирог)

def остудить: (ГорячийПирог, Полотенце, T) => Future(ГотовыйПирог)

let холоднаяДуховка = Духовка()
let чистыйПротивень = Противень()
let масло = Масло(450г)
let сода = Сода(1ч.л.)
let cоль = Соль(1ч.л.)
... etc

let пирог = for {
    духовка <- разогретьДуховку(холоднаяДуховка,175)
    противень <- подготовитьПротивень(чистыйПротивень, Мука(50г))
    основа <- подготовитьОсновуДляТеста(...)
    заправка <- приготовитьЗаправку(...)
    тесто <- приготовитьТесто(основа, заправка, Кефир(200г), Орехи(50г))
    горячийПирог <- выпечь(противень, духовка, тесто, 30мин)
    пирог <- остудить(горячийПирог, Полотенце(), 25)
} yield пирог

Сложно?

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

Безусловно, это пример успешной работы автоматики в автономном режиме. Но случись внештатная ситуация — и всё было бы иначе.
Именно то, для чего они созданы.
Кроме того, 737 и прочие садятся на автопилоте по глиссадному лучу, то есть, при взаимодействии с наземными системами. Кстати говоря, не знаю, как автопилот отрабатывает посадку при сильном боковом ветре. О посадке на реку Гудзон в автоматическом режиме можно даже не думать.

В истории космических полётов автоматически садился Буран, но в серийное производство система не пошла. К тому же, на случай внештатной ситуации его встречали пара истребителей. Да и живых людей на борту не было.
Здравствуй, Paulkin!

С удовольствием воспользуюсь одним инвайтом! Адрес: kreslavsky at gmail.com

Спасибо!

Информация

В рейтинге
Не участвует
Откуда
Москва и Московская обл., Россия
Дата рождения
Зарегистрирован
Активность