Всем привет!
На этой неделе с вами Алексей Демедецкий (@DAlooG). Team lead из Харькова. Сейчас работаю для компании Sigma Software. Немного моих работ: medium.com/@DAloG, github.com/AlexeyDemedets…, speakerdeck.com/dalog
Я занимаюсь мобильной разработкой больше 8 лет. Три из них я потратил на продвижение реактивного программирования. Последние 4 - однонаправленных подходов.
#день1 будет посвящен обучению и все что с ним связано. Расскажу свою историю выбора профессии и тех точек бифуркации которые я уже прошел.
#день2 будет про UI. Какие способы организации логики вокруг UI я знаю, какие считаю правильными и расширяемыми. Буду много говорить про data-driven ui и чем он отличается от declarative ui.
#день3 - redux и все что рядом. Расскажу про свою историю знакомства, какие боли я им решал и как эволюционировал мой подход к этой парадигме.
#день4 посвятим работе с side effects в data-driven архитектурах. Сеть, шифрование, диск, биометрия.
Пятница день тяжелый, поэтому #день5 будем обсуждать здоровье и хобби. Игры, спорт, ментальные практики.
#день7 хочу затронуть тему публичных выступлений. Как я готовлюсь к выступлениям, зачем я иногда езжу на конференции, и чему я учусь у других людей.
Какой технической теме уделить больше внимания?
Какой soft теме уделить больше внимания?
#день1 Выбор профессии. Я увлекся программированием еще в школе. Меня в это затянули школьные олимпиады по математике. В один день в 7 классе мне вручили учебник и сказали что через неделю я поеду на олимпиаду еще и по информатике. Больше было не кому особо.
Прочитав учебник я худо бедно начал писать примитивные программы в тетрадке и показывать их учителю. Он их читал и вроде как соглашался. Компьютера все равно не было, так что это были алгоритмы больше чем программы.
Свою первую олимпиаду в районе я выиграл ввиду отсутствия конкуренции. Задача была про какую-то трансформацию массива чисел. Решение было на листике, с блок схемой и "кодом" на паскале.
Потом была долгая подготовка к областной олимпиаде, с кучей задачек примерно в той же области. На областной олимпиаде я сел за IDE в первый раз вспоминая картинки из книги как то что то пытался написать. Ничего естественно не вышло, и выше какой-то там сотни я не поднялся.
Через год родители договорились про секцию в компьютерном классе в городе, и я стал кататься каждые выходные в эту секцию и переписывать свои "программы" с тетрадки в IDE. Выходные превратились в день сурка - 5 утра поезд, 3 часа пути, 2 часа занятия, в 5 вечера поезд обратно.
Сейчас это все мне кажется каким-то сюром, но тогда мотивация позволяла не думать о таких вещах. На волне моего увлечения мне скупали книги, которые я глотал в поездах и ожиданиях поезда. Но без доступа к компьютеру расти стало невозможно.
К счастью, это продолжалось не долго. К 10 классу у меня был свой компьютер, а в школе компьютерный класс. Благодаря этому я допрыгнул до своей самой высокой отметке в олимпиадном программировании - третьему месту на областной олимпиаде по информатике.
Это достижение дало мне право поступить в профильный вуз вне конкурса. Для меня это был счастливый билет и на следующие 3 года я мог забыть о проблеме выбора в своей жизни.
Пора бежать в детский сад за старшим, чуть позже продолжу рассказ и устрою тред с моими любимыми книгами.
#день1 Про образование. Мои первые курсы в институте это прекрасная эпоха познания. Все было интересно, все было нужно. Все проподователи виделись полубогами на своих олимпах.
Математика и физика, алгоритмика и ООП, микропроцессоры и операционные системы. Этих предметов хватало на обсуждение днями напролёт нашей небольшой компанией друзей. Ну а вечера и ночи беззаботно сливались на World of Warcraft.
Программы предметов были не самыми современными, но учителя компенсировали нам это курсовыми программами в вольном стиле.
Для физики мы напросились писать визуализацию функции распределения плотности вероятности электронного облака в атоме. На этой работе я познакомился с open gl, и даже сделал подобие рендеринга тумана. Старались тратить время постоянно. Но в итоге все равно кранчили сутки напролет
Для микропроцессоров - симулятор x86 процессора с конвеерами микрокоманд и предсказателем переходов. Было примитивно но безумно увлекательно.
Самый масштабный (команда на 4 человека) был про оптимизацию прокладки сетей в здании. На этом проекте я познал ад постоянно меняющихся требований. Спустя полгода никто из команды так и не понимал чего от нас хочет заведующий кафедры.
После 2 курса я отправился на поиски подработки. Своё первое собеседование я с треском провалил, но на втором мне дали задание написать калькулятор и отпустили на недельку. Задание я успешно выполнил, и на этом историю про мое образование можно считать оконченной.
Все остальные предметы закрывались за красивую зачетку и иногда коньяк. Предметы ставших курсов утратили очарование и чистоту математики, и их устаревшесть было все труднее не замечать. Вплоть до защиты диплома про ВУЗ я практически забыл.
#день1 Про развитие карьеры. Я работал в 3 с половиной компаниях за всю свою карьеру. Первая дала мне вырасти до уровня «хочу рассказывать как всем работать», вторая позволила это делать и вырости до уровня «хочу рассказывать бизнесы как им делать бизнес». Третья дала это делать.
В 21 год у меня даже была визитка System architect. Это ничего не поменяло в моей работе но я очень хотел и мне сделали. Сейчас смешно вспоминать свою молодость. Писать код я начал на чем прийдется и за бесплатно. Через 3 месяца мне дали первые деньги и попросили приходить почаще
Первый проект был подобием google maps для промышленности. Рисовать карты и точки на картах. Для Windows Mobile 6.5 SDK не было. Поэтому движок карт собирали сами на коленке. Все было очень корявым но родным и очень важным. Максимализм он такой.
Потом я бросался на Symbian, PalmOS и даже игры делал для iPhone. Для этого я ушёл в другую компанию где сидел и учил objective c. Там я понял что писать игры очень скучно если это чужие игры. И вернулся обратно но со знаниями objective c.
Так я втянулся в ios разработку. И начал втягивать в неё своих друзей и одногруппников. Мы вместе делали несложные проекты, ругались на код коллег, и в целом беззаботно «кодили».
Но зарплаты росли а вместе с ней и отвественности. Головной боли было все больше. Спойлер - это тенденция сохраняется все время. Поэтому вопрос «хочу ли я больше денег» меняется на вопрос «вынесу ли я ещё больше задач».
В новую компанию меня позвали после выступлений на локальном митапе. Я рассказывал что то про архитектуру. Никаких своих мыслей, просто пересказ чужих книг. Зарплата отличалась так сильно что сказать нет я был не готов.
В новой компании меня ждала команда интернов и задача по воспитанию из них middle ios разработчиков. Я так усердно взялся за эту задачу что через год они стали middle ios. Правда в других компаниях :)
Эксперимент повторять не стали, а просто добрали с рынка людей. В этот период я начал проводить много собеседований. Одно помню до сих пор - на вопрос о предыдущем опыте парень ответил что у него проблемы с органами внутренних дел из-за распространения пиратского софта.
В этот период я похоронил 5 или 6 проектов. Несмотря на все мои старания и расширяемость архитектуры, и самые современные технологии (ReactiveCocoa в бета стадии) проекты все равно закрывались.
Тогда я стал много думать и искать виноватых. Ими стали менеджеры :) Им это очень не понравилось. Где то год я изучал и формировал в своей голове формат самоорганизующихся команд и пытался продавать это текущей компании.
Ничего не вышло и я пошёл искать себе новый дом с четким списком требований. На это ушло где то полгода не слишком активных поисков. У меня было красивое резюме и список требований к компании. В один хороший день мне написали из Sigma Software и вот уже четвёртый год я тут :)
Мораль тут наверно простая - найти единорога намного проще когда ты не только знаешь как он выглядит но ещё и громко кричишь об этом на каждом углу. Ждать то, что подходит именно тебе и не бросаться на любую возможность - выгодно при игре в длинную.
#день1 Книги. Хочу поделиться книгами которые я люблю и считаю хорошими. Все таки книги это отличный источник развития и роста. Начнём с полки :) pic.twitter.com/mmVn9S0OMR
Совершенный код. То что я перечитывал три раза и возил в рюкзаке по 2 часа в день в метро :) Эту книгу стоит прочитать хотя бы ради навыка защитного программирования. pic.twitter.com/GsHZifvYgb
Справочник ассемблера. Не самая полезная вещь в нашем мире, но воспоминания очень тёплые :) pic.twitter.com/jMHA6L5FzH
Кнут. Самый фундаментальный труд который я видел. Для такой книги надо брать академотпуск на работе и снимать дачу на речке. pic.twitter.com/RNglpcALGE
Фаулер направлял меня в период моего роста как разработчика. Идея о том что код не бывает хорошим или плохим, а просто недоработанным. Эти книги научили меня писать плохо и улучшать непрерывно. pic.twitter.com/dYBwwZhnaV
Пошёл менеджмент. Обожаю эту книгу. Она про работу на результат а не процесс. pic.twitter.com/gtFnUFxcWz
Тоже прекрасная вещь. Научила меня видеть ценность в неудачах и экспериментах. Работать в условиях неопределённости и вечных изменений. pic.twitter.com/txmrHNZjoa
А эту книга толкнула меня на крайную авантюру - создать команду полного цикла - от бизнес анализа до инфраструктуры. pic.twitter.com/2AzJDzR2vi
Моя жемчужина. Книги про визуальное представление данных. С историей примерами и анализом. pic.twitter.com/k97afGTUdt
Ещё одна книга того же автора. Красота данных в чистом виде. pic.twitter.com/VTpoveGBRC
Последние 3 года я играю в Destiny. И обожаю их арт буки. Отдельное удовольствие от смешения реального и виртуального. pic.twitter.com/7HGmaHxod6
Так же стоят коллекционные книги от Diablo 3. Своего рода артефакты. Напоминание о времени которое я потратил на эту вселенную. Кстати моя первая программа была редактором Dave файла от второй Diablo. Я делал её все лето, но в школе мне никто не поверил. pic.twitter.com/BfutVwGELV
Трёхтомник истории и мифов Workd of Warcraft. Сантабарбара для гиков. pic.twitter.com/C6wdsN1lE7
Ещё я обожаю настолки и ролевые игры. Мечтаю собрать стабильную партию в D&D и найти на это время. pic.twitter.com/s19KSMeW74
Последняя гордость - подписанные альбомы прог рока Ayreon. pic.twitter.com/ToiEx3Ph9D
Не все книги у меня в физической копии. Вот из любимых цифровых.
Книга про структуру организаций. Как и зачем разные виды организации существуют, что нужно что бы перейти до одной к другой. pic.twitter.com/6mpYcNRp67
Если трудно работать с тестами - витайте эту книгу. По важности - рядом с совершенным кодом. pic.twitter.com/GL5QruYiPZ
Программирование типами это отдельная гимнастика для абстрактных мышц :) полезно для развития интуиции и ориентировании в разговорах о высоком. pic.twitter.com/OpWnisIIbi
Сегодня начинаем день под @muse music.apple.com/ua/album/simul…
#день2 Теория Data-Driven UI.
Если смотреть супер упрощенно, то наш UI data-driven если у нас есть функция как на картинке. Data это то что мы рисуем, UI это то что получилось. Вроде просто, но есть нюансы. pic.twitter.com/OTz9tN6vbu
Эта функция должна обладать несколькими свойствами.
Во первых она должна быть тотальной.
Другими словами, для любой возможной Data должен существовать корректный способ отобразить её в UI.
Например, для таких данных очень трудно написать тотальную функцию. Потому что состояний много, и корректно отобразить не получится. pic.twitter.com/4VJujmQBLz
Намного проще написать тотальную функцию для таких данных. Тут возможны только корректные состояния и в целом намного меньше места для ошибки. pic.twitter.com/Y1QuRl1Bhw
Второе обязательной свойство этой функции - это однозначность. Каждому UI должна соответствовать конкретная Data. Также вы можете щегольнуть, и называть такие функции биективными (ru.wikipedia.org/wiki/Биекция)
Ну и третье свойство - это размер Data. Чем он меньше - тем функция приятней. Посчитать размер довольно просто - структура (класс) дает нам умножение, перечисление - сложение. Вот примеры для двух версий Data. pic.twitter.com/0MI4RtT3NY
Реальный мир пока не позволяет нам создать чистую функцию от данных (SwiftUI, React, Flutter - позволяют). Поэтому наша красивая функция становится чуть сложнее pic.twitter.com/EBHHmHwC1W
Фактический мы ввели состояние в наш уютный мирок. Теперь наша функция может сказать как изменить UI с учетом того, что сейчас показывается. Тоже самое можно записать на вашем любимом языке в виде интерфейса с одним методом - render pic.twitter.com/8jXwvHG7sS
Итого. Мы хотим что бы наш UI выглядел как одна функция которая принимает в себя Data. Мы называем эту Data - Props (потому что React). Мы хотим что бы Props были как можно меньше, но при этом однозначно указывали на UI который должен получится.
#день2 Практика data-driven UI.
Собирать Props это самое сложное во всей практике. Будьте готовы к тому что вам будет неудобно довольно долго.
Использование enum (sealed class, union type) это обычное дело. Для людей которые не привыкли так моделировать - это challenge.
Props могут передавать не только состояния, но и обратную связь. Для этого используется абстракция Command. По сути своей это обертка над произвольным лямбда выражением. Для Android это еще дает мостик от async / await к главному потоку. pic.twitter.com/7nsK4bQfln
Команды могут быть опциональными, тогда состояние UI кнопки легко привязать к тому есть команда или нет.
В этом примере кнопка login может появится только когда там появится команда. pic.twitter.com/P8KAQsiD08
Props должны быть локальными. Это значит что у каждого UI они свои, и не имеют нечего общего с другими экранами. Даже если они похожи на 90%. Дублирование кода используется как буфер для изменений. Это позволяет вносить изменения и не ломать другие модули.
Рендеринг Props это одна функция. Я не рекомендую разбивать эту функцию на несколько. Большие функции могут быть признаком плохого кода, но позволяют быстрее вносить изменения. Тем более, что всю логику мы уже оставили за бортом (там где создавали Props)
Рендеринг может учитывать старые Props. Обычно это применяется для оптимизации в тех местах где UIKit / Android перерисовывается слишком часто. Обычно это нужно в сложных таблицах.
Анимации отдаются на откуп рендереру. Функция рендеринга смотрит на то как изменились Props и запускает / останавливает анимации.
Тестировать рендеринг можно через snapshot tests. UI рендерит пропсы, делает фотографию того что получилось и сравнивает с оригиналом. Стоят такие тесты дорого, часто дают ложно-положительные срабатывания. Подходят для очень мачурных продуктов, где UI не меняется просто так.
Альтернатива - Storybook (storybook.js.org).
Это отдельный режим работы приложения где можно посмотреть на то как разные экраны рисуют разные Props.
Бонусом идет быстрая отладка анимаций, и очень короткий цикл проверки правок UI кода.
@mobileunderhood А не должна ли быть тут ещё структура Button с полями, например, title: String и pressed: Command? И у Props let login: Button?
Отличный вопрос! В Props нужно оставлять только те поля, которые меняются. Если делать наоборот мы приходим от data-driven к декларативному подходу. Это оставляет сложность системы в функции рендеринга и помогает в тотальности. twitter.com/grishamsc/stat…
Не все значения title могут быть корректными, и мы можем допустить существование багов из-за выбранной структуры Props
@mobileunderhood звучит так как будто декларативный подход исключает датадривен, на мой взгляд он отлично дополняет его, можешь пояснить в чем усложнение на твой взгляд?
Декларативный подход фокусируется на одной функции рендеринга на всю систему. Она может отрисовать любые данные и построить любой UI. Поэтому в данных должна быть вся информация. И динамическая и статическая. Data-driven фокусируется на динамической части UI. twitter.com/maxim_bazarov/…
#день2 Data-driven UI на iOS.
Props умеет рендерить наследник UIViewController. Сами Props объявлены вложенным типом, что символизирует их связывание.
У контроллера есть функция render которая сохраняет пропсы и переносит их на UI компоненты.
Полезно в render делать setNeedsDisplay, а сам рендеринг в viewWillLayoutSubviews. Это не работает с таблицей - у неё свой цикл и reloadData достаточно.
@mobileunderhood Вы используете Storybook для кода на свифте? Или это пример релевантный к подходу?
У нас своя версия Storybook для мобильных платформ. Это просто таблица экранов которая ведёт на таблицу состояний отдельного экрана. Является частью приложения и запускается первой.
Это позволяет получить отзыв про UI работу до интеграции в систему. Ну и багов много меньше. twitter.com/agapov_one/sta…
Каждый контроллер должен кто то подключить к системе. Такая сущность может называться Connector или Presenter. Его жизненный цикл совпадает с жизненным циклом контроллера и на каждый контроллер он свой. При переходе между экранами коннектор создаёт новые коннекторы.
@mobileunderhood С таблицей можно еще делать дифф UI и UI’, и обновлять ее с помощью insert/remove rows. Так можно получить бесплатные анимации + перерисовывать не весь UI, а только тот которые изменился
@mobileunderhood Как мутируете состояние? Параллельно отправились несколько асинхронных Action, как обработается изменение Props? Есть какой-то я synchronized reduce метод?
Куда пихаете алерты/ошибки? Их тоже драйвят Props?
Кстати почему вообще Props, это ведь State flaviocopes.com/react-state-vs…
Про мутацию и redux будет сегодня. Пропсы нельзя поменять в обход команд. По сути единственная ниточка в внешний мир для экрана это команды. twitter.com/M0rtyMerr/stat…
@mobileunderhood Как мутируете состояние? Параллельно отправились несколько асинхронных Action, как обработается изменение Props? Есть какой-то я synchronized reduce метод?
Куда пихаете алерты/ошибки? Их тоже драйвят Props?
Кстати почему вообще Props, это ведь State flaviocopes.com/react-state-vs…
Ошибки могут быть частью экрана - тогда они есть в его пропсах. Алерты обычно это отдельный side effect. О них завтра :) twitter.com/m0rtymerr/stat…
@mobileunderhood Как мутируете состояние? Параллельно отправились несколько асинхронных Action, как обработается изменение Props? Есть какой-то я synchronized reduce метод?
Куда пихаете алерты/ошибки? Их тоже драйвят Props?
Кстати почему вообще Props, это ведь State flaviocopes.com/react-state-vs…
State это состояние компонента которое он меняет. Например текущий фокус ввода текста, показана клавиатура или нет.
Props это внешние данные которые кто-то отдал компоненту. twitter.com/m0rtymerr/stat…
#день3 Redux для мобильных проектов.
К redux я пришел через 3 года работы с реактивным кодом. Почему я отказался от реактивщины 4 года назад? В первую очередь - из-за багов порядка реакции на сигналы.
В реактивных системах очень трудно удержать логику в ячейках графа сигналов. Рано или поздно она протекает в саму структуру графа. switchToLatest это явный признак того, что структура графа меняется со временем. Покрывать такой код тестами очень трудно и долго.
4 года назад я начал использовать Signal<Props> вместо ViewModel. Это был первый прототип data-driven UI из #день2. Несколько месяцев я пытался адаптировать архитектуру Elm на Swift (guide.elm-lang.org/architecture/). Но у меня никак не получалось её масштабировать.
Проблемы с Elm у меня с её иерархической структурой. Сообщения привязаны к компонентам которые на них реагируют, и практически любой рефакторинг заставляет менять много кода, а вслед за ним и тесты. Намного больше мне зашел redux (redux.js.org)
Redux делает акцент на плоской группировке маленьких, изолированных компонентов. Важным для понимания Redux является концепция нормализации данных (redux.js.org/recipes/struct…). При таком дизайне мы можем изолировать логику каждого отдельного компонента от других.
Redux основан на нескольких принципах.
-
Единое состояние. Всё состояние приложения хранится в 1 месте и все могут получать обновления этого состояния.
-
Неизменяемое состояние. Никто не может поменять состояние приложения в обход reducer
-
reducer это чистая функция.
Что такое reducer? Это чистая функция которая может сказать как нужно изменить состояние для произвольного события в системе. pic.twitter.com/DhBXZXYgcG
Что такое Action? это сумма всех типов событий в системе. Суммы типов могут быть открытыми (interface, protocol) и закрытыми (enum, sealed class). Мой опыт говорит что открытая сумма практичнее. Каждый конкретный Action это просто наследник этого маркера. pic.twitter.com/oEJEaAhSNe
Как выглядит отдельный компонент состояния? Нужно стремится делать его как можно меньше. Иногда это одно поле, которое реагирует на несколько Action. Боятся такого не надо, надо писать тесты. pic.twitter.com/PzFofT48t4
Тесты для reducer это самые простые тесты. Вы просто создаете состояние, и вызываете reducer. pic.twitter.com/cgHX3Rc9gu
Много, много маленьких компонентов собираются в 1 большой и плоский State c большим и примитивным reducer. На него тесты писать не стоит. В приложениях кол-во компонентов измеряется в сотнях обычно. pic.twitter.com/Rj8nHewMLK
Как только у вас есть State и набор Action которые его могут менять - время писать Store.
Этот компонент обеспечивает упорядоченное применение Action (отдельная очередь), неизменяемость состояния и рассылку уведомлений.
Его имплементация не особо важна и меняется под проект.
Многие любят использовать реактивные фреймворки для этого компонента, из-за дешевой подписки и легкого управления очередями. Мне нравится писать его руками)
Store, State и Action позволяют получить полностью рабочее приложение без UI и сервисов которое может поддерживать согласованность и реагировать на внешние команды. По сути это наш functional core из clean architecture.
Чуть позже разберем как подключить data-driven ui к этому ядру. А пока можно поотвечать на вопросы. Обычно их много на этом этапе
@mobileunderhood @egroden_ На примере ввода данных в форму хорошо бы рассмотреть, что должно происходить при вводе текста? Набранный текст это State? Initial значения могут быть разные у одной и той же вью в разных кейсах, кто за что отвечает, поле email например, где выполнять валидацию?
Вот пример формы из демо проекта. Ввод текста отправляется в команды, там он триггерит получение новых Props и они приходят в UI и триггерят его перерисовку. Начальные значения задаются в самом контроллере. Валидация выполняется снаружи тем, кто создает Props. twitter.com/BruinInaSweate… pic.twitter.com/bJ0Zm9DEiB
@mobileunderhood Как проектировать такой плоский и широкий стейт? В примере есть login, loginError, logout стейты, и все совместны — я бы до такого не дошёл, потому что выглядит неестественным для меня. Я бы сделал это опциональным енамом с кейсами loggedIn(user)|failed(error), и потом бы мучался
На такой стейт проще писать тесты, а его согласованность обеспечивается reducer функциями. Но это требует практики и рефлексии. Запаситесь терпением и в бой :) twitter.com/asanoryodan/st…
@mobileunderhood А в чем смысл запихивать все в viewWillLayoutSubviews?
Props могут меняться чаще чем UI готов отрисовываться. setNeedsLayout позволяет избежать лишних перерисовок. twitter.com/akaduality/sta…
@mobileunderhood А норм, что начальное состояние задается в контроллере, а не снаружи, где в итоге обработка команд происходит? Я когда такой подход (после твоего доклада на мобиусе 👍🏼) затащила к себе в проект, очень хотелось и начальный стейт сеттить снаружи, чтобы ответвенности не смешивались.
Это скорее сбоишь избежать опциональных пропсов при условии что контроллер (фрагмент) создаёт система а не код приложения.
Если вы полностью контролируете создание контроллера - можно передавать Props в конструкторе и не иметь начального состояния в самом контроллере. twitter.com/kathrinpetrova…
#день3 Использование Redux и data-driven UI.
Первым делом создаётся Store с начальным состоянием. Обычно это происходит в AppDelegate или Application классах.
Независимо от этого создаются все начальные компоненты UI. Storyboard или NavigationGraph соответственно.
Потом идёт фаза подключения всех обработчиков эффектов. Это несколько десятков сервисов - сеть, диск, навигация, алёрты. Подробнее об этом я хочу поговорить в #день4.
Ну и заканчивается настройка подключением Store к UI компонентам. Store умеет работать только со State и Action, и ничего не знает про Props. UI - с точностью наоборот.
Для того что бы подружить эти 2 сущности используется Connector.
Connector это тип который умеет собирать Props из State. По сути это его единственное задание. Коннектор не обязан быть чистой функцией, но очень не против такого развития событий.
Его жизненный цикл связан с UI компонентом. Он не держит ссылок на UI а просто выдаёт Props. Передача этих Props в UI реализована в виде отдельной функции: “store.connect(UI, using: connector)”
Такой дизайн позволяет написать интеграционный тест всего функционального ядра стстемы без императивной оболочки.
При переходе между экранами один коннектор создаёт другой и подключает к нему следующий экран.
При формировании Props необходимо указать команды. Connector так же занимается их формированием. Команда обычно забрасывает Action в систему или подключает новый коннектор.
Тестировать коннектор через Unit test очень дорого - он зависит от всего State и его настройка занимает слишком много сил. Поэтому мы тестируем его через интеграционные тесты.
Создаётся State, к которому подключаются все коннекторы которые мы хотим проверить. Их Props складываются в тестовые рендереры которые просто хранят пропсы. Тест проверяет поля в Props и дергает команды.
@mobileunderhood Вопрос по паджинации. Вы каждый раз перегенериваете все пропсы для таблицы, после загрузки следующей страницы?
Даже более того. После любого изменения состояния пропсы всех компонентов генерируются заново. Иногда это бывает чаще чем 60 раз в секунду.
Такая особенность позволяет системе самокорректироваться после этого незначительных сбоев. twitter.com/degtyarserg/st…
@mobileunderhood Что если стейт довольно большой? Например речь о текстовом редакторе или банально огромной табличке в миллион записей?
Сначала стоит задуматься о каком-то хранилище. Но если стейт влазит в оперативную память - это может не потребоваться.
Если производительность упирается в трансформацию из State в Props то стоит проверить нормализацию State. Не должно быть поиска по массивам. twitter.com/Kri_zai/status…
В самых экстремальных случаях можно воспользоваться промежуточными состояниями с мемоизацией. Для вдохновения можно смотреть в reselect - github.com/reduxjs/resele…
@mobileunderhood А можно тут поподробнее?
Подробнее про интеграционные тесты.
State + Reducer + Connector дают Props которые и улетят компонентам. Если мы верим нашим компонентам (или пишем для них тесты) то правильных Props достаточно что бы сказать что вся система работает правильно. twitter.com/maxim_bazarov/…
Примерный flow который проверят наши тесты. У нас есть операция по скачиванию счетчиков с сервера, она должна запустится, и отработать. А наша таблица должна показать все что пришлет операция.
Сначала мы настраиваем операцию. Проверяем что у неё в Props есть один активный запрос, и выполняем его. После выполнения проверяем что активных запросов у операции больше нет. pic.twitter.com/tH19HxmzdH
Теперь подключаем к системе таблицу. И проверяем что она показывает один счетчик (столько мы ответили в операции в предыдущей секции) pic.twitter.com/jtRajWs6LV
Теперь очередь операции на создание нового счетчика. Это отдельная операция и отдельный Connector. pic.twitter.com/jEdjXRBCqD
Теперь мы можем у Props таблицы вызвать команду на создание счетчика и проверить что эта запрос появился в операции. pic.twitter.com/t5FkWFWOM8
В подобном стиле мы можем описать всю систему. Эти тесты довольно дороги в поддержке и мы пишем их только на функционал который стабилен. Относится к ним стоит как к UI тестам, но без трудностей UI тестов.
@mobileunderhood Посложнее бы пример. С таблицей
Таблицу можно смоделировать примерно такими Props. Она умеет показывать большой экран загрузки, пустой экран, ошибку или контент. twitter.com/larryonoff/sta… pic.twitter.com/3X2xbWDsQ8
Сам Content можно представить как список элементов и указатель на следующую страницу. Элемент можно выбрать, но id мы негде не передаем - он "вшит" в команду pic.twitter.com/wRUzraiTEZ
Это можно описать примерно таким состоянием. Интересно отметить нормализацию элементов - есть отдельная таблица, а есть список ID. pic.twitter.com/bFV0uuKGa9
Коннектор выглядит как то так. bind это операция которая оборачивает Action в Command. По сути просто делает { store.dispatch(action) } pic.twitter.com/ewolEFYC0e
Элементы можно отобразить в props как то так. fatalError можно и избежать, но не хочу вводить еще 1 абстракцию. Если интересно - спрашивайте в комментариях. pic.twitter.com/pwAnvFb4gY
NextPage тоже собирается просто. Cursor это стандартная абстракция для порционного поглощения данных. pic.twitter.com/cLfiH7imOZ
@mobileunderhood Так. Ну хорошо. А про какие-то минусы этого подхода будет?
Давайте поговорим про минусы!
Любая методология является результатом решения множества компромиссов в ту или иную сторону. Использовать методологию и не понимать какие компромиссы она в себе содержит - рискованно. twitter.com/imgnta/status/…
Не мейнстрим. 4 года назад это было очень дико, сейчас вроде бы как хайп раскачивается. Но до мейнстрима еще далеко. У реактивных подходов на это ушло лет 10. Риск в том что найти разработчика с рынка будет сложно. Мы решаем это менторингом в основном.
Другая ментальность. Очень многие особенности идут в разрез с привычными практиками. Один гигансткий стейт доступный всем и отовсюду выглядит как издевательство над всеми известными шаблонами проектирования.
Компромиссы ради тестируемости. Очень многие решения сделаны именно так ради обеспечения тестов. Если команда не хочет / любит / умеет писать тесты - преимуществ будет меньше.
Недружелюбная платформа. UIKit и Android не созданы с учетом data-driven практик. Это усложняет реализацию в неожиданных местах. Например UIRefreshControl очень сопротивляется контролю из render.
Мало ресурсов. Искать помощи может быть негде. Как такового сообщества и движения нету. Остается только быть смелыми и брать на себя все риски по внедрению.
@mobileunderhood Хочу уточнить как это работает на гипотетическом экране логина: При каждом изменении UITextField будет вызываться updateWith соответствующего поля? Что обычно происходит внутри этого updateWith? Генерация следующего Props?
Каждый ввод символа в UITextField вызывает команду, она отдает в Store какой-то Action, он ловится кем то из Reducer, собирается новый State который отдается всем Connector которые формируют новые Props и отдают их всем компонентам которые обновляют UI. Вроде ничего не забыл ) twitter.com/rbsgn/status/1…
@mobileunderhood Например, экран информации о каком-то объекте. Например, Список контактов → Информация о контакте. Здесь экран информации о контакте похож на статичный экран.
Каждый экран контакта отличается от другого. Props это единственный внешний источник информации для модуля. Это позволяет по Props судить о том что можно ждать от экрана. twitter.com/rbsgn/status/1…
@mobileunderhood У меня Google упорно говорит, что Cursor — это синоним для Iterator pattern, а итератор, вроде как, не имеет отношения к порционному поглощению данных. У тебя есть ссылка на описание работы Cursor?
Этот термин я подцепил из GraphQL мира. Вот некоторые ссылки.
#день4 Про управление side effect в redux парадигме.
Архитектура которая не умеет в side effect это немножко бесполезная архитектура.
В мире Redux.js эсть множество решений этой проблемы. От "умных" Action (github.com/reduxjs/redux-…) до саг (github.com/redux-saga/red…)
Я перепробовал множество вариантов за 4 года практики, но последний год я полностью доволен data-driven side effect.
Идея состоит в том что UI это тоже side-effect. И нет смысла придумывать новый способ выполнения side effect. Так что абсолютно все сервисы я подключаю так же как и UI.
У каждого сервиса есть Props которые описывает то состояние мира который сервис должен обеспечить. Сервис постоянно получает новые Props, смотрит на внешний мир и пытается его поменять так, что бы Props не отличались от внешнего мира.
Мы называем такие сервисы - операторы. Это название привязалось из Kubernetes абстракции которая делает то же самое. (kubernetes.io/docs/concepts/…). Вообще весь Kubernetes это data-driven система, которую интересно рассматривать как пример для вдохновения.
Конкретные примеры. Переход между экранами логина и самим приложением происходит после изменения State, и удобно описывается вот такими пропсами. pic.twitter.com/ExHDrg9o9p
Оператор получает 2 подключенных контроллера и переключает их в зависимости от Props.
У него так же есть свой Connector который эти Props формирует. Тестировать оператор можно только в составе всего приложения (руками) так как операторы зависят от конкретных фреймворков. pic.twitter.com/VICJNYX1on
Другой пример это сеть. В Props оператора сети указаны все запросы которые сейчас должны выполняться. Мы делаем отдельный оператор на каждый запрос, но можно и один общий написать. pic.twitter.com/vUuDbOCzZs
Оператор держит в себе список запросов которые запущены, что бы избежать повторов. Когда в Props появляется новый запрос - оператор запускает его. Когда пропадает - отменяет его если он еще выполняется. pic.twitter.com/M53QzZX03R
Таких операторов может быть довольно много: Keychain, UserDefaults, GPS, Alert. Все сервисы которые вам могут понадобиться.
@mobileunderhood А анимации сопротивляются?
На самом деле не так сильно как может показаться.
У UI есть понимание что сейчас показывается, есть то что надо показать - достаточно просто запустить анимацию перехода в это состояние. В последние годы все реже приходится опускаться до уровня CoreAnimation. twitter.com/imgnta/status/…
@mobileunderhood @udfmobileru Интересно еще про загрузку картинок, например как лучше передать в пропсы URL или передавать уже Data например? Как отменять загрузку? Уверен ты пробовал оба способа, как в итоге кажется лучше?
Загрузку картинок я оставляю на откуп системе, так что по системе гуляет URL, и UI пускай сам ворочается.
Написано много кода и библиотек которые умеют это делать. Мне кажется показ картинки из URL должен быть частью системных фреймворков. twitter.com/maxim_bazarov/…
@mobileunderhood Да, я вот тоже на этом остановился пока, единственный минус приходится изгаляться со снепшот тестами вроде такого pic.twitter.com/kT76ipolxP
Можно вместо URL картинки ввести чуть более сложный примитив. И использовать в тестах уже его. Ну и научить UIImageView понимать этот примитив и грузить из разных источников. Тогда в snapshot тестах можно отдавать локальные картинки которые будут грузиться синхронно. twitter.com/maxim_bazarov/… pic.twitter.com/rXPA6ph7PI
@VasiliyZukanov @imgnta @mobileunderhood А вот глобальный стор на всё приложение... Ну это такое себе, можно было и не тянуть это из веба.
Распространенное мнение. Меня подкупает возможность отображать состояние на разных экранах не думая про их синхронизацию. Ну и нормализация хорошо работает только с single source of truth. twitter.com/tim_plotnikov/…
@mobileunderhood Стейт как-то чистится? К примеру когда будут очищены загруженные контакты отображаемые в контроллере? После закрытия контроллера или по memory warning? Как определяется какая часть стейта сейчас не нужна для отображения и ее можно выгрузить?
Съест может очистится при определенных событиях. Но я не встречал потребности в такой чистке twitter.com/soniccat/statu…
@mobileunderhood Что если какое-то поле должно валидировать ввод посимвольно и запрещать некоторый ввод. Пусть например у нас есть какая-то маска для email, где будет такого рода логика?
Эта логика будет в reducer который обрабатывает команды с новым текстом в поле. twitter.com/kri_zai/status…
@mobileunderhood Kak v AppState soxranit dannie (subState) takix odnotipnix viewController-ov, koqda mojno beskonecno perexodit ot odnoqo viewController-a k druqomu? pic.twitter.com/kO1S7wEnad
Состояние хранится в нормализованном виде, а id видимого компонента передаётся в Connector (не state) twitter.com/elshad_ff/stat…
#день5 Обещал говорить про здоровье.
Вот уже 2 года по пятницам я беседую с психологом по 1 часу. Попытаюсь рассказать вам, почему я считаю что эта активность будет полезна всем.
DISCLAIMER: Любые советы и мнения сугубо индивидуальны. Не доверяйте никому.
Для меня поводом обратится к психологу стало ощущения загнанности в треугольник работа, семья, ивенты. Я ощущал что не принадлежу себе и не контролирую свою жизнь. Мне повезло, и хорошие отношения у меня сложились с первым психологом.
С тех пор я успешно перестал об этом беспокоится (почти всегда) и беспокоится о том что я об этом беспокоюсь (всегда). Научился брать паузы перед принятием решений и прислушиваться к своему я. Стал чуть более жить моментом и чуть более расслабился по всем фронтам.
Почему я считаю, что терапия нужна всем? Современный образ жизни не оставляет много времени на скуку и рефлексию. А без этих активностей мы очень быстро скатываемся в автоматизм существования.
Самый опасный мой враг - конфликт между сознанием (мне надо) и подсознанием (я хочу). Когда он обостряется - я устаю и становлюсь пассивным овощем.
Когда такой конфликт затягивается - можно стелить коврик выгоранию)
Кстати про выгорание. Для меня его вестником стали рабочие ночи. Когда все прекрасно, идеи так и прут и кажется что наконец то вот оно - вдохновение.
На самом же дела подсознание устало кричать что вам надо отдыхать и отрубилось. Сознанию никто не мешает и оно успешно выжигает.
Это понимание подружило меня с прокрастинацией. Теперь я воспринимаю ее как сигнал о том что я делаю что то не то. Пересматриваю список своих обязательств и выбрасываю (делегирую) те которыми не хочется заниматься.
Так же терапия помогает развивать личность. Я бы сравнил это с фитнесом для психики - регулярные дозированные нагрузки под присмотром тренера. Это сделало меня более выносливым эмоционально и способным оказать поддержку близким людям.
Терапия не ставит своей целью решение проблем, скорее дает пространство для рефлексии над их причинами и последствиями. Так что не стоит ждать от неё исцеления - будете разочарованы.
Не всегда терапевт подойдет вам с первого раза. Это нормально. Отношения с терапевтом это обычно на года, поэтому ищите того, с кем вам будет интересно. Это добавит мотивации и поможет расслабится.
До терапевта я пробовал заниматься медитацией. Мне это давалось нелегко и я не видел прогресса, поэтому я оставил эту технику. Но это одна из тех вещей которые стоит попробовать - может быть вам это окажется чрезвычайно полезно.
Терапия помогает мне справляться с синдромом самозванца. Это особенно важно в насыщенном информационном поле, в котором мы живем.
Однако, терапия делает вас не таким удобным для других людей, кому то это может не понравится и вам предстоит социальная перестройка. Мне хочется верить что это хорошо при игре в долгую.
Не стоит забывать и о физическом развитии. Если не занимались спортом с детства - берегите суставы (колени). Их травмы самые противные.
Если нет времени на зал - практикуйте планку или йогу дома. Статическая нагрузка хорошо разминает мышцы.
Берегите себя и любите себя. Тогда любить всех остальных будет намного проще)
Ну и конечно же сон. Депривация сна (меньше 6 часов) приводит к объективно ощутимому снижению производительности. Но субъективно его нет.
Не доверяйте своим ощущениям и спите больше.
Я стараюсь как можно легче пропускать информацию. Все что на самом деле важно само найдет к вам путь. Так что смело урезайте себе ленту, и ограничивайте контент и медиа которое вы потребляете.
Можете провести эксперимент и каждому твиту / посту в ленте присвоить оценку. 0 - бесполезно, 10 - изменило вашу жизнь. Начинайте прямо с этого твита)
Постройте распределение оценок за день и подумайте - стоят ли эти оценки вашего времени.
@mobileunderhood Делаю то же самое, но мне быстро надоедает. Насущное разобрали. Потом скучно. Вот вопрос: за два года на каких временных отрезках случались самые большие изменения?
Первые полгода самые заметные наверно. Потом скорее профилактика и анализ шаблонов поведения.
От скуки меня спасает широкий набор тем которые я могу обсудить. twitter.com/imgnta/status/…
- https://github.com/AlexeyDemedetskiy
- https://github.com/AlexeyDemedetskiy/GwentBrowser
- https://github.com/reduxjs/reselect
- https://github.com/reduxjs/redux-thunk
- https://github.com/redux-saga/redux-saga
- https://medium.com/@DAloG
- https://speakerdeck.com/dalog
- https://music.apple.com/ua/album/simulation-theory-super-deluxe/1440779866?l=uk
- https://ru.wikipedia.org/wiki/Биекция
- https://storybook.js.org/
- https://redux.js.org/
- https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape
- https://guide.elm-lang.org/architecture/
- https://www.sitepoint.com/paginating-real-time-data-cursor-based-pagination/
- https://graphql.org/learn/pagination/
- https://api.slack.com/docs/pagination
- https://kubernetes.io/docs/concepts/extend-kubernetes/operator/