Реактивний програмування: поняття, навчання, особливості та поради фахівців. Розбираємося з фреймворком ReactiveX і пишемо в реактивному стилі для Android Реактивний програмування

Я хочу розповісти вам про сучасну дисципліни програмування, що відповідає зростаючим вимогам масштабованості, відмовостійкості і швидкого відгуку, і незамінною як в багатоядерних середовищах так і в хмарні обчислення, а також представити відкритий онлайн-курс по ній, який почнеться за все через кілька днів.

Якщо ви нічого не чули про реактивне програмування, все в порядку. Це стрімко розвивається дисципліна, в якій скомбіновані паралелізм (concurrency) c подієвої орієнтованістю і асинхронних. Реактивність властива будь-якому веб-сервісу і розподіленої системі, і служить ядром багатьох вископроізводітельних систем з великим ступенем паралелізму. Якщо коротко, то автори курсу пропонують розглядати реактивне програмування як природне розширення функціонального програмування (з функціями вищих порядків) на паралельні системи з розподіленим станом, координовані і оркестріруемие асинхронними потоками даних, якими обмінюються активні суб'єкти, або актори.

Більш зрозумілими словами це описується в Реактивному маніфесті, нижче я перекажу основні положення з нього, а повний переклад опублікований на Хабре. Як розповідає вікіпедія, термін реактивне програмування існує досить давно і має практичні застосування різного ступеня екзотичності, але новий поштовх до розвитку і поширення він отримав зовсім недавно, завдяки зусиллям авторів реактивного маніфесту - ініціативній групі з Typesafe Inc. Typesafe відома в середовищі функціонального програмування як компанія, заснована авторами прекрасного мови Scala і революційної паралельної платформи Akka. Зараз вони позиціонують свою компанію як творця першої в світі реактивної платформи, призначеної для розробки нового покоління. Їх платформа дозволяє швидко розробляти складні інтерфейси і надає новий рівень абстракції над паралельними обчисленнями і багатопоточність, зменшуючи властиві їм ризики завдяки гарантовано передбачуваному масштабування. Вона реалізує на практиці ідеї реактивного маніфесту і дозволяє розробнику осмислювати і створювати додатки, що відповідають сучасним запитам.

Ви можете познайомитися з цією платформою і реактивним програмуванням, взявши участь в масовому відкритому онлайн-курсі «Принципи реактивного програмування». Цей курс є продовженням курсу Мартін Одерски «Принципи функціонального програмування на Скала», який набрав більше 100 000 учасників і продемонстрував одну з найвищих в світі ступінь успішного проходження масового відкритого онлайн-курсу його учасниками. Разом з творцем мови Скала новий курс читають Ерік Мейер, який розробляв середу Rx для реактивного програмування под.NET, і Роланд Кун, провідний команду розробки Akka в Typesafe в даний час. Курс розкриває ключові елементи реактивного програмування і показує, як вони застосовуються для конструювання подієво-орієнтованих систем, що володіють масштабованість і відмовостійкість. Навчальний матеріал ілюструється короткими програмами і супроводжується набором завдань, кожне з яких - це програмний проект. У разі успішного виконання всіх завдань учасники отримують сертифікати (зрозуміло, і участь і сертифікати безкоштовні). Курс триває 7 тижнів і починається в цей понеділок, 4 листопада. Докладний план, а також вступне відео доступні на сторінці курсу: https://www.coursera.org/course/reactive.

Тим хто зацікавився, або сумнівається, пропоную стислий виклад базових концепцій реактивного маніфесту. Його автори відзначають значні зміни у вимогах до додатків за останні роки. Сьогодні додатки розгортаються в будь-якому оточенні від мобільних пристроїв до хмарних кластерів з тисячами багатоядерних процесорів. Ці оточення висувають нові вимоги до програмного забезпечення та технологій. В архітектурі попереднього покоління акцент робився на керовані сервера і контейнери, а масштабування досягалося за рахунок додаткового дорогого обладнання, пропрієтарних рішень і паралельних обчислень через багатопоточність. Зараз розвивається нова архітектура, в якій можна виділити чотири найважливіші риси, все більше переважають як в призначених для користувача, так і в корпоративних промислових середовищах. Системи з такою архітектурою: подієво-орієнтовані (Event-driven), масштабуються (Scalable), відмовостійкості (Resilient) і мають швидкий відгуком, тобто чуйні (Responsive). Це забезпечує комфортну взаємодію з користувачем, що дає відчуття реального часу, і підтримуване самовідновлюватися масштабується прикладним стеком, готовим до розгортання в багатоядерних і хмарних середовищах. Кожна з чотирьох характеристик реактивної архітектури застосовується до всього технологічного стека, що відрізняє їх від ланок в багаторівневих архітектур. Розглянемо їх трохи докладніше.


Подієво-орієнтовані додатки припускають асинхронні комунікації компонент і реалізують їх слабку зв'язаність (loosely coupled design): відправник і одержувач повідомлення не потребують відомостях ні один про одного, ні про спосіб передачі повідомлення, що дозволяє їм сконцентруватися на змісті комунікацій. Крім того, що слабкозв'язаного компоненти значно покращують сопровождаемость, розширюваність і еволюціонування системи, асинхронність та неблокірующій характер їх взаємодії дозволяють також звільнити значну частину ресурсів, знизити час оклику і забезпечити б пробільшу пропускну здатність у порівнянні з традиційними програмами. Саме завдяки подієво-орієнтованої природі можливі інші риси реактивної архітектури.

масштабованість в контексті реактивного програмування - це реакція системи на зміну навантаження, тобто еластичність, що досягається можливістю додавання або звільнення обчислювальних вузлів у міру необхідності. Завдяки низькій пов'язаності, асинхронного обміну повідомленнями та незалежності від розміщення компонентів (location transparency), спосіб розгортання і топологія додатки стають рішенням часу розгортання і предметом конфігурації і адаптивних алгоритмів, що реагують на навантаження. Таким чином, обчислювальна мережа стає частиною програми, спочатку має явну розподілену природу.

відмовостійкість реактивної архітектури теж стає частиною дизайну, і це значно відрізняє її від традиційних підходів до забезпечення безперервної доступності системи шляхом резервування серверів і перехоплення управління при відмові. Стійкість такої системи досягається її здатністю коректно реагувати на збої окремих компонент, ізолювати ці збої, зберігаючи їх контекст у вигляді викликали їх повідомлень, і передавати ці повідомлення іншого компонента, здатному прийняти рішення про те, як слід обробляти помилку. Такий підхід дозволяє зберегти чистою бізнес-логіку додатка, відокремивши від неї логіку обробки збоїв, яка формулюється в явному декларативному вигляді для реєстрації, ізолювання, і обробки збоїв засобами самої системи. Для побудови таких самовідновлюються систем компоненти впорядковуються ієрархічно, і проблема ескаліруется до того рівня, який здатний її вирішити.

І наприкінці чуйність - це здатність системи реагувати на призначене для користувача вплив незалежно від навантаження і збоїв, такі додатки втягують користувача у взаємодію, створюють відчуття тісного зв'язку з системою і достатньому оснащенні для виконання поточних завдань. Чуйність актуальна не тільки в системах реального часу, а й необхідна для широкого кола додатків. Більш того, система, нездатна до швидкого відгуку навіть у момент збою, не може вважатися відмовостійкої. Чуйність досягається застосуванням спостережуваних моделей (observable models), потоків подій (event streams) і клієнтів зі станом (stateful clients). Спостережувані моделі генерують події при зміні свого стану і забезпечують взаємодію реального часу між користувачами і системами, а потоки подій надають абстракцію, на якій побудовано це взаємодія шляхом неблокірующіх асинхронних трансформацій і комунікацій.

Таким чином, реактивні додатки є збалансований підхід до вирішення широкого спектра завдань сучасної розробки ПЗ. Побудовані на подієво-орієнтованому підставі, вони надають кошти, необхідні для гарантій масштабованості і відмовостійкості і підтримують повнофункціональний чуйне призначене для користувача взаємодія. Автори очікують, що все більше число систем дотримуватиметься принципів реактивного маніфесту.

На додачу привожу план курсу без перекладу. Просто на випадок, якщо ви дочитали до цього місця, і вам все ще цікаво.

Week 1: Review of Principles of Functional Programming: substitution model, for-expressions and how they relate to monads. Introduces a new implementation of for-expressions: random value generators. Shows how this can be used in randomized testing and gives an overview of ScalaCheck, a tool which implements this idea.

Week 2: Functional programming and mutable state. What makes an object mutable? How this impacts the substitution model. Extended example: Digital circuit simulation

Week 3: Futures. Introduces futures as another monad, with for-expressions as concrete syntax. Shows how futures can be composed to avoid thread blocking. Discusses cross-thread error handling.

Week 4: Reactive stream processing. Generalizing futures to reactive computations over streams. Stream operators.

Week 5: Actors. Introduces the Actor Model, actors as encapsulated units of consistency, asynchronous message passing, discusses different message delivery semantics (at most once, at least once, exactly once) and eventual consistency.

Week 6: Supervision. Introduces reification of failure, hierarchical failure handling, the Error Kernel pattern, lifecycle monitoring, discusses transient and persistent state.

Week 7: Conversation Patterns. Discusses the management of conversational state between actors and patterns for flow control, routing of messages to pools of actors for resilience or load balancing, acknowledgement of reception to achieve reliable delivery.

Принципи реактивного програмування не нові, і їх можна простежити з 70-х і 80-х років в основоположних роботах Джима Грея і Пета Хелланд по тандемной системі.

Ці люди набагато випередили свій час. Тільки в останні 5-10 років технологічна індустрія була змушена переглянути існуючі «кращі практики» для розвитку корпоративної системи. Вона навчилася застосовувати знання про реактивних принципах сьогоднішнього світу багатоядерних і хмарних обчислень.

Основою для реактивної системи є передача повідомлень, яка створює тимчасову кордон між компонентами, дозволяє їх розв'язувати в часі, використовуючи паралельність і простір, що розподіляє навантаження і забезпечує мобільність. Ця розв'язка є вимогою повної ізоляції між компонентами і становить основу як для стійкості, так і для еластичності систем.

Основи реактивного програмування

Це програмування направлено на потоки інформації і поширення змін даних. При використанні мов програмування легко виділити статичні і динамічні потоки, при цьому базова модель автоматично поширить зміни через всі потоки даних. Говорячи простими словами, в програмування Rx, що випускаються одним компонентом, і базова структура, що надається бібліотеками Rx, буде поширювати ці зміни на інший компонент, зареєстрований для отримання цих змін. Реактивний програмування Rx складається з трьох ключових моментів.

Основні функції компонентів:

  1. Спостережувані - не що інше, як потоки даних. Спостережуваний упаковує дані, які можуть передаватися з одного потоку в інший. Вони в основному випускають дані періодично або тільки один раз в своєму життєвому циклі на основі конфігурацій. Існують різні оператори, які можуть допомогти спостерігачеві відправити деякі конкретні дані на основі певних подій.
  2. Спостерігачі споживають потік даних, що випромінюється спостережуваним. Спостерігачі підписуються за допомогою методу реактивного програмування subscribeOn () для отримання даних, які передають спостережуваним. Всякий раз, коли спостерігається передасть дані, всі зареєстровані спостерігачі отримують дані в зворотному виклику onNext (). Тут вони можуть виконувати різні операції, такі як розбір відповіді JSON або оновлення призначеного для користувача інтерфейсу. Якщо є помилка, викликана спостережуваним, спостерігач отримає її в onError ().
  3. Планувальники (розклад) - це компонент в Rx, який повідомляє спостережуваним і спостерігачам, за яким потоку вони повинні працювати. Можна використовувати метод observOn (), щоб повідомляти спостерігачам, на якому потоці вони повинні спостерігати. Крім того, можна використовувати schedOn (), щоб повідомити спостерігається, в якому потоці вони повинні запускатися.

У реактивному програмуванні з використанням RxJava передбачені основні потоки за замовчуванням, такі як Schedulers.newThread () створять новий фон. Schedulers.io () виконає код в потоці вводу-виводу.

Основними перевагами Rx є збільшення використання обчислювальних ресурсів на багатоядерному і многопроцессорном обладнанні, підвищення продуктивності за рахунок скорочення точок і підвищення продуктивності за рахунок скорочення точок сериализации, відповідно до Закону Амдаля і Універсального закону про масштабованості Гюнтера.

Друга перевага - висока продуктивність для розробників, оскільки традиційні парадигми програмування щосили намагалися забезпечити простий і підтримуваний підхід до роботи з асинхронними і неблокірующіх обчисленнями і IO. З цими завданнями справляється функціональне реактивне програмування, оскільки воно зазвичай усуває необхідність в явній координації між активними компонентами.

Там, де виникає Rx, створюється процес створення компонентів і склад робочих процесів. Щоб повністю використовувати асинхронне виконання, включення противодавления має вирішальне значення, щоб уникнути надмірного використання або, скоріше, необмеженого споживання ресурсів. Щоб забезпечити стійкий стан з точки зору потоку даних, зворотний тиск на основі навантаження відсилає попит, що протікає вгору по потоку, і приймає повідомлення.

Таким чином, основними перевагами системи є:

  1. Підвищена продуктивність - завдяки можливості швидко і стабільно обробляти величезні обсяги даних.
  2. Покращений UX - через те, що додаток більше реагує на користувача.
  3. Спрощені модифікації і оновлення - завдяки більш читабельного і більш легкому прогнозування коду.

Але незважаючи на те, що Reactive Programming - дуже корисна штука при створенні сучасного програмного забезпечення, щоб міркувати про систему на більш високому рівні, потрібно використовувати інший інструмент - Reactive Architecture для процесу проектування реактивних систем. Крім того, важливо пам'ятати, що існує багато парадигм програмування, і Rx - це лише один з них, як і будь-який інструмент, він не призначений для всіх випадків використання.

Стійкість реактивних систем

Стійкість - це чуйність при невдачі і є невід'ємним функціональним властивістю системи. Для неї потрібні розробки, а не просто додавання в систему в ретроактивності вигляді. Стійкість реактивного програмування javascript виходить за межі відмовостійкості і це відбувається не через деградацію, а в разі невдачі вона може повністю сама виправитися.

Для цього потрібно ізоляція компонентів і стримування відмов, щоб уникнути збоїв, що поширюються на сусідні компоненти, що може привести до катастрофічних сценаріїв з каскадними збоями. Таким чином, ключем до створення систем Resilient - самовідновлення - є те, що вони можуть бути охарактеризованими як повідомлення, відправлені на інші компоненти, які діють як супервізори і управляються з безпечного контексту поза відмовив компонента.

Тут, будучи керованими повідомленнями, ці кошти відходять від сильно пов'язаних, тендітних, глибоко вкладених синхронних ланцюжків викликів, які в більшості випадків ігноруються. Ідея полягає в тому, щоб відокремити управління відмовами від ланцюжка викликів, наприклад, звільняючи клієнта від відповідальності за обробку збоїв сервера.

Оскільки більшість систем за своєю природою складні, одним з найбільш важливих аспектів є забезпечення того, щоб системна архітектура забезпечувала мінімальне зниження продуктивності як при розробці, так і в обслуговуванні компонентів, і в той же час зменшувала випадкову складність до мінімуму. Це важливо, оскільки протягом життєвого циклу системи, якщо вона не розроблена належним чином, буде все важче і важче підтримувати працездатність, і буде потрібно все більше часу і зусиль для розуміння, щоб локалізувати і усунути проблеми.

Реактивні системи являють собою найбільш продуктивну системну архітектуру, в контексті багатоядерних, хмарних і мобільних архітектур:

  1. Ізоляція відмов пропонує перебирання між компонентами, запобігаючи відмови від каскадирования і обмежуючи обсяг і ступінь відмов.
  2. Ієрархії супервайзерів пропонують кілька рівнів захисту в поєднанні з можливостями самовідновлення, що усуває безліч тимчасових відмов від будь-яких експлуатаційних витрат для розслідування.
  3. Пропуск передачі повідомлень і прозорість розташування дозволяють відключати і замінювати компоненти, не зачіпаючи роботу кінцевого користувача. Це знижує вартість збоїв, їх відносну актуальність, а також ресурси, необхідні для діагностики та виправлення.
  4. Реплікація знижує ризик втрати даних і зменшує вплив збою на доступність пошуку і зберігання інформації.
  5. Еластичність дозволяє зберігати ресурси в міру того, як використання коливається, що мінімізує експлуатаційні витрати при низькому завантаженні і ризик збоїв або термінових інвестицій в масштабованість в міру збільшення навантаження.

Веб-додатки можуть в значній мірі виграти від стилю розробки з Rx, що дозволяє скласти робочі процеси запиту-відповіді, що включають розгалуження на виклики служб, асинхронне витягання ресурсів і складання відповідей, і подальшу сортування для клієнта. Зовсім недавно push-а-серверні події і веб-сокети стали все частіше використовуватися на практиці, і для виконання цього в масштабі потрібно ефективний спосіб зберігання безлічі відкритих підключень і де IO не блокує.

Для цього є інструменти, наприклад, Streams and Futures, які роблять простими неблокірующіх і асинхронні перетворення, і підштовхує їх до клієнтів. Реактивний програмування c рівнем доступу до даних - оновлює і запитує їх в ефективному ресурсі, переважно з використанням баз даних SQL або NoSQL з асинхронними драйверами.

Веб-додатки також виграють від розробки реактивної системи для таких речей, як розподілене кешування, узгодженість даних і повідомлення з декількома вузлами. Традиційні веб-додатки зазвичай використовують стоять вузли. Але як тільки програмісти починають використовувати Server-Sent-Events (SSE) і WebSockets - ці вузли стають працездатними, оскільки, як мінімум, вони підтримують стан клієнтського з'єднання, а push-повідомлення надсилаються до них відповідним чином. Для цього потрібна розробка реактивної системи, так як це область, де важлива адресація одержувачів за допомогою обміну повідомленнями.

Суть реактивне програмування Java

Не потрібно обов'язково використовувати Rx в реактивних системах. Тому що Rx програмування і реактивні системи - не одне і теж. Хоча вони часто використовуються взаємозамінні терміни, але не є точними синонімами і відбивають різні речі. Системи являють собою наступний рівень «реактивності». Будь ласка, вважайте конкретні проектні та архітектурні рішення, які дозволяють створювати стійкі і гнучкі програми.

Проте дуже хороша ідея - комбінація методів - приносить ще більше переваг додатків, так як робить їх ще більш пов'язаними, дозволяє більш ефективно використовувати ресурси і забезпечує більш низьку затримку. Коли мова йде про величезні обсяги даних або багатозадачності, часто потрібна асинхронна обробка, щоб системи були швидкими і чуйними.

У Java - представника старого об'єктно-орієнтованого програмування, асинхронність може стати дійсно складною і зробити код важко зрозумілим і підтримуваним. Таким чином Rx особливо корисно для цієї «чисто» об'єктно-орієнтованого середовища, оскільки воно спрощує роботу з асинхронними потоками.

З його останніми випусками, починаючи з Java 8, сама Java зробила деякі спроби впровадити вбудовану реактивність, але ці спроби сьогодні не надто популярні у розробників. Проте є деякі живі і регулярно оновлювані сторонні реалізації для реактивного програмування Java, які допомагають заощадити день і тому особливо цінуються розробниками Java.

У звичайному додатку для зазвичай неодноразово виконують деякі операції реактивного програмування з використанням RxJava, тому потрібно порівняти швидкість, використання процесора і пам'яті з тими ж операціями, які були реалізовані як з співпрограмами Kotlin, так і з RxJava. Це початковий тест продуктивності.

Всякий раз, коли застосовується новий інструмент, який буде широко використовуватися в усьому кодексі, важливо зрозуміти, чи вплине воно на загальну продуктивність програми, перш ніж приймати рішення про те, наскільки доцільно використовувати його. Практика використання дає коротку відповідь: в більшості випадків користувачам варто подумати про заміну RxJava на співпрограми Kotlin, особливо в Android.

Реактивний програмування із застосуванням RxJava може як і раніше використовуватися в обмеженій кількості випадків, і в цих випадках можна змішувати, як RxJava, так і співпрограми.

Прості причини:

  1. Забезпечують набагато більшу гнучкість, ніж звичайне Rx.
  2. Надає багатий набір операторів в колекціях, які будуть виглядати так само, як з операторами RxJava.
  3. Реактивний програмування Kotlin можуть взаємодіяти при необхідності з використанням rxjava.
  4. Вони дуже легкі та ефективні, з огляду на більш високий рівень використання ЦП для збору сміття з усіх об'єктів, створених RxJava.

реактивні розширення

Reactive Extensions (ReactiveX або RX) - це бібліотека, яка слідує за принципами Rx, тобто складання асинхронних і заснованих на подіях програм з використанням спостерігається послідовності. Ці бібліотеки надають безліч інтерфейсів і методів, які допомагають розробникам писати чистий і простий код.

Реактивні розширення доступні на декількох мовах. Програмісти особливо зацікавлені в RxJava і RxAndroid, так як андроїд - це сама сфокусована область.

Реактивний програмування з використанням RxJava - це реалізація Java Reactive Extension з Netflix. В основному це бібліотека, яка становить асинхронні події, слідуючи шаблоном спостерігача.

Можна створювати асинхронний трафік, перетворювати і споживати їх спостерігачем в різних потоках даних. Бібліотека пропонує широкий спектр дивовижних операторів, таких як карта, об'єднання і фільтр, які можуть бути застосовані до потоку даних. Коли програміст почне використовувати фактичні приклади коду, він дізнається більше про операторів і перетвореннях.

Нить в додатках Android

Android "class \u003d" if uuid-2938324 "src \u003d" / misc / i / gallery / 73564 / 2938324.jpg "/\u003e

Android реактивне програмування (RxAndroid) специфічний для платформи Android з декількома доданими класами поверх RxJava. Більш конкретно - планувальники представлені в RxAndroid (AndroidSchedulers.mainThread ()), який грає важливу роль в підтримці концепції многопоточности в додатках для Android.

Крім усього іншого, фахівці радять використовувати тільки бібліотеку RxJava. Навіть завдяки великій кількості планувальників, використовуваних в програмуванні для Android.

Нижче наведено список планувальників і їх короткий зміст:

  1. Schedulers.io () - використовується для виконання неінтенсивних операцій, таких як Інтернет-дзвінки, читання дисків / файлів, операцій з базами даних і який підтримує пул потоків.
  2. AndroidSchedulers.mainThread () - забезпечує доступ до основної теми Thread / UI. Зазвичай в цьому потоці відбуваються операції, такі як оновлення призначеного для користувача інтерфейсу, взаємодія з користувачем. Фахівці радять користувачам, що вони не повинні виконувати будь-які інтенсивні операції над цим потоком, так як це може викликати кидок додатки або діалог ANR.
  3. Schedulers.newThread () - використовуючи це, новий потік буде створений кожен раз, коли запланована завдання. Зазвичай пропонується не використовувати розклад для дуже тривалих робіт. Нитки, створені за допомогою newThread (), що не будуть повторно застосовуватися.
  4. Schedulers.computation () - цей графік може застосовуватися для виконання інтенсивних операцій з процесором, по обробці величезних даних центру реактивного програмування, обробка растрових зображень. Кількість потоків, створених з використанням цього планувальника, повністю залежить від кількості доступних ядер ЦП.
  5. Schedulers.single () - цей планувальник виконає всі завдання в наступному порядку, що можна використовувати, коли потрібно необхідність послідовного виконання.
  6. Schedulers.immediate () - цей планувальник виконує завдання негайно, синхронно блокуючи основний потік.
  7. Schedulers.trampoline () - виконує завдання в режимі First In-First Out. Всі заплановані завдання будуть виконуватися одна за одною, обмежуючи кількість потоків фону одним.
  8. Schedulers.from () - дозволяє створювати планувальник від виконавця, обмежуючи кількість створюваних потоків. Коли пул потоків зайнятий, завдання будуть поставлені в чергу.

Тепер, коли є хороші теоретичні знання про RxJava і RxAndroid, можна перейти до деяких прикладів коду, щоб краще зрозуміти концепцію. Для початку роботи потрібно додати залежно RxJava і RxAndroid до проектів build.gradle і синхронізувати проект.

Програмування.

Observer підписують на Observable, щоб він міг почати отримувати дані, використовуючи два методи:

  1. SubscribeOn (Schedulers.io ()) - говорить Observable, щоб запустити задачу в фоновому потоці.
  2. ObservOn (AndroidSchedulers.mainThread ()) - вказує Observer отримувати дані в потоці призначеного для користувача інтерфейсу Android.

Ось і все, таким чином програміст зможе написати свою першу програму реактивного програмування з RxJava.

Підприємства і постачальники проміжного програмного забезпечення почали використовувати Reactive, а в 2016 -2018 роках спостерігався величезний ріст корпоративної зацікавленості в прийнятті цієї парадигми.

Rx пропонує продуктивність для розробників завдяки ресурсоефективності на рівні компонентів для внутрішньої логіки і перетворення потоку даних, в той час, як реактивні системи пропонують продуктивність для архітекторів і DevOps, завдяки стійкості і еластичності на рівні системи. Вони застосовуються для створення «Cloud Native» та інших широкомасштабних розподілених систем. На практиці також широко використовують книги про реактивному програмуванні Java з методами дозволяють комбінувати принципів проектування реактивних систем.

Світ ООП-розробки взагалі і мову Java зокрема живуть дуже активним життям. Тут є свої модні тенденції, і сьогодні розберемо один з головних трендів сезону - фреймворк ReactiveX. Якщо ти ще в стороні від цієї хвилі - обіцяю, вона тобі сподобається! Це точно краще, ніж джинси із завищеною талією :).

реактивний програмування

Як тільки ООП-мови доросли до масового застосування, розробники усвідомили, наскільки іноді не вистачає можливостей С-подібних мов. Оскільки написання коду в стилі функціонального програмування серйозно руйнує якість ООП-коду, а значить, і підтримувані проекту, був придуманий гібрид - реактивне програмування.

Парадигма реактивної розробки будується на ідеї постійного відстеження змін стану об'єкта. Якщо такі зміни відбулися, то всі зацікавлені об'єкти повинні отримати вже оновлені дані і працювати тільки з ними, забувши про старі.

Хорошим прикладом ідеї реактивного програмування може служити Excel-таблиця. Якщо зв'язати кілька осередків однією формулою, результат обчислення буде змінюватися кожного разу, коли зміняться дані в цих осередках. Для бухгалтерії таке динамічна зміна даних - звична справа, але для програмістів це швидше виняток.

A \u003d 3; b \u003d 4; c \u003d a + b; F1 (c); a \u003d 1; F2 (c);

У цьому прикладі функції F1 і F2 будуть працювати з різними значеннями змінної C. Часто потрібно, щоб у обох функцій були тільки найактуальніші дані, - реактивне програмування дозволить без зміни логіки самих функцій відразу ж викликати F1 з новими параметрами. Така побудова коду дає додатку можливість моментально реагувати на будь-які зміни, що зробить його швидким, гнучким і чуйним.

ReactiveX

Втілювати з нуля ідеї реактивного програмування може бути досить клопітно - є підводні камені, та й часу це займе пристойно. Тому для багатьох розробників ця парадигма залишалася тільки теоретичним матеріалом, поки не з'явився ReactiveX.

Фреймворк ReactiveX - це інструмент для реактивного програмування, що працює з усіма популярними ООП-мовами. Самі творці називають його мультиплатформенним API для асинхронної розробки, заснованим на паттерне «Спостерігач» (Observer).

Якщо термін «реактивне програмування» - це свого роду теоретична модель, то патерн «Спостерігач» - готовий механізм відстеження змін в програмі. А відстежувати їх доводиться досить часто: завантаження і оновлення даних, оповіщення про події і так далі.

Патерн «Спостерігач» існує приблизно стільки ж, скільки і саме ООП. Об'єкт, стан якого може помінятися, називається видавцем (популярний переклад терміна Observable). Всі інші учасники, яким цікаві ці зміни, - передплатники (Observer, Subscriber). Для отримання повідомлень передплатники реєструються у видавця, явно вказуючи свій ідентифікатор. Видавець час від часу генерує повідомлення, які їм же розсилаються за списком зареєстрованих передплатників.

Власне, творці ReactiveX не придумали нічого революційного, вони просто зручно реалізували патерн. І хоча в багатьох ООП-мовах, і в Java зокрема, є готові реалізації паттерна, в цьому фреймворку присутній додатковий «тюнінг», який перетворює «Спостерігач» в дуже потужний інструмент.

RxAndroid

Порт бібліотеки ReactiveX для світу Android називається rxAndroid і підключається, як завжди, через Gradle.

Compile "io.reactivex: rxandroid: 1.1.0"

Видавець, що генерує повідомлення, тут задається за допомогою класу Observable. У видавця може бути кілька передплатників, для їх реалізації скористаємося класом Subscriber. Стандартну поведінку для Observable - випустити одне або кілька повідомлень для передплатників, а потім завершити свою роботу або видати повідомлення про помилку. Як повідомлень можуть бути як змінні, так і цілі об'єкти.

Rx.Observable myObserv \u003d rx.Observable.create (new rx.Observable.OnSubscribe () (@Override public void call (Subscriber subscriber) (subscriber.onNext ( "Hello"); subscriber.onNext ( "world"); subscriber.onCompleted ();)));

В даному випадку видавець myObserv спочатку відправить рядки hello і message, а потім повідомлення про успішне завершення роботи. Видавець може викликати методи onNext (), onCompleted () і onEror (), тому у передплатників вони повинні бути визначені.

Subscriber mySub \u003d new Subscriber () (... @Override public void onNext (String value) (Log.e ( "got data", "" + value);));

Все готово для роботи. Залишилося пов'язати об'єкти між собою - і «Hello, world!» в реактивному програмуванні готовий!

MyObserv.subscribe (mySub);

Треба сказати, що це був дуже простий приклад. У ReactiveX є безліч варіантів поведінки всіх учасників патерну: фільтрація, групування, обробка помилок. Користь від реактивного програмування можна відчути, тільки спробувавши його в справі. Приступимо до задачі серйозніше.

Продовження доступно тільки учасникам

Варіант 1. Приєднайся до товариства «сайт», щоб читати всі матеріали на сайті

Членство в співтоваристві протягом зазначеного терміну відкриє тобі доступ до ВСІХ матеріалами «Хакера», збільшить особисту накопичувальну знижку і дозволить накопичувати професійний рейтинг Xakep Score!

На сьогоднішній день існує цілий ряд методологій програмування складних систем реального часу. Одна з таких методологій носить назву ФРП (FRP). Вона ввібрала в себе шаблон проектування, званий спостерігач (Observer) З одного боку, а з іншого, як не важко здогадатися, принципи функціонального програмування. У цій статті ми розглянемо функціональне реактивне програмування на прикладі реалізації його в бібліотеці Sodium для мови Haskell.

Теоретична частина

Традиційно програми прийнято ділити на три класи:

  • пакетні
  • Інтерактивні
  • реактивні

Відмінності між цими трьома класами програм криються в типі їх взаємодії із зовнішнім світом.

Виконання пакетних програм ніяк не синхронізується із зовнішнім світом. Вони можуть бути запущені в певний момент часу і завершені тоді, коли вихідні дані будуть оброблені і отриманий результат. На відміну від пакетних інтерактивні і реактивні програми обмінюються даними з зовнішнім світом безперервно в процесі своєї роботи з моменту запуску до моменту зупинки.

Реактивна програма на відміну від інтерактивної жорстко синхронізована з зовнішнім середовищем: від неї вимагається реагувати на події зовнішнього світу з прийнятною затримкою в темпі виникнення цих подій. У той же час інтерактивної програми дозволено змушувати зовнішнє середовище чекати. Наприклад, текстовий редактор є інтерактивною програмою: користувач, що запустив процес виправлення помилок у тексті, повинен дочекатися його завершення, щоб продовжити редагування. Однак, автопілот є реактивної програмою, оскільки при виникненні перешкоди, вона повинна відразу ж скорегувати курс, щоб зробити маневр обльоту, адже реальний світ не можна поставити на паузу, як користувача текстового редактора.

Ці два класи програм з'явилися дещо пізніше, тоді, коли комп'ютери почали використовуватися в управлінні машинами, і стали розвиватися інтерфейси. З тих пір змінилося безліч методик реалізації, кожна наступна з яких покликана була виправити недоліки попередніх. Спочатку це були прості подієво-керовані програми: в системі виділяли деякий безліч подій, на які необхідно було реагувати, і створювали обробники цих подій. Обробники в свою чергу також могли генерувати події, які йшли до зовнішнього світу. Ця модель була проста і вирішувала деяке коло простих інтерактивних завдань.

Але з часом інтерактивні і реактивні програми ставали складніше і подієво-орієнтоване програмування перетворилося в пекло. Виникла потреба в більш просунутих інструментах синтезу подієво-керованих систем. У цій методології було два головних вад:

  • неявне стан
  • недетермінірованность

Щоб виправити це, спочатку був створений шаблон спостерігач (observer), Трансформувати події в мінливі в часі значення. Набір цих значень представляв собою явне стан, в якому знаходиться програма. Так на зміну обробці подій прийшла звична для пакетних програм робота з даними. Розробник міг змінювати спостережувані значення і підписувати обробники на зміну цих значень. З'явилася можливість реалізовувати залежні значення, які змінювалися за заданим алгоритмом, при зміні тих значень, від яких вони залежали.

Хоч події в цьому підході і пішли, але потреба в них все ж залишилася. Далеко не завжди подія має на увазі зміна значення. Наприклад, подія реального часу має на увазі збільшення лічильника часу на секунду, проте подія спрацьовування будильника щодня у визначений час зовсім не має на увазі ніякого значення. Звичайно, можна зв'язати і ця подія з певним значенням, але це буде штучним прийомом. Наприклад, ми можемо ввести значення: время_срабативанія_будільніка \u003d\u003d остаток_от_деленія (текущее_время, 24 * 60 * 60). Але це буде не зовсім те, що нас цікавить, оскільки ця змінна прив'язана до секунді, і насправді значення змінюється двічі. Щоб з'ясувати, що будильник спрацював, передплатник повинен визначати, що значення стало справжнім, а не навпаки. Значення буде істинним рівно одну секунду, а якщо ми змінимо період тиків з секунди, скажімо, на 100 мілісекунд, то і значення істина буде вже не секунду, а ці 100 мілісекунд.

Поява методології функціонального реактивного програмування це своєрідна відповідь функціональщіков на шаблон спостерігач. У FRP були переосмислені вироблені підходи: події (Events) нікуди не поділися, однак, спостерігаються значення також з'явилися і були названі характеристиками (Behaviors). Подія в цій методології є якимось дискретно генерується значення, а характеристика --- безперервно генерується. Обидва вони можуть бути пов'язані між собою: характеристики можуть генерувати події, а події виступати в якості джерел для характеристик.

Проблема недетермінованости стану набагато складніша. Вона виникає з того факту, що подієво-керовані системи де-факто асинхронні. Це тягне виникнення проміжних станів системи, які можуть бути з різних причин неприпустимі. Для вирішення цієї проблеми з'явилося так зване синхронне програмування.

Практична частина

бібліотека Sodium з'явилася як проект реалізації FRP із загальним інтерфейсом на різних мовах програмування. У ній присутні всі елементи методології: примітиви (Event, Behavior) і шаблони їх використання.

Примітиви і взаємодія з зовнішнім світом

Два основних примітиву, з якими нам належить працювати, це:

  • Event a - подія зі значенням типу a
  • Behavior a - характеристика (або мінливий значення) типу a

Ми можемо створювати нові події і значення функціями newEvent і newBehavior:

NewEvent :: Reactive (Event a, a -\u003e Reactive ()) newBehavior :: a -\u003e Reactive (Behavior a, a -\u003e Reactive ())

Як бачимо, обидві ці функції можуть бути викликані тільки в монаді Reactive, В результаті повертається власне сам примітив, а також функція, яка повинна бути викликана для активації події або не будуть змінені. Функція створення характеристики приймає в якості першого аргументу початкове значення.

Щоб зв'язати реальний світ з реактивної програмою, існує функція sync, А щоб зв'язати програму з зовнішнім світом існує функція listen:

Sync :: Reactive a -\u003e IO a listen :: Event a -\u003e (a -\u003e IO ()) -\u003e Reactive (IO ())

Перша функція, як можна зрозуміти з назви, виконує деякий реактивний код синхронно, вона дозволяє потрапити в контекст Reactive з контексту IO, А друга служить для додавання обробників подій, що виникають в контексті Reactive, Що виконуються в контексті IO. функція listen повертає функцію unlisten, Яку потрібно викликати, щоб від'єднати обробник.

Таким чином реалізується своєрідний механізм транзакцій. Коли ми робимо щось всередині монади Reactive, код виконується в межах однієї транзакції в момент виклику функції sync. Стан детерміновано тільки поза контекстом транзакції.

Це базис реактивного функціонального програмування, який достатній для написання програм. Може трохи збентежити той факт, що слухати можна тільки події. Саме так і має бути, як ми побачимо далі між подіями і характеристиками існують тісні взаємозв'язки.

Операції над основними примітивами

Для зручності в методологію внесені додаткові функції, що перетворюють події і характеристики. Розглянемо деякі з них:

Подія, яке ніколи не відбудеться - (може використовуватися в якості заглушки) never :: Event a - Об'єднання двох подій однакового типу в одне - (зручно для визначення одного обробника для класу подій) merge :: Event a -\u003e Event a -\u003e Event a - Витягає значення з Maybe подій - (відокремлюємо зерна від плевел) filterJust :: Event (Maybe a) -\u003e Event a - Перетворює подія в характеристику з початковим значенням - (міняємо значення при виникненні подій) hold :: a -\u003e Event a -\u003e Reactive (Behavior a) - Перетворює характеристику в подія - (генеруємо події при зміні значення) updates :: Behavior a -\u003e Event a - Перетворює характеристику в подія - (також генеруємо подію для початкового значення) value :: Behavior a -\u003e Event a - При виникненні події бере значення характеристики, - застосовує функцію і генерує подія snapshot :: (a -\u003e b -\u003e c) -\u003e Event a -\u003e Behavior b - \u003e Event c - Отримує поточне значення характеристики sample :: Behavior a -\u003e Reactive a - Зводить повторювані події в одне coalesce :: (a -\u003e a -\u003e a) -\u003e Event a -\u003e Event a - Пригнічує всі події, крім першого once :: Event a -\u003e Event a - Розділяє подія зі списком на кілька подій split :: Event [a] -\u003e Event a

Сферичні приклади в вакуумі

Давайте спробуємо що-небудь написати:

Import FRP.Sodium main \u003d do sync $ do - створюємо подія (e1, triggerE1)<- newEvent -- создаём характеристику с начальным значением 0 (v1, changeV1) <- newBehavior 0 -- определяем обработчик для события listen e1 $ \_ -> putStrLn $ "e1 triggered" - визначаємо обробник для зміни значення характеристики listen (value v1) $ \\ v -\u003e putStrLn $ "v1 value:" ++ show v - Генеруємо подія без значення triggerE1 () - Змінюємо значення характеристики changeV1 13

встановимо пакет Sodium за допомогою Cabal і запустимо приклад в інтерпретаторі:

# Якщо хочемо працювати в окремій пісочниці # створюємо її\u003e cabal sandbox init # встановимо\u003e cabal install sodium\u003e cabal repl GHCi, version 7.6.3: http://www.haskell.org/ghc/:? for help Loading package ghc-prim ... linking ... done. Loading package integer-gmp ... linking ... done. Loading package base ... linking ... done. # Завантажимо приклад Prelude\u003e: l Example.hs Compiling Main (Example.hs, interpreted) Ok, modules loaded: Main. # Виконаємо приклад * Main\u003e

А тепер поекспериментуємо. Закомментіруем рядок, де ми змінюємо наше значення (changeV1 13) і перезапустити приклад:

* Main\u003e: l Example.hs Compiling Main (Example.hs, interpreted) Ok, modules loaded: Main. * Main\u003e main e1 triggered v1 value: 0

Як бачимо, тепер виводиться початкове значення, так відбувається тому що функція value генерує перша подія з початковим значенням характеристики. Давайте замінимо функцію value на updates і подивимося, що вийшло:

* Main\u003e: l Example.hs Compiling Main (Example.hs, interpreted) Ok, modules loaded: Main. * Main\u003e main e1 triggered

Тепер початкове значення не виводиться, але якщо розкоментувати рядок, в якій ми змінили значення, то як і раніше буде виведено змінений значення. Давайте повернемо все, як було, і сгенерируем подія e1 двічі:

* Main\u003e: l Example.hs Compiling Main (Example.hs, interpreted) Ok, modules loaded: Main. * Main\u003e main e1 triggered e1 triggered v1 value: 13

Як бачимо, подія також спрацювало двічі. Спробуємо цього уникнути, для чого в функції listen замінимо аргумент e1 на (Once e1), Тим самим створивши нову подію, що спрацьовує один раз:

* Main\u003e: l Example.hs Compiling Main (Example.hs, interpreted) Ok, modules loaded: Main. * Main\u003e main e1 triggered v1 value: 13

Коли у події відсутня аргумент, нам важливий сам факт наявності або відсутності події, тому функція once для об'єднання подій вірний вибір. Однак, коли аргумент присутній, це не завжди підходить. Перепишемо приклад наступним чином:

<- newEvent (v1, changeV1) <- newBehavior 0 listen e1 $ \v -> putStrLn $ "e1 triggered with:" ++ show v listen (value v1) $ \\ v -\u003e putStrLn $ "v1 value:" ++ show v triggerE1 "a" triggerE1 "b" triggerE1 "c" changeV1 13

Отримаємо, як і очікували, всі події зі значеннями в тому ж порядку, в якому вони були згенеровані:

* Main\u003e: l Example.hs Compiling Main (Example.hs, interpreted) Ok, modules loaded: Main. * Main\u003e main e1 triggered with: "a" e1 triggered with: "b" e1 triggered with: "c" v1 value: 13

Якщо використовуємо функцію once з e1, То отримаємо тільки перша подія, тому спробуємо використовувати функцію coalesce, Для чого замінимо аргумент e1 в listen аргументом (Coalesce (\\ _ a -\u003e a) e1):

* Main\u003e: l Example.hs Compiling Main (Example.hs, interpreted) Ok, modules loaded: Main. * Main\u003e main e1 triggered with: "c" v1 value: 13

І дійсно, ми отримали тільки остання подія.

ще приклади

Давайте розглянемо приклади складніше:

Import FRP.Sodium main \u003d do sync $ do (e1, triggerE1)<- newEvent -- создаём характеристику, изменяемую событием e1 v1 <- hold 0 e1 listen e1 $ \v -> putStrLn $ "e1 triggered with:" ++ show v listen (value v1) $ \\ v -\u003e putStrLn $ "v1 value is:" ++ show v - генеруємо події triggerE1 1 triggerE1 2 triggerE1 3

Ось що маємо на виході:

* Main\u003e: l Example.hs Compiling Main (Example.hs, interpreted) Ok, modules loaded: Main. * Main\u003e main e1 triggered with: 1 e1 triggered with: 2 e1 triggered with: 3 v1 value is: 3

Значення характеристики виводиться тільки один раз, хоча подій було згенеровано кілька. У цьому полягає особливість синхронного програмування: характеристики синхронізовані з викликом sync. Щоб це продемонструвати, злегка переробимо наш приклад:

<- sync $ do (e1, triggerE1) <- newEvent v1 <- hold 0 e1 listen e1 $ \v -> putStrLn $ "e1 triggered with:" ++ show v listen (value v1) $ \\ v -\u003e putStrLn $ "v1 value is:" ++ show v return triggerE1 sync $ triggerE1 1 sync $ triggerE1 2 sync $ triggerE1 3

Ми всього-лише винесли тригер події у зовнішній світ і викликаємо його в різних фазах синхронізації:

* Main\u003e: l Example.hs Compiling Main (Example.hs, interpreted) Ok, modules loaded: Main. * Main\u003e main v1 value is: 0 e1 triggered with: 1 v1 value is: 1 e1 triggered with: 2 v1 value is: 2 e1 triggered with: 3 v1 value is: 3

Тепер при кожній події демонструється нове значення.

Інші операції над примітивами

Розглянемо наступну групу корисних функції:

Об'єднує події з використанням функції mergeWith :: (a -\u003e a -\u003e a) -\u003e Event a -\u003e Event a -\u003e Event a - Фільтрує події, залишаючи тільки ті, - для яких функція повертає істину filterE :: (a -\u003e Bool) -\u003e Event a -\u003e Event a - Дозволяє "вимикати" події - коли характеристика дорівнює False gate :: Event a -\u003e Behavior Bool -\u003e Event a - Організовує перетворювач подій - з внутрішнім станом collectE: : (a -\u003e s -\u003e (b, s)) -\u003e s -\u003e Event a -\u003e Reactive (Event b) - Організовує перетворювач характеристик - з внутрішнім станом collect :: (a -\u003e s -\u003e (b , s)) -\u003e s -\u003e Behavior a -\u003e Reactive (Behavior b) - Створює характеристику як результат накопичення подій accum :: a -\u003e Event (a -\u003e a) -\u003e Reactive (Behavior a)

Звичайно, це ще далеко не всі функції, що надаються бібліотекою. Є і куди більш екзотичні речі, які виходять за рамки даної статті.

приклади

Давайте спробуємо ці функції в дії. Почнемо з останньої, організуємо щось на зразок калькулятора. Нехай у нас буде якесь значення, до якого можна застосовувати арифметичні функції і отримувати результат:

Import FRP.Sodium main \u003d do triggerE1<- sync $ do (e1, triggerE1) <- newEvent -- пусть начальное значение будет равно 1 v1 <- accum (1:: Int) e1 listen (value v1) $ \v -> putStrLn $ "v1 value is:" ++ show v return triggerE1 - додаємо 1 sync $ triggerE1 (+ 1) - множимо на 2 sync $ triggerE1 (* 2) - віднімаємо 3 sync $ triggerE1 (+ (-3) ) - додаємо 5 sync $ triggerE1 (+ 5) - зводимо в ступінь 3 sync $ triggerE1 (^ 3)

запустимо:

* Main\u003e: l Example.hs Compiling Main (Example.hs, interpreted) Ok, modules loaded: Main. * Main\u003e main v1 value is: 1 v1 value is: 2 v1 value is: 4 v1 value is: 1 v1 value is: 6 v1 value is: 216

Може здатися, що набір можливостей досить мізерний, але насправді це не так. Адже ми маємо справу з Хаскела, аплікативного функтори і монади нікуди не поділися. Ми можемо виконувати над характеристиками і подіями будь-які операції, які ми звикли виконувати над чистими значеннями. В результаті чого виходять нові характеристики та події. Для характеристик реалізований клас функтор і аплікативного функтор, для подій ж з очевидних причин лише функтор.

наприклад:

<$>), (<*>)) Import FRP.Sodium main \u003d do (setA, setB)<- sync $ do (a, setA) <- newBehavior 0 (b, setB) <- newBehavior 0 -- Новая характеристика a + b let a_add_b = (+) <$> a<*> b - Нова характеристика a * b let a_mul_b \u003d (*)<$> a<*> b listen (value a) $ \\ v -\u003e putStrLn $ "a \u003d" ++ show v listen (value b) $ \\ v -\u003e putStrLn $ "b \u003d" ++ show v listen (value a_add_b) $ \\ v - \u003e putStrLn $ "a + b \u003d" ++ show v listen (value a_mul_b) $ \\ v -\u003e putStrLn $ "a * b \u003d" ++ show v return (setA, setB) sync $ do setA 2 setB 3 sync $ setA 3 sync $ setB 7

Ось що буде виведено в інтерпретаторі:

λ\u003e main a \u003d 0 b \u003d 0 a + b \u003d 0 a * b \u003d 0 a \u003d 2 b \u003d 3 a + b \u003d 5 a * b \u003d 6 a \u003d 3 a + b \u003d 6 a * b \u003d 9 b \u003d 7 a + b \u003d 10 a * b \u003d 21

А тепер подивимося як щось подібне працює з подіями:

Import Control.Applicative ((<$>)) Import FRP.Sodium main \u003d do sigA<- sync $ do (a, sigA) <- newEvent let a_mul_2 = (* 2) <$> a let a_pow_2 \u003d (^ 2)<$> a listen a $ \\ v -\u003e putStrLn $ "a \u003d" ++ show v listen a_mul_2 $ \\ v -\u003e putStrLn $ "a * 2 \u003d" ++ show v listen a_pow_2 $ \\ v -\u003e putStrLn $ "a ^ 2 \u003d "++ show v return sigA sync $ do sigA 2 sync $ sigA 3 sync $ sigA 7

Ось що буде виведено:

λ\u003e main a \u003d 2 a * 2 \u003d 4 a ^ 2 \u003d 4 a \u003d 3 a * 2 \u003d 6 a ^ 2 \u003d 9 a \u003d 7 a * 2 \u003d 14 a ^ 2 \u003d 49

У документації є перелік примірників класів, які реалізовані для Behavior і Event, Але ніщо не заважає реалізувати екземпляри відсутніх класів.

Зворотний бік реактивності

Функціональне реактивне програмування безсумнівно спрощує розробку складних систем реального часу, проте існує безліч аспектів, які необхідно враховувати при використанні цього підходу. Тому розглянемо тут проблеми, які найбільш часто мають місце бути.

неодночасність

Синхронне програмування має на увазі якийсь механізм транзакцій, що забезпечує узгодженість послідовно змінюють один одного станів системи, і, отже, відсутність проміжних несподіваних станів. В Sodium за транзакції відповідають виклики sync. Хоч стан усередині транзакції не визначено, однак не можна вважати, що все всередині неї відбувається одночасно. Значення змінюються в певному порядку, який впливає на результат. Так, наприклад, спільне використання подій і характеристик може викликати несподівані ефекти. Розглянемо приклад:

Import Control.Applicative ((<$>)) Import FRP.Sodium main \u003d do setVal<- sync $ do (val, setVal) <- newBehavior 0 -- создаём булеву характеристику val > 2 let gt2 \u003d (\u003e 2)<$> val - створюємо подія зі значеннями, які\u003e 2 let evt \u003d gate (value val) gt2 listen (value val) $ \\ v -\u003e putStrLn $ "val \u003d" ++ show v listen (value gt2) $ \\ v -\u003e putStrLn $ "val\u003e 2?" ++ show v listen evt $ \\ v -\u003e putStrLn $ "val\u003e 2:" ++ show v return setVal sync $ setVal 1 sync $ setVal 2 sync $ setVal 3 sync $ setVal 4 sync $ setVal 0

Можна очікувати виведення на зразок цього:

Val \u003d 0 val\u003e 2? False val \u003d 1 val\u003e 2? False val \u003d 2 val\u003e 2? False val \u003d 3 val\u003e 2? True val\u003e 2: 3 val \u003d 4 val\u003e 2? True val\u003e 2: 4 val \u003d 0 val\u003e 2? False

Однак насправді рядок val\u003e 2: 3 буде відсутній, а в кінці з'явиться рядок val\u003e 2: 0. Так відбувається тому, що подія зміни значення (Value val) генерується до того, як буде обчислена залежна характеристика gt2, І тому подія evt не виникає для встановленого значення 3. В кінці ж, коли ми знову встановили 0, обчислення характеристики gt2 запізнюється.

Загалом, ефекти ті ж, що і в аналоговій і цифровій електроніці: гонки сигналів, для боротьби з якими використовують різні прийоми. Зокрема, синхронізацію. Так ми і зробимо, щоб змусити цей код працювати належним чином:

Import Control.Applicative ((<$>)) Import FRP.Sodium main \u003d do (sigClk, setVal)<- sync $ do -- Мы ввели новое событие clk -- сигнал синхронизации -- прям как в цифровой электронике (clk, sigClk) <- newEvent (val, setVal) <- newBehavior 0 -- Также вы создали альтернативную функцию -- получения значения по сигналу синхронизации -- и заменили все вызовы value на value" let value" = snapshot (\_ v -> v) clk let gt2 \u003d (\u003e 2)<$> val let evt \u003d gate (value "val) gt2 listen (value" val) $ \\ v -\u003e putStrLn $ "val \u003d" ++ show v listen (value "gt2) $ \\ v -\u003e putStrLn $" val\u003e 2? "++ show v listen evt $ \\ v -\u003e putStrLn $" val\u003e 2: "++ show v return (sigClk, setVal) - Ввели нову функцію sync" - яка викликає сигнал синхронізації - в кінці кожної транзакції - - І замінили їй все виклики sync let sync "a \u003d sync $ a \u003e\u003e sigClk () sync" $ setVal 1 sync "$ setVal 2 sync" $ setVal 3 sync "$ setVal 4 sync" $ setVal 0

Тепер наш висновок став таким як і очікувалося:

λ\u003e main val \u003d 0 val\u003e 2? False val \u003d 1 val\u003e 2? False val \u003d 2 val\u003e 2? False val \u003d 3 val\u003e 2? True val\u003e 2: 3 val \u003d 4 val\u003e 2? True val\u003e 2: 4 val \u003d 0 val\u003e 2? False

ліниво

Проблеми іншого роду пов'язані з ледачою природою обчислень в Haskell. Це призводить до того, що при випробуванні коду в інтерпретаторі, деякий висновок в кінці може просто не бути. Що можна запропонувати в цьому випадку, так виконати даремний крок синхронізації в кінці, наприклад sync $ return ().

висновок

На цьому поки, думаю, досить. На даний момент один з авторів бібліотеки Sodium пише книгу про ФРП. Будемо сподіватися, це якось заповнить прогалини в даній області програмування і послужить популяризації прогресивних підходів в наших закостенілих умах.

Поїхали.

Реактивний програмування спершу звучить, як назва зароджується парадигми, але насправді, відноситься до методу програмування, в якому для роботи з асинхронними потоками даних використовується подієво-орієнтований підхід. Грунтуючись на постійно поточних даних, реактивні системи реагують на них шляхом виконання ряду подій.
Реактивний програмування слід шаблоном проектування "Спостерігач", який можна визначити наступним чином: якщо в одному об'єкті відбувається зміна стану, то всі інші об'єкти сповіщаються і оновлюються відповідним чином. Тому, замість того, щоб опитувати події на предмет змін, події пушаться асинхронно, щоб спостерігачі могли їх обробити. У цьому прикладі, спостерігачі - функції, які виконуються, коли подія надіслано. А згаданий потік даних - фактичний спостережуваний.

Майже всі мови і фреймворки використовують цей підхід в своїй екосистемі, і останні версії Java - не виняток. У цій статті я поясню як можна застосувати реактивне програмування, використовуючи останню версію JAX-RS в Java EE 8 і функціонал Java 8.

реактивний Маніфест

У Реактивному Маніфесті перераховані чотири фундаментальних аспекти, необхідних додатку, щоб бути більш гнучким, слабо пов'язаним і простим для масштабування, а отже і здатним бути реактивним. У ньому говориться, що програма має бути чуйним, гнучким (а значить і масштабується), стійким і message-driven.

Основна мета - дійсно чуйне додаток. Припустимо, є додаток, в якому обробкою запитів користувачів займається один великий потік, і після виконання роботи цей потік відправляє відповіді назад оригінальним запитувачам. Коли додаток отримує більше запитів, ніж може обробити, цей потік стає bottleneck'ом, і додаток втрачає свою колишню чуйність. Щоб зберегти чуйність, програма має бути масштабованим і стійким. Стійким можна вважати додаток, в якому є функціонал для авто-відновлення. З досвіду більшості розробників, тільки message-driven архітектура дозволяє додатку бути масштабованим, стійким і чуйним.

Реактивний програмування стало впроваджуватися в версії Java 8 і Java EE 8. Мова Java представив такі поняття, як CompletionStage, і його реалізацію CompletableFuture, а Java почав використовувати ці функції в таких специфікаціях, як Reactive Client API в JAX-RS.

JAX-RS 2.1 Reactive Client API

Подивимося, як реактивне програмування може використовуватися в додатках Java EE 8. Щоб розібратися в процесі, потрібні базові знання API Java EE.

JAX-RS 2.1 представив новий спосіб створення REST клієнта з підтримкою реактивного програмування. Дефолтна реалізація invoker, пропонована в JAX-RS - синхронна, це означає, що створюваний клієнт відправить блокуючий виклик точки призначення (endpoint) сервера. Приклад реалізації представлений в Listing 1.

Response response \u003d ClientBuilder.newClient () .target ( "http: // localhost: 8080 / service-url") .request () .get ();
Починаючи з версії 2.0, JAX-RS надає підтримку створення асинхронного invoker на клієнтському API за допомогою простого виклику методу async (), як показано в Listing 2.

Future response \u003d ClientBuilder.newClient () .target ( "http: // localhost: 8080 / service-url") .request () .async () .get ();
Використання асинхронного invoker на клієнті повертає інстанси Future з типом javax.ws.rs.core.Response. Це може привести до опитування відповіді, з викликом future.get (), або реєстрації колбек, який буде викликатися при наявності доступного HTTP відповіді. Обидві реалізації підходять для асинхронного програмування, але все зазвичай ускладнюється, якщо ви хочете згрупувати зворотні виклики або додати умовні кейси в ці асинхронні мінімуми виконання.

JAX-RS 2.1 надає реактивний спосіб подолання цих проблем з новим JAX-RS Reactive Client API для збирання клієнта. Це так само просто, як виклик rx () методу під час складання клієнта. У Listing 3 rx () метод повертає реактивний invoker, який існує під час виконання клієнта, і клієнт повертає відповідь з типом CompletionStage.rx (), який дозволяє перехід від синхронного invoker до асинхронного за допомогою простого виклику.

CompletionStage response \u003d ClientBuilder.newClient () .target ( "http: // localhost: 8080 / service-url") .request () .rx \u200b\u200b() .get ();
CompletionStage<Т> - новий інтерфейс, введений в Java 8. Він являє обчислення, яке може бути етапом в рамках більшого обчислення, як і випливає з назви. Це єдиний представник реактивності Java 8, який потрапив в JAX-RS.
Після отримання інстанси відповіді, я можу викликати AcceptAsync (), де я можу надати фрагмент коду, який буде виконуватися асинхронно, коли відповідь стане доступним, як це показано в Listing 4.

Response.thenAcceptAsync (res -\u003e (Temperature t \u003d res.readEntity (Temperature.class); // do stuff with t));
Додавання реактивності в точку endpoint REST

Реактивний підхід не обмежується клієнтської стороною в JAX-RS; його можна використовувати і на стороні сервера. Для прикладу, спершу я створю простий сценарій, де зможу запросити список місць розташування однієї точки призначення. Для кожного положення я зроблю окремий виклик з даними розташування до іншої точки, щоб отримати значення температури. Взаємодія точок призначення буде таким, як показано на Figure 1.

Figure 1. Взаємодія між точками призначення

Спочатку я просто визначаю модель області визначення, а потім сервіси для кожної моделі. У Listing 5 показано, як визначається клас Forecast, який обертає класи Location і Temperature.

Public class Temperature (private Double temperature; private String scale; // getters & setters) public class Location (String name; public Location () () public Location (String name) (this.name \u003d name;) // getters & setters ) public class Forecast (private Location location; private Temperature temperature; public Forecast (Location location) (this.location \u003d location;) public Forecast setTemperature (final Temperature temperature) (this.temperature \u003d temperature; return this;) // getters)
Для обгортки списку прогнозів, клас ServiceResponse імплементований в Listing 6.

Public class ServiceResponse (private long processingTime; private List forecasts \u003d new ArrayList<>(); public void setProcessingTime (long processingTime) (this.processingTime \u003d processingTime;) public ServiceResponse forecasts (List forecasts) (this.forecasts \u003d forecasts; return this;) // getters)
LocationResource, показаний в Listing 7, визначає три зразка розташування, що повертаються з шляхом / location.

@Path ( "/ location") public class LocationResource (@GET @Produces (MediaType.APPLICATION_JSON) public Response getLocations () (List locations \u003d new ArrayList<>(); locations.add (new Location ( "London")); locations.add (new Location ( "Istanbul")); locations.add (new Location ( "Prague")); return Response.ok (new GenericEntity \u003e (Locations) ()). Build (); ))
TemperatureResource, показаний в Listing 8, повертає випадково сгенерированное значення температури між 30 і 50 для заданої локації. Затримка в 500 мс додана в імплементацію для симуляції зчитування датчика.

@Path ( "/ temperature") public class TemperatureResource (@GET @Path ( "/ (city)") @Produces (MediaType.APPLICATION_JSON) public Response getAverageTemperature (@PathParam ( "city") String cityName) (Temperature temperature \u003d new Temperature (); temperature.setTemperature ((double) (new Random (). nextInt (20) + 30)); temperature.setScale ( "Celsius"); try (Thread.sleep (500);) catch (InterruptedException ignored) (ignored.printStackTrace ();) return Response.ok (temperature) .build ();))
Спочатку я покажу реалізацію синхронного ForecastResource (дивіться Listing 9), що видає розташування. Потім, для кожного положення він викликає температурний сервіс, щоб отримати значення в градусах за Цельсієм.

@Path ( "/ forecast") public class ForecastResource (@Uri ( "location") private WebTarget locationTarget; @Uri ( "temperature / (city)") private WebTarget temperatureTarget; @GET @Produces (MediaType.APPLICATION_JSON) public Response getLocationsWithTemperature () (long startTime \u003d System.currentTimeMillis (); ServiceResponse response \u003d new ServiceResponse (); List locations \u003d locationTarget .request () .get (new GenericType \u003e () ()); locations.forEach (location -\u003e (Temperature temperature \u003d temperatureTarget .resolveTemplate ( "city", location.getName ()) .request () .get (Temperature.class); response.getForecasts (). add (new Forecast (location) .setTemperature (temperature));)); long endTime \u003d System.currentTimeMillis (); response.setProcessingTime (endTime - startTime); return Response.ok (response) .build (); ))
Коли точка призначення прогнозу запитується як / forecast, ви отримаєте висновок, схожий на той, що зазначений в Listing 10. Зверніть увагу, що час обробки запиту зайняло 1.533 мс, що логічно, так як синхронний запит значень температури з трьох різних місць розташування додає до 1.5 мс.

( "Forecasts": [( "location": ( "name": "London"), "temperature": ( "scale": "Celsius", "temperature": 33)), ( "location": ( "name ":" Istanbul ")," temperature ": (" scale ":" Celsius "," temperature ": 38)), (" location ": (" name ":" Prague ")," temperature ": (" scale ":" Celsius "," temperature ": 46))]," processingTime ": 1 533)
Поки все йде за планом. Настав час ввести реактивне програмування на стороні сервера, де виклики до кожної локації можуть виконуватися паралельно після отримання всіх місць розташування. Це явно може поліпшити синхронний потік, показаний раніше. Це виконується в Listing 11, де показано визначення реактивної версії сервісу прогнозів.

@Path ( "/ reactiveForecast") public class ForecastReactiveResource (@Uri ( "location") private WebTarget locationTarget; @Uri ( "temperature / (city)") private WebTarget temperatureTarget; @GET @Produces (MediaType.APPLICATION_JSON) public void getLocationsWithTemperature (@Suspended final AsyncResponse async) (long startTime \u003d System.currentTimeMillis (); // Створити етап (stage) для вилучення місць розташування CompletionStage \u003e LocationCS \u003d locationTarget.request () .rx \u200b\u200b() .get (new GenericType \u003e () ()); // Створивши окремий етап на етапі розташування, // описаному вище, зібрати список прогнозів, // як в одному великому CompletionStage final CompletionStage \u003e ForecastCS \u003d locationCS.thenCompose (locations -\u003e (// Створити етап для отримання прогнозів // як списку СompletionStage List \u003e ForecastList \u003d // Стрім розташування й обробка кожного // з них окремо locations.stream (). Map (location -\u003e (// Створити етап для отримання // значень температури тільки одного міста // по його назві final CompletionStage tempCS \u003d temperatureTarget .resolveTemplate ( "city", location.getName ()) .request () .rx \u200b\u200b() .get (Temperature.class); // Потім створити CompletableFuture, в якому // міститься інстанси прогнозу // з місцем розташування і температурним значенням return CompletableFuture.completedFuture (new Forecast (location)) .thenCombine (tempCS, Forecast :: setTemperature); )). Collect (Collectors.toList ()); // Повернути фінальний інстанси CompletableFuture, // де всі представлені об'єкти completable future // завершені return CompletableFuture.allOf (forecastList.toArray (new CompletableFuture)) .thenApply (v -\u003e forecastList.stream () .map (CompletionStage :: toCompletableFuture) .map (CompletableFuture :: join) .collect (Collectors.toList ())); )); // Створити інстанси ServiceResponse, // в якому міститься повний список прогнозів // разом з часом обробки. // Створити його future і об'єднати з // forecastCS, щоб отримати прогнози // і вставити у відповідь сервісу CompletableFuture.completedFuture (new ServiceResponse ()) .thenCombine (forecastCS, ServiceResponse :: forecasts) .whenCompleteAsync ((response, throwable) - \u003e (response.setProcessingTime (System.currentTimeMillis () - startTime); async.resume (response);)); ))
Реактивна реалізація може здатися складною, на перший погляд, але після більш уважного вивчення ви помітите, що вона досить проста. У реалізації ForecastReactiveResource я спочатку створюю клієнтський виклик на сервіси пунктів з допомогою JAX-RS Reactive Client API. Як я згадував вище, це додаток для Java EE 8, і воно допомагає створювати реактивний виклик просто за допомогою методу rx ().

Тепер я створюю новий етап на основі розташування, щоб зібрати список прогнозів. Вони будуть зберігатися, як список прогнозів, в одному великому completion stage, названому forecastCS. В кінцевому підсумку, я створю відповідь виклику сервісу, використовуючи тільки forecastCS.

А тепер, зберемо прогнози у вигляді списку completion stage'eй, визначених у змінній forecastList. Щоб створити completion stage для кожного прогнозу, я передаю дані по местоположениям, а потім створюю змінну tempCS, знову використовуючи JAX-RS Reactive Client API, який викликає сервіс температури з назвою міста. Тут для збірки клієнта я використовую метод resolveTemplate (), і це дозволяє мені передавати назву міста збирачеві як параметр.

В якості останнього кроку потокової передачі, я здійснюю виклик CompletableFuture.completedFuture (), передаючи новий інстанси Forecast як параметр. Я поєдную цей future з tempCS етапом, щоб у мене було значення температури для проітерірованних локацій.

Метод CompletableFuture.allOf () в Listing 11 перетворює список completion stage'ей в forecastCS. Виконання цього кроку повертає великий інстанси completable future, коли всі надані об'єкти completable future завершені.

Відповідь сервісу - інстанси класу ServiceResponse, тому я створюю completed future, а потім поєдную forecastCS completion stage зі списком прогнозів і обчислюють час відгуку сервісу.

Звичайно, реактивне програмування змушує тільки серверну сторону виконуватися асинхронно; клієнтська сторона буде заблокована доти, поки сервер не відправить відповідь назад запитувачу. Щоб подолати цю проблему, Server Sent Events (SSEs) може бути використаний для часткової відправки відповіді, як тільки він опиниться доступний, щоб температурні значення для кожної локації передавалися клієнту одне за іншим. Висновок ForecastReactiveResource буде схожий на той, що представлений в Listing 12. Як показано у висновку, час обробки складає 515 мс, що є ідеальним часом виконання для отримання температурних значень з однієї локації.

( "Forecasts": [( "location": ( "name": "London"), "temperature": ( "scale": "Celsius", "temperature": 49)), ( "location": ( "name ":" Istanbul ")," temperature ": (" scale ":" Celsius "," temperature ": 32)), (" location ": (" name ":" Prague ")," temperature ": (" scale ":" Celsius "," temperature ": 45))]," processingTime ": 515)
висновок

У прикладах цієї статті, я спочатку показав синхронний спосіб отримання прогнозів з допомогою сервісів розташування і температури. Потім, я перейшов до реактивного підходу для того, щоб асинхронна обробка виконувалася між викликами сервісу. Коли ви використовуєте JAX-RS Reactive Client API в Java EE 8 разом з класами CompletionStage і CompletableFuture, доступними в Java 8, сила асинхронної обробки виривається на волю, завдяки реактивному програмування.

Реактивний програмування - це більше, ніж просто реалізація асинхронної моделі з синхронної; воно також спрощує роботу з такими концепціями, як nesting stage. Чим більше воно використовується, тим простіше буде управляти складними сценаріями в паралельному програмуванні.

Дякуємо за увагу. Як завжди чекаємо ваші коментарі та питання.

Ви можете допомогти і перевести трохи коштів на розвиток сайту