În acest articol, vom discuta despre o abordare pe care am aplicat-o într-un proiect Phoenix. Canalele au fost o parte importantă a soluției, dat fiind că aplicația include funcții de colaborare în timp real.

grăsime

Aveam un modul de canal specific, cu un număr tot mai mare de funcții de gestionare a mesajelor. În ceea ce privește designul, am crezut că a avea toate interacțiunile gestionate de un singur modul de canal încă avea sens. Cu toate acestea, am început să observăm că modulul în sine a scăpat de sub control, cu o mulțime de logici fără legătură și un număr mare de linii de cod. Din acest motiv, am început să ne gândim cum să împărțim codul în diferite module, dar într-un mod care funcționează încă folosind un singur canal Phoenix.

Problema

Să presupunem că vrem să implementăm o interfață de asistent în mai mulți pași care să aibă un fel de interacțiune colaborativă în timpul fiecărui pas. Înseamnă că toată lumea din grup va lucra la același pas, la un moment dat. Iată cum am implementat inițial acest vrăjitor.

După cum probabil ați observat deja, această abordare nu se amplifică bine. Pe măsură ce se adaugă mai multe acțiuni în cadrul pașilor și mai mulți pași în sine, modulul de canal devine din ce în ce mai complex.

Împărțirea logicii în diferite module

Crearea de module specifice pentru fiecare pas este probabil cea mai naturală soluție la problema noastră, așa că am încercat-o:

Destul de simplu. Doar mutăm logica către module dedicate și, în același timp, adăugăm un cod trivial care delegă de la modulul de canal. Ei bine, există o problemă. Această soluție nu compilează 🙈!

Avem probleme cu funcția broadcast/3 care nu se găsește în contextul modulului FirstStep. Același lucru s-ar întâmpla dacă am folosi oricare dintre funcțiile disponibile furnizate de Phoenix.Channel cum ar fi push/3, reply/2 și așa mai departe.

În căutarea funcțiilor lipsă

Toate acele funcții specifice canalului sunt disponibile în WizardChannel, deoarece avem această linie acolo:

Nu putem face pur și simplu același lucru în modulele noastre auxiliare, cum ar fi MyExampleAppWeb.WizardChannel.FirstStep, deoarece această linie face mai mult decât să importe o grămadă de funcții: definește un proces care va fi generat în timpul rulării și va fi responsabil pentru gestionarea tuturor mesajelor mergând înainte și înapoi în conexiunea noastră web.

Soluția este destul de ușoară. Putem importa direct funcțiile necesare definite în modulul Phoenix.Channel. Nu există nici un impediment pentru accesul la aceste funcții și fac parte din API-ul public al cadrului (deși este mai ușor să găsiți exemple de utilizare completă a modulului Phoenix.Channel în documentația oficială)

O soluție de lucru pentru FirstStep este următoarea:

Se naște un nou DSL

Să căutăm o clipă cum arată modulul nostru WizardChannel după ce am adăugat încă câțiva pași și funcții de gestionare:

Am observat că codul a fost în mare parte repetitiv, neavând multă logică în afară de definirea modului și funcției corespunzătoare. Mai mult, grupam funcțiile și adăugam acele linii de comentarii care se referă la diferiți pași, astfel încât fișierul să fie mai ușor de navigat.

Aici am început să vedem șansa de a implementa un DSL personalizat, în încercarea de a avea o bucată de cod mai lizibilă și mai ușor de întreținut. Elixir și capacitățile sale de metaprogramare fac posibilă crearea DSL-urilor cu câteva linii de cod (deși înțelegerea codului ar necesita învățarea modului în care funcționează macro-urile în Elixir).

Să vedem cum a arătat acest modul după implementarea acestei noi idei:

O mulțime de repetări au dispărut! Și suntem bucuroși să vedem că acest cod comunică mai bine intenția (presupunând o anumită familiaritate cu DSL-ul nostru personalizat, desigur).

Cum funcționează? Rețineți că am adăugat următoarele la acest modul:

Să explorăm modul în care este implementată macrocomanda handle_step_messages, uitându-ne la codul modulului WizardChannel.MessagesHandler.

Macrocomanda __using__ este invocată când se utilizează directiva de utilizare. Folosim acea macro specială doar pentru a ne asigura că toate funcțiile și macro-urile din acest modul sunt disponibile în modulul nostru gazdă (care este MyExampleAppWeb.WizardChannel în cazul nostru).

Macrocomenzile Elixir sunt utilizate pentru a produce cod programat care este injectat acolo unde este invocată macrocomanda la momentul compilării. Avem acum un macro care generează toate funcțiile pe care le scrieam manual. Acesta primește o listă de atomi cu numele mesajelor și modulul în care este implementată logica personalizată pentru fiecare pas de asistent specific.

Facem uz de câteva convenții aici. Funcțiile din diferite module sunt egale cu numele mesajelor. De exemplu, tipul de mesaj: send_info va fi gestionat de funcția FirstStep.send_info/2. De asemenea, presupunem că acele funcții au o aritate fixă, primind corpul mesajului și structura Phoenix.Socket.

Așadar, parcurgem lista de tipuri de mesaje și generăm o definiție a funcției pentru fiecare. Corpul fiecărei funcții generate este următorul

care se bazează pe Kernel.apply/3. Este posibil să invocați dinamic funcția corectă din modulul specificat, deoarece numele mesajelor sunt transmise acum ca variabile în loc de litere.

Nu este intenția acestui articol să explice pe deplin aspectul metaprogramării acestei soluții. Dacă lucruri precum ghilimelele și necitările încă vă încurcă, vă recomand cu tărie să citiți documentația aferentă din ghidurile Elixir.

Rezultatul

După implementarea acestui DSL, ne-am simțit mult mai bine cu această parte a codului. Am ajuns la o modalitate decentă de a împărți funcționalitatea canalului în diferite module și, de asemenea, o modalitate clară de a scrie tot codul de lipici în modulul canalului folosind macro-ul nostru handle_step_messages/2.

Această abordare deschide noi oportunități și provocări. De exemplu, am dezvoltat soluția prezentată pentru a sprijini funcții cu aritate diferită (unele funcții nu foloseau parametrul Phoenix.Socket struct). În plus, explorăm acum câteva abordări pentru a rula validări partajate în funcție de modulul țintă. Oricum, luarea prea departe a metaprogramării ar putea duce la o soluție supra-concepută, greu de înțeles pentru alți dezvoltatori care se ocupă mai târziu de cod, deci este clar că trebuie să avem un echilibru între concizie și simplitate a codului.

Ați găsit probleme similare cu canalele Phoenix groase? Cum ai reușit? Vă rugăm să comentați dacă ați încercat diferite soluții sau dacă vi se pare utilă povestea noastră.

Codificare fericită cu Elixir și Phoenix ‍👩🏽‍💻👨🏻‍💻!

Mulțumiri

Mulțumiri imense lui Nicolás Ferraro și Javier Morales pentru că au contribuit la scrierea acestui articol. Amândoi au fost implicați în implementarea soluției descrise.