Зменшення жирових каналів у Феніксі

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

Phoenix Channel

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

Проблема

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

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

Розбиття логіки на різні модулі

Створення конкретних модулів для кожного кроку є, мабуть, найбільш природним рішенням нашої проблеми, тому ми спробували це:

Досить просто. Ми просто переносимо логіку до виділених модулів і одночасно додаємо якийсь тривіальний код, який делегує з модуля каналу. Ну, є проблема. Це рішення не компілюється 🙈!

У нас є проблеми з функцією трансляції/3, яку не можна знайти в контексті модуля FirstStep. Те ж саме сталося б, якби ми використовували будь-яку з доступних функцій, наданих Phoenix.Channel, як push/3, reply/2 тощо.

Шукаємо відсутні функції

Усі ці функції для конкретного каналу доступні в нашому WizardChannel, оскільки у нас є такий рядок:

Ми не можемо просто зробити те ж саме у наших допоміжних модулях, таких як MyExampleAppWeb.WizardChannel.FirstStep, оскільки цей рядок робить більше, ніж імпортує купу функцій: він визначає процес, який буде виникати під час виконання і буде відповідальним за обробку всіх повідомлень рухаючись туди-сюди в нашому веб-з'єднанні.

Рішення досить просте. Ми можемо безпосередньо імпортувати необхідні функції, визначені в модулі Phoenix.Channel. Немає перешкод для доступу до цих функцій, і вони є частиною загальнодоступного API фреймворку (хоча простіше знайти приклади повного використання модуля Phoenix.Channel в офіційній документації)

Робочим рішенням для нашого FirstStep є наступне:

Народжується новий DSL

Давайте на мить подивимося, як виглядає наш модуль WizardChannel після додавання ще кількох кроків та функцій обробника:

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

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

Давайте подивимося, як цей модуль виглядав після реалізації цієї нової ідеї:

Багато повторень пішло! І ми раді бачити, що цей код краще повідомляє про намір (звичайно, передбачаючи певне знайомство з нашим користувацьким DSL).

Як це працює? Зверніть увагу, що ми додали до цього модуля наступне:

Давайте вивчимо, як реалізований макрос handle_step_messages, дивлячись на код модуля WizardChannel.MessagesHandler.

Макрос __using__ викликається, коли використовується директива use. Ми використовуємо цей спеціальний макрос лише для того, щоб переконатися, що всі функції та макроси цього модуля доступні в нашому хост-модулі (а в нашому випадку це MyExampleAppWeb.WizardChannel).

Макроси Elixir використовуються для програмного створення коду, який вводиться там, де макрос викликається під час компіляції. Зараз у нас є макрос, який генерує всі функції, які ми звикли писати від руки. Він отримує список атомів з іменами повідомлень та модулем, де реалізована власна логіка для кожного конкретного кроку майстра.

Тут ми використовуємо кілька конвенцій. Функції в різних модулях дорівнюють іменам повідомлень. Наприклад, тип повідомлення: send_info буде оброблятися функцією FirstStep.send_info/2. Крім того, ми припускаємо, що ці функції мають фіксовану суть, отримуючи тіло повідомлення та структуру Phoenix.Socket.

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

який покладається на Kernel.apply/3. Можна динамічно викликати правильну функцію із зазначеного модуля, оскільки імена повідомлень тепер передаються як змінні замість літералів.

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

Результат

Після впровадження цього DSL нам стало набагато краще з цією частиною коду. У нас з’явився гідний спосіб розділити функціональність каналу на різні модулі, а також чіткий спосіб написати весь клейовий код в модулі каналу, використовуючи наш макрос handle_step_messages/2.

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

Ви знайшли подібні проблеми з жирними каналами Фенікс? Як вам це вдалося? Будь ласка, коментуйте, якщо ви пробували різні рішення або якщо вам здається, що наша історія корисна.

Щасливого кодування з Elixir та Phoenix ‍👩🏽‍💻👨🏻‍💻!

Подяка

Величезна подяка Ніколасу Ферраро та Хав'єру Моралесу за допомогу в написанні цієї статті. Обидва вони брали участь у реалізації описаного рішення.