TSM - Aplicații deconectate ocazional

Florin Nicolae Cristea - Technical Lead @ Yonder


Trăim într-o eră a internetului, în care suntem conectați aproape în permanență, fie la job, acasă sau când ne plimbăm prin oraș. Cu toate acestea, mai sunt momente în care conexiunea cu online-ul ne poate crea probleme. Cât de des am văzut cu toții mesajul: "Fără conexiune la internet. Asigurați-vă că WI-FI sau datele mobile sunt pornite, apoi reîncercați."

Astfel de situații sunt evident neplăcute, dar ce putem face ca dezvoltatori software în cazul în care atât operatorul de date cât și rețeaua WI-FI nu sunt în favoarea noastră?

În principiu, avem întotdeauna de ales între a afișa un mesaj prietenos (n.r. aplicație web sau mobile) în care precizăm că această aplicație funcționează doar cu internet, și a dezvolta o aplicație care să ofere utilizatorului o funcționalitate limitată. Așadar, provocarea principală este cum să gândim arhitectura unei aplicații, astfel încât chiar și atunci când conectivitatea este scăzută sau inexistentă, situațiile neplăcute pentru utilizator să fie reduse la minim, iar consistența datelor să fie asigurată la revenirea în online. Astfel de aplicații mai sunt cunoscute și ca aplicații deconectate ocazional.

Să presupunem că avem o aplicație care facilitează realizarea a diverse servicii la domiciliul clientului pe baza unui sistem centralizat de cereri. Informațiile despre locația (adresa, client, etc.) la care trebuie realizat serviciul pot fi schimbate fie din sistemul central, fie de către alt operator de pe teren. Cum ar fi ca pentru perioade scurte de timp, în timpul intervenției la domiciliul clientului, operatorul companiei de servicii să nu poată beneficia de informațiile necesare din sistem, din cauza lipsei internetului? Dar dacă se constată că cererea nu mai este validă sau e necesară înregistrarea unui nou serviciu în sistem?

O abordare tradițională ar fi folosirea unei arhitecturi clasice de tip MVC (n.r. Model View Controller), prin care avem o interfață (View) în aplicație, care prin intermediul unui Controller va gestiona Modelul pentru realizarea operațiilor de citire/scriere (CRUD) solicitate de utilizator. De asemenea, Viewul va fi actualizat de către controller ori de câte ori apare o schimbare în Model. De altfel, arhitectura MVC este un șablon arhitectural foarte utilizat, în special pentru crearea aplicațiilor cu interfață grafică pentru utilizator, având acest avantaj major al separării părții vizuale de partea de business.

În cazul în care decidem să valorificăm o astfel de arhitectură pentru aplicațiile deconectate ocazional, având în vedere că partea de business trebuie să funcționeze și fără internet, se poate păstra același View, dar trebuie luate în calcul următoarele aspecte suplimentare, pe lângă cele clasice de MVC:

Modelul de date

Același model de citire/scriere a datelor care în general rulează pe server, va trebui să fie replicat și pe client (pentru modul offline). Așadar, vor exista multiple structuri de date, una principală pe server și mai multe instanțe pe fiecare client.

Confidențialitatea și securitatea datelor pe client

Se va impune ca datele sensibile care vor fi stocate în structurile de date (de exemplu: Local Storage, IndexDB), să fie criptate la stocare și decriptate la afișare.

Sincronizarea datelor

Chiar și atunci când există conexiune la internet, aplicația va trebui să decidă momentul oportun în care își va sincroniza datele pentru suportul offline. Sincronizarea o putem realiza automat la revenirea conexiunii, la logarea utilizatorului în aplicație, sau on request de către utilizator. Totuși, în toate cele trei variante menționate anterior, se recomandă blocarea Viewului până la terminarea procesului de sincronizare pentru evitarea unor eventuale conflicte.

Consistența datelor

Pentru asigurarea consistenței datelor între serverul central și diverși clienți deconectați, este necesară folosirea unor indicatori de timp pentru fiecare entitate în parte. Pe baza acestor indicatori care ar putea fi folosiți pentru operațiile de creare, editare sau ștergere, sistemul va decide ce schimbări să aplice pe o anumită entitate. În cazul detectării unei coliziuni pe aceeași entitate, există două opțiuni:

De cele mai multe ori, dacă se ține cont cu seriozitate de aspectele menționate mai sus, arhitectura MVC clasică este o arhitectură de succes și în cazul aplicațiilor deconectate ocazional. Cu toate acestea, un inconvenient major al acestui tip de arhitectură este adăugarea de complexitate și mentenanță suplimentară aplicațiilor pe care le dezvoltăm.

Cum ar fi însă dacă am încerca să privim această problemă dintr-o altă perspectivă? Complexitatea folosirii unei arhitecturi MVC clasice în această situație e dată de duplicarea modelului sau a structurii de date, sincronizarea entităților și asigurarea consistenței datelor. Prin urmare, dacă am reuși doar să separăm modelul de date în două modele independente de citire, respectiv scriere a datelor, situația unei aplicații fără conexiune la internet s-ar simplifica. Din fericire, există un șablon arhitectural în acest sens, care ne ajută să schimbăm puțin paradigma de separare a operațiilor de citire cu cele de scriere. Denumit CQRS (Command Query Responsibility Segregation), conceptul a fost prima dată introdus de Greg Young în Februarie 2010. Fiind un concept sau șablon arhitectural, se poate folosi în continuare o arhitectură clasică de tip MVC, dar de această dată îmbunătățită prin CQRS în modul de manipulare a datelor.

Prin aplicarea conceptului CQRS peste o arhitectură de tip MVC, practic vom folosi întotdeauna același model de date pentru citire, prin queries, respectiv un model diferit pentru scriere, prin comenzi, ceea ce ne va facilita sincronizarea și optimizarea separată a celor două modele în funcție de necesități.

Cu toate acestea, să vedem cum s-ar modifica aspectele anterioare care trebuiau luate în calcul la adaptarea unui model arhitectural MVC pentru aplicațiile deconectate ocazional:

Modelul de date

E important să menționăm că nu există neapărat constrângeri de modelare și că un model încărcat ar putea crea aceleași probleme ca și în abordarea cu un MVC clasic. Din acest punct de vedere, pentru a evita potențialele conflicte, noi am optat pentru un model atomic de comenzi, astfel încât, în esență, fiecare comandă să modifice maxim o proprietate dintr-o entitate.

Așadar, dacă pentru expunerea datelor către utilizator, ne putem folosi de simple servicii REST cu repository-uri pentru interogarea directă a bazei de date, pentru modificarea, crearea sau ștergerea datelor, vom folosi comenzi atomice. Prin folosirea unui hub de comenzi și printr-o versionare corectă a fiecărei comenzi, se vor putea sincroniza comenzi și nu entități, iar posibilele conflicte pot fi foarte bine gestionate, observate și tratate automat sau cu intervenția utilizatorului.

Ce se întâmplă însă cu expunerea datelor către utilizator în cazul în care acesta rămâne fără conexiune la internet? În această situație, ultimele informații citite de la server vor fi folosite de către aplicație cât este deconectată, iar partea interesantă este că exact același set de comenzi va fi generat, atât online, cât și offline. Acest lucru înseamnă că la revenirea conexiunii de internet, clientul va prelua rezultatele comenzilor executate de către alți clienți sau de către un operator central și le va aplica pe setul personal de date.

Confidențialitatea și securitatea datelor pe client

Am ales ca atât entitățile aplicației, cât și înlănțuirea de comenzi să fie stocate în structuri de date din memoria internă a aplicației. Pe de altă parte, va fi necesară stocarea informațiilor într-o bază de date locală, în cazul în care e necesară și logarea în aplicație fără conexiune la internet. În această situație, este obligatoriu ca datele să fie criptate la stocare și decriptate la afișare.

Sincronizarea datelor

Indiferent de conexiunea la internet, am ales să folosim câte un eveniment pentru fiecare comandă executată. Conceptul CQRS se pretează foarte bine la folosirea de evenimente asincrone, care ne ajută să confirmăm dacă o comandă a fost aplicată cu succes sau nu. De exemplu, în cazul unui eveniment pozitiv, operația de modificare inițiată de către utilizator va fi păstrată în aplicație, dar în cazul unui eveniment negativ, utilizatorul va putea să decidă rezoluția unui posibil conflict.

Putem face sincronizarea și propagarea de evenimente, ca rezultat al unor comenzi, prin metode ca long-polling, server-sent-events sau chiar conexiuni de tip websocket. Avantajul folosirii unei conexiuni de tip websocket este comunicarea în timp real între aplicația client și server, având întotdeauna un canal de comunicare deschis care interceptează evenimentele generate de server. De asemenea, în cazul în care conexiunea websocket este închisă, serverul va putea determina cu exactitate momentul din care utilizatorul de pe teren a rămas fără conexiune la internet, respectiv locația acestuia, dacă folosește un dispozitiv mobil.

Consistența datelor

Prin sistemul central de comenzi și notificări, conflictele vor fi rezolvate fie automat de către aplicație, fără intervenția utilizatorului, fie prin notificarea utilizatorului în vederea luării unei decizii. Consistența datelor poate fi realizată prin folosirea unei versiuni la nivel de proprietate din root-aggregate. Indicatorii de timp nu mai sunt necesari, în schimb este esențială trimiterea în ordine cronologică a comenzilor și implicit a evenimentelor.

Soluționarea conflictelor continuă să reprezinte un aspect critic și în această abordare. Totuși există avantajul că acestea pot fi rezolvate consistent și indiferent de modul de lucru: cu sau fără conexiune la internet. Folosirea unui hub de comenzi ne permite de altfel și gruparea/executarea comenzilor în grupuri tranzacționale acolo unde este cazul.

Concluzie

Chiar dacă uneori ne este greu să ne decidem asupra unui model arhitectural atunci când dorim dezvoltarea unei aplicații deconectate ocazional, un aspect important e identificarea problemelor care trebuie rezolvate, apoi scalabilitatea și mentenanța soluției alese. În cazul CQRS, schimbarea paradigmei unui CRUD clasic cu izolarea operațiilor de citire/scriere și menținerea unui singur model, poate fi benefică pe termen mediu/lung.