Il mio primo approccio con la
Programmazione Reattiva (o meglio
FRP -
Functional
Reactive
Programming) risale ormai a metà del 2013 quando, dopo aver letto una serie di articoli che descrivevano il paradigma di programmazione, ho deciso di introdurre un'implementazione di tale tecnica per .NET in una nuova release di un sistema
MES (
Manufacturing
Execution
System) che ho progettato ed implementato per l’azienda per cui lavoravo all’epoca. La libreria in questione era denominata
Rx - Reactive Extensions.
In parole povere, le diverse implementazioni di
Rx, consentono di manipolare flussi di dati e/o eventi (
Observable) al fine di semplificare notevolmente lo sviluppo di codice asincrono mediante una
sottoscrizione,
Subscription, alle variazioni di tali flussi.
Tra i vari benefici che si possono avere con l’adozione di tali tecniche, quella che, a mio parere, è quella di maggior pregio, è data dal fatto che una volta creato uno stream di dati, diversi client possono effettuare una sottoscrizione alle variazioni dello stesso ottenendo, a costo zero, un sistema in grado di comunicare in modo
broadcast con diversi client.
Le diverse implementazioni del paradigma, comprendono anche una libreria JavaScript denominata
RxJS, e viene installata tra i diversi pacchetti a corredo di
Angular 2.
Una delle applicazioni che meglio si prestano all’utilizzo della programmazione reattiva in un’applicazione Angular 2 è data dalla fruizione di un servizio REST.
Nei post precedenti abbiamo creato sia un’applicazione client basata su Angular 2 che una Web API sviluppata con ASP.NET Core, ed entrambe hanno trovato posto su
Azure.
Riprendendo il post precedente
RESTful API con Swagger, e concentrandoci sul dettaglio della lista dei metodi esposti dal servizio, abbiamo:
Con questi metodi, possiamo pensare alla realizzazione di un’applicazione CRUD finalizzata alla gestione di una piccola biblioteca personale.
I sorgenti dell’intero progetto sono disponibili per la consultazione su
github, ed il risultato finale, è una Web Application di azure raggiungibile all’indirizzo
http://talking-things.azurewebsites.net/#/library.
Il modulo principale è dato dal componente
library che si presenta come segue:
ed la cui implementazione è data da:
import { Component, OnInit, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs/Rx';
import { Router } from '@angular/router';
import { environment } from '../../environments/environment';
import { BookStoreService } from '../bookstore.service';
import { Book } from '../models/book';
import { ModalComponent } from '../modal/modal.component';
@Component({
selector: 'app-library',
templateUrl: './library.component.html',
styleUrls: ['./library.component.css'],
providers: [BookStoreService]
})
export class LibraryComponent implements OnInit {
title = 'Book Store';
books: Book[] = [];
errorMessage: string = '';
isLoading: boolean = true;
@ViewChild(ModalComponent) modal: ModalComponent;
private subscription: Subscription;
constructor(private _router: Router, private _bookStoreService: BookStoreService) { }
ngOnInit() {
this.reloadData();
}
reloadData() {
this._bookStoreService
.GetAll()
.subscribe(
b => this.books = b,
e => this.errorMessage = e,
() => this.isLoading = false);
}
onNew() {
console.log("onNew");
this._router.navigate(['/edit', 'new']);
}
onEdit(book: Book) {
console.log('edit ' + book.id);
this._router.navigate(['/edit', book.id]);
}
onDelete(book) {
var message: string = 'Delete \'' + book.title + '\'?: ';
this.modal.Title = 'Warning';
this.modal.show(message);
this.subscription = this.modal.observable.subscribe(x => {
if (x) {
this._bookStoreService.Delete(book.id).subscribe(
book => {
let b = this.books.find(item => item.id === book.id);
let id = this.books.indexOf(b);
this.books.splice(id, 1);
if (!environment.production)
console.log(JSON.stringify(book));
},
error => {
console.log(error);
}
);
}
this.subscription.unsubscribe();
});
}
}
e dal servizio
BookStoreService, cuore del vero interfacciamento con il servizio REST:
import { Injectable } from '@angular/core';
import { Http, Response, Headers } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import { Book } from './models/book';
import { environment } from '../environments/environment';
@Injectable()
export class BookStoreService {
private baseUrl: string;
constructor(private _http: Http) {
this.baseUrl = environment.bookStoreApi.server + environment.bookStoreApi.apiUrl + '/books/';
}
GetAll(): Observable<Book[]> {
if (!environment.production)
console.log(this.baseUrl);
let books$ = this._http.get(this.baseUrl, { headers: this.GetHeaders() })
.map(mapBooks)
.catch(handleError);
return books$;
}
public GetById = (id: string): Observable<Book> => {
let books$ = this._http.get(this.baseUrl + id, { headers: this.GetHeaders() })
.map(response => response.json())
.catch(handleError);
return books$;
}
public Create = (book: Book): Observable<any> => {
let book$ = this._http.post(this.baseUrl, book, { headers: this.GetHeaders() })
.catch(handleError);
return book$;
}
public Update = (id: string, book: Book): Observable<any> => {
let book$ = this._http.put(this.baseUrl + id, book, { headers: this.GetHeaders() })
.catch(handleError);
return book$;
}
public Delete = (id: string): Observable<Book> => {
let book$ = this._http.delete(this.baseUrl + id)
.catch(handleError);
return book$;
}
private GetHeaders() {
let headers = new Headers();
headers.append('Accept', 'application/json');
return headers;
}
}
function mapBooks(response: Response): Book[] {
return response.json().map(toBook);
}
function toBook(r: any): Book {
if (!environment.production)
console.log('toBook: ' + JSON.stringify(r));
let book = <Book>({
id: r.id,
title: r.title,
authors: r.authors,
publicationYear: r.publicationYear,
isAvailable: r.isAvailable
});
if (!environment.production)
console.log('Parsed book: ', book);
return book;
}
function handleError(error: Response) {
return Observable.throw(error || 'Server error');
}
Per avere un’idea di come l’utilizzo della
FRP abbia reso più agevole la gestione della programmazione asincrona focalizziamo la nostra attenzione sul metodo
GetAll() che ritorna uno stream
Observable
GetAll(): Observable<Book[]> {
if (!environment.production)
console.log(this.baseUrl);
let books$ = this._http.get(this.baseUrl, { headers: this.GetHeaders() })
.map(mapBooks)
.catch(handleError);
return books$;
}
ed al quale il componente
library effettua una sottoscrizione mediante il metodo
subscribe:
this._bookStoreService
.GetAll()
.subscribe(
b => this.books = b,
e => this.errorMessage = e,
() => this.isLoading = false);
In modo analogo vengono implementate le interfacce verso gli altri metodi esposti dal servizio REST.
Facile e pulito…
Enjoy