mercoledì 4 gennaio 2017

Angular 2 & Reactive Programming

Rx Angular 2
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:
REST API
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:
libary-main
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


4 commenti: