В тази статия ще обсъдим подход, който приложихме в проект на Финикс. Каналите бяха важна част от решението, тъй като приложението включва функции за сътрудничество в реално време.

канали

Имахме специфичен модул за канал с нарастващ брой функции за обработка на съобщения. По отношение на дизайна, ние смятахме, че да имаме всички взаимодействия, управлявани само от един канален модул, все още има смисъл. Започнахме обаче да забелязваме, че самият модул излиза извън контрол, с много несвързана логика и голям брой редове код. Поради тази причина започнахме да мислим как да разделим кода на различни модули, но по начин, който все още работи, използвайки само един канал на Феникс.

Проблемът

Да предположим, че искаме да внедрим многоетапен интерфейс на съветника, имащ някакво съвместно взаимодействие по време на всяка стъпка. Това означава, че всички в групата ще работят на една и съща стъпка, в даден момент. Ето как първоначално внедрихме този съветник.

Както може би вече сте забелязали, този подход не се мащабира добре. С добавянето на повече действия в рамките на стъпки и повече стъпки към съветника, модулът на канала става все по-сложен.

Разделяне на логиката в различни модули

Създаването на специфични модули за всяка стъпка е може би най-естественото решение на нашия проблем, затова го опитахме:

Достатъчно просто. Ние просто преместваме логиката към специални модули и в същото време добавяме някакъв тривиален код, който делегира от модула на канала. Е, има проблем. Това решение не се компилира 🙈!

Имаме проблеми с функцията излъчване/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.

И така, ние преглеждаме списъка с типове съобщения и генерираме дефиниция на функция за всеки един. Тялото на всяка генерирана функция е следното

който разчита на Kernel.apply/3. Възможно е динамично да извикате правилната функция от посочения модул, тъй като имената на съобщенията се предават сега като променливи вместо литерали.

Целта на тази статия не е да обясни изцяло аспекта на метапрограмирането на това решение. Ако неща като цитати и кавички все още ви озадачават, горещо препоръчвам да прочетете съответната документация в ръководствата за Elixir.

Резултатът

След внедряването на този DSL се почувствахме много по-добре с тази част от кода. В крайна сметка получихме приличен начин да разделим функционалността на канала в различни модули, а също и ясен начин да напишем целия лепилен код в модула на канала, използвайки нашия макрос handle_step_messages/2.

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

Открили ли сте подобни проблеми с дебелите канали на Финикс? Как се справихте? Моля, коментирайте, ако сте опитвали различни решения или ако смятате, че нашата история е полезна.

Щастливо кодиране с Elixir и Phoenix ‍👩🏽‍💻👨🏻‍💻!

Благодарности

Огромни благодарности на Николас Фераро и Хавиер Моралес за помощта при написването на тази статия. И двамата участваха в изпълнението на описаното решение.