Cu toții am învățat în școală modul de programare imperativ, care are avantajul de a fi simplu de explicat și implementat. Aceasta paradigmă de programare ne permite să scriem un cod în care descriem, pas cu pas, ce anume vrem să facem, iar rezultatul va fi întocmai ce voiam să se întâmple.
Să încercăm să exemplificăm acest lucru folosind o situație reală: un magazin online, unde, prin intermediul browserului, orice utilizator poate intra pentru a se uita la produse și să-și comande unul dintre produsele agreate.
Ca administrator al acestui magazin online, am vrea să creăm un algoritm care să ne returneze un top al celor mai "înstăriți" clienți: spre exemplu, dorim să aflăm câți clienți au în coșul lor produse care însumează cel puțin 1000 Ron.
În cod, clientul nostru va fi asociat cu clasa Customer. Avem nevoie de doar două proprietăți, numele acestuia (Name) precum și totalul coșului de cumpărături, exprimat prin proprietatea OrderAmount. Pentru simplitate, vom folosi lei românești (RON), atunci când ne vom raporta la totalul coșului. Exemplele ce vor urma sunt scrise în C#, însă subiectul articolului nu este limitat doar la acest limbaj.
public class Customer
{
public string Name { get; set; }
public int
OrderAmount { get; set; }
public Customer
(string name, int orderAmount)
{
Name = name;
OrderAmount = orderAmount;
}
}
Pornim de la premisa că la un moment dat, în spațiul nostru virtual privat se află cinci clienți, fiecare având un nume precum și un coș de o anumită valoare:
public class Customer
{
public string Name { get; set; }
public int
OrderAmount { get; set; }
public Customer
(string name, int orderAmount)
{
Name = name;
OrderAmount = orderAmount;
}
}
Algoritmul nostru imperativ, prin care dorim să aflăm care din cei cinci clienți a cheltuit cel puțin 1000 Ron ar putea arăta în felul următor:
public class Customer
{
public string Name { get; set; }
public int
OrderAmount { get; set; }
public Customer
(string name, int orderAmount)
{
Name = name;
OrderAmount = orderAmount;
}
}
În ton cu definiția stilului de programare imperativă amintită la începutul articolului, am descris pe fiecare linie, pas cu pas, ceea ce am dorit să se întâmple. Mai întâi am creat o listă nouă topSpendersList, apoi am parcurs lista customers element cu element, interesându-ne doar numele persoanei al cărei coș de cumpărături depășește sau este egal cu 1000 Ron. La finalul execuției acestui algoritm, vom observa ca în lista topSpendersList avem trei membri ("Felicia", "Robert" și "Fred"), pentru că fiecare din cei trei a cheltuit cel puțin 1000 Ron.
Totul a fost bine până acum, pentru că am aflat ceea ce ne interesa, dar să vedem cum am putea rescrie acest algoritm folosind modul de programare declarativ.
Programarea declarativă, în contrast cu cea imperativă, este un stil de programare în care se descrie logica unui calcul, însă fără să se prezinte modul de execuție. Cei familiarizați cu .Net (C#, VB) deja folosesc acest mod de programare, atunci când scriu cod folosind LINQ:
var topSpendersList = customers
.Where(customer => customer.OrderAmount >= 1000)
.Select(customer => customer.Name)
.ToList();
În liniile de cod de mai sus este exprimat logica unui calcul (ia din lista customers doar clienții care au valoarea coșului de cumpărături cel puțin egală cu 1000 Ron, apoi folosind numele acestora creează o nouă listă), fără a fi necesar să scriem pas cu pas ce trebuie făcut. Codul sursă al funcțiilor Where(), Select() si ToList() se află în librăria .NET Framework.
Rezultatul execuției acestui algoritm va fi identic cu cel anterior, deci lista topSpendersList va conține aceiași trei membri ("Felicia", "Robert" și "Fred").
Ce înseamnă a fi reactiv? Din DEX aflăm că a fi reactiv înseamnă a răspunde la un stimul.
Să presupunem că de fiecare dată când apare un client nou pe site, vrem ca lista topSpendersList să își facă update cu numele noului client, dacă respectă condiția impusă de noi.
Exemplele scrise anterior nu vor mai funcționa, pentru ca ele depind de o listă de clienți fixă, ori noi acum avem un flux de clienți.
Și aici intervin Extensiile Reactive, ce reprezintă o librărie gratuită cu ajutorul căreia putem rescrie algoritmul nostru, astfel încât să țină cont de faptul ca trebuie sa "reacționeze" de fiecare dată când avem un client nou.
ObservableCollection customersList =
new ObservableCollection();
var topSpendersList = new List();
Observable
.FromEventPattern(customersList,"CollectionChanged")
.SelectMany(handler =>(handler.EventArgs
as NotifyCollectionChangedEventArgs)
.NewItems.Cast())
.Where(customer => customer.OrderAmount >= 1000)
.Subscribe(customer =>
{
topSpendersList.Add(customer.Name);
});
Deși sunt mai multe linii de cod scrise decât în exemplele anterioare, vom afla că sunt ușor de înțeles. De aceea, acest pattern îl vom folosi des pe viitor.
Să le luăm pe rând:
Dacă executăm noul algoritm, fără a avea nici un element în customersList, vom vedea că nu se întâmplă nimic. Pe măsură ce adăugăm elemente noi, algoritmul va fi parcurs și în urma execuției lui, topSpendersList va fi populat.
customersList.Add(new Customer("John", 200));
customersList.Add(new Customer("Alice", 150));
customersList.Add(new Customer("Felicia", 1250));
customersList.Add(new Customer("Robert", 2100));
customersList.Add(new Customer("Fred", 1850));
După ce adăugăm cei cinci clienți inițiali, în topSpendersList vom regăsi numele ultimilor trei clienți.
Însă acum customersList nu mai este o listă fixă, deci după ce îi vom adăuga si pe următorii cinci, algoritmul nostru va "reacționa" la această acțiune și va mai adăuga încă trei persoane (Phil, Anastacia și Aurora) în topSpendersList:
customers.Add(new Customer("Phil", 1100));
customers.Add(new Customer("Beatrice", 660));
customers.Add(new Customer("Anastacia", 3550));
customers.Add(new Customer("Aurora", 20000));
customers.Add(new Customer("Mark", 165));
Putem spune deci că programarea reactivă este un model (paradigmă) de programare declarativă care se concentrează pe fluxurile de date și propagarea schimbării.
Sintaxa nouă, ce folosește cuvintele cheie Observable și Subscribe, va fi accesibilă, după ce adăugăm pachetul NuGet numit System.Reactive
Ca să înțelegem mai bine exemplul de cod reactiv, vom încerca o minină detaliere a celor două interfețe, a căror definiție se află in .NET Framework încă de la versiunea 4.0. Implementarea lor, însă, nu este în framework, ci o regăsim în librăria System.Reactive
public interface IObserver
{
void OnNext(T value);
void OnError(Exception error);
void OnCompleted();
}
public interface IObservable
{
IDisposable Subscribe(IObserver observer);
}
Cel mai ușor de explicat este atunci când facem o analogie cu o situație din viața reală, precum am făcut cu magazinul online, așa că vom face și aici la fel. Să privim IObservable ca o bandă rulantă, care primește produse într-un capăt și le transportă până în celălalt capăt. Produsele pot fi de diferite mărimi și culori și pot ajunge pe bandă la diferite intervale de timp între ele. Dacă banda rulantă merge și nu este nimeni la capătul celălalt, ele vor cădea și vor fi pierdute.
Ca să nu se întâmple acest lucru, avem nevoie de un operator uman care să preia acele pachete. Cu alte cuvinte, el se "abonează" (Subscribe) la fluxul de pachete de pe banda rulantă. Operatorul (sau observer-ul) implementează interfața IObserver, ce conține trei metode: OnNext, OnError și OnCompleted.
Observable-ul (banda rulantă), din momentul în care are un subscriber, va ști să apeleze aceste metode astfel:
Observăm că este o legătură strânsă între IObservable si IObserver. Mai mult de atât, atunci când apelăm Subscribe, IObservable ne va returna un IDisposable, lucru care este foarte util, deoarece atunci când nu mai avem nevoie de această abonare (subscription), pur și simplu îi dăm Dispose(). Librăria System.Reactive vine cu o altă clasă ajutătoare, numită CompositeDisposable, în care putem adăuga fiecare abonare în parte folosind metoda Add(), iar la final, apelând Clear() pe instanța CompositeDisposable, eliminăm tot ce ține de abonări, pentru un management al memoriei cât mai bun.
Dacă ne întoarcem acum la algoritmul cu programarea reactivă, ne vom da seama că are mai mult sens. Pentru simplitate, în corpul metodei Subscribe am scris doar o expresie lambda care gestionează doar cazurile în care avem clienți noi (OnNext), nu și cazul în care apare o eroare (OnError). De asemenea, nici nu este gestionat situația în care Observable-ul apelează OnCompleted atunci când nu mai sunt elemente noi.
Extensiile reactive sunt disponibile și pentru alte limbaje de programare, nu numai C#:
Site-ul oficial este http://reactivex.io.
În rândurile de mai sus, am realizat o scurtă introducere în programarea reactivă. Extensiile Reactive ne pot ajuta să scriem algoritmi ușor de înțeles, scalabili, oferindu-ne o mână de ajutor în ceea ce privește managementul memoriei (prin intermediul CompositeDisposable). Nu în ultimul rând ne pot fi de un real ajutor referitor la programarea asincronă. Exemplele prezentate reprezintă baza acestui mod de a gândi reactiv.
Extensiile reactive ne mai pun la îndemână o serie de operatori foarte utili, a căror definiție o voi da pe scurt: