jueves, 15 de diciembre de 2016

Angular 2. Entrega 13. Servicios

El servicio es otro elemento clave de Angular2. Veamos como nos justifica Micael Gallego el uso de servicios.

  • No es buena práctica meter en el componente la lógica de las peticiones http.
  • El componente podría llegar a ser muy complejo
  • Si el componente tiene muchas responsabilidades, los test se vuelven muy complejos.
  • Es mejor modularizar la aplicación a componentes con una única responsabilidad.
  • Es aconsejable desacoplar la aplicación.


Un servicio debe ser fácil de testear, y por tanto facimente manejable por el inyector de dependencias. También se les puede dar la propiedad de ser únicas o singleton (solo puede haber un único objeto de esa clase en toda la aplicación).

Los elementos que no tienen interfaz de usuario son servicios. Hay muchos servicios predefinidos como Http, servicios de login, que tiene acceso a la REST API.

Para que sea más sencillo implementar test. los servicios se inyectan en los componentes.


1. Implementación de un servicio


  • Se crea una clase para el servicio.
  • Se anota dicha clase con @Inyectable().
  • Se indica el servicio en la lista de providers del NgModule.
  • Se pone como parámetro en el constructor del componente que usa el servicio.

Veamos el código del servicio books.service.ts (observar el nombre del fichero que tiene "service")

import { Injectable } from '@angular/core';

@Injectable ()
export class BooksService {
  getBooks(title: string) {
    return [ 'Java One', 'Java Two','Java en 10 minutos'];
  }
}

Veamos ahora el componente ( app.component.ts) que hace uso del servicio

import { Component } from '@angular/core';
import {BooksService } from './books.service';

@Component ({
  selector: 'app-root',
  templateUrl: './app.component.html'
})

export class AppComponent {
  private books: string[] = [];
  
  constructor(private booksService : BooksService)()

  serach (title: string) {
    this.bools = this.bookService.getBooks(title);
  }
}

Por defecto, todos los servicios son singleton (se puede configurar para que no lo sean). Y por tanto se pueden utilizar para guardar el ESTADO de la aplicación (tada la información esta custodiada en uno o varios servicios con independencia de que tengamos múltiples componentes. Por ejemplo un servicio puede guardar el estado del login, otro que indica en que ventana estamos etc.

Veamos el NgModule, donde hay que definir el servicio en provider y en el import

import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { NgModule } from '@angular/core';
import { HttpModule, JsonModule } from '@angular/http';

import {AppComponent } from './app.component';
import {BooksService } from './books.service';

@NgModule ({
  declarations: [AppComponent],
  imports: [BrowserModule, FormsModule, HttpModule, JsonModule],
  bootstrap: [appcomponent],
  providers: [BooksService}
})
export class AppModule {}


2. Y si nuestro servicio no queremos que sea singleton ....

Podemos hacer que un servicio sea exclusivo para un componente y sus hijos, o sea, que NO sea singleton. O sea que si hay varias intancias de un componente o hijo, que haya varias intancias del servicio. Para ello se declara el servicio en el elemento providers del componente en vez del ngModel. Ahora queda disponible para este componene y sus hijos. Quedando nuestro componente

import { Component } from '@angular/core';
import {BooksService } from './books.service';

@Component ({
  selector: 'app-root',
  templateUrl: './app.component.html',
  providers: 'BookService'
})

export class AppComponent {
  private books: string[] = [];
  
  constructor(private booksService : BooksService)()

  serach (title: string) {
    this.bools = this.bookService.getBooks(title);
  }
}

3. Problemas con la ejecución asíncrona.

Http actua de forma asíncrona que significa que:

  • No puede devolver la información de forma inmediata.
  • Solo puede devolver información cuando llega la respuesta del servidor
  • En javaScript los métodos no se pueden bloquear esperando la respuesta.
  • Son asíncronos/reactivos.
Este código de llamada al servicio NO ES CORRECTO, ya que llama de forma sincrona (bloqueante) a un serviio asíncrono.

private service : BooksService = ...
let books = this.booksService.getBooks(title);
console.log(books);

Hay 3 formas de implementar un servicio con operaciones asíncronas en JavaScript:
  1. Callbacks: Se pasa como parámetro una función(de callback) que será ejecutada cuando llegue el resultado. Esta función recibe como resultado el error (si ha habido). No se recomienda.
  2. Promesas: El método devuelve un objeto Promise. Con el método then de este objeto, se define la función a ejecutar cuando llegue el resultado. Con el método catch se define la función a ejecutar si hay error. La promesa o se completa bien o se completa con error. Es la forma estandard de ES6.
  3. Observables: Tienen el método subscribe que contine las 2 funciones que serán ejecutadas (1) cuando llegue el resultado o (2) si se produce el error. Se implementa en RxJS (reactive extensions for Javascript) y es la recomendada en angular2. El resultado es un objeto de alto nivel

Veamos un ejemplo de Callback:

service.getBooks(title, (error, books)=> {
  if (error) {
    return console.error(error);
  }
  console.log(books);
});


Veamos un ejemplo de Promesa que parece ser mejor solución que el callback:

service.getBooks(title)
  .then  (books => console.log(books))
  .catch (error => console.error(error));
  
  
En el caso de Observables:

service.getBooks(title).subscribe(
   books => console.log(books),   // Función (1): si va todo bien
   error => console.error(error)  // Función (2): si hay error
);

4. El método map

El método map permite la transmisión de asincronia como si fuera un "pipe".
En este ejemplo, en vez de subscribe, utilizamos map, donde hacemos un return del objeto pero transformándolo para devolver solo los títulos, para ello utilizamos un lambda.

Veamos como queda el servicio books.service.ts . Para ello hacemos un import de 'rxjs/Rx' para utilizar observables (en concreto el método map), y utilizamos el método map (en 2 funciones) para trasformar el objeto Response a otro de alto nivel, pero hay que tener en cuenta que el map se debe utilizar solo en el servicio. y cuando ejecutemos este servicio en el componente, debemos utilizar

import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import 'rxjs/Rx'; // forma de Typescript para importar libreria. Ahora es reactive

@Injectable()
export class BooksService {
  constructor (private http: Http) { }
  getBooks (title: string) {
    let url = "https://www.googleapis.com/books/v1/volumens?q=intitle:" + title;
    return this.http.get(url).map {
      response=> this.extractTitles(response); //extreae títulos de la respuesta con un lambda
    }
  }
  private extractTitles(response: Response) {
     //volvemos a utilizar el map como si fuera un pipe para extraer solo los títulos.
     return response.json().items.map( book => book.volumeInfo.title);
  }
}


Para el compomente app.component.ts tenemos utilizando observables, que como ya tenemos el response transformado en alto nivel en el servicio gracias al efecto "pipe" del map, ahora, se puede utilizar el subscribe :

import { Componet } from '@angular/core';
import { BooksService} from './books.service';


@Component({
  selector; app-root',
  templateUrl: './app.compopnent.html'
});
export class AppComponent {
  private books: string[]=[];
  constructor (private booksService: BooksService) {} // desaparece el private http
  search(title: string) {
       this.books = [];
    this.booksService.getBooks(title).subscribe (
      books => this.books =books , // cuando llega la respuesta. acvtualiza el array de books
      error => console.error(error)   
    );
  }
  
}


5. Manipular el error en observables RxJS con map y catch

También podemos transformar el error a "alto nivel" en los observables RxJS (map), para ello, tras una petición REST (get) en este caso, se dispone de una estructura muy similar a las promesas, con (1) con .map y (2) con .catch .
Con catch gestionamos el error y podemos devolver un nuevo error o simular una resouesta correcta (con un valor por defecto).  En el caso de lanzar un nuevo error se hace utilizando el método de clase Observable.throw Veamos:

getBooks(title: string) {
  let url= ...
  return this.http.get(url)
    .map   (response => this.extractTitles(response))
    .catch (error    => Observable.throw('Server error')) // Lanzamos nuevo error
}


6. Estados en los servicios http

Los estados en los servicios http pueden ser:

1. Stateles (sin estado)

  • No guardan información
  • Sus métodos devuleven valores, pero no cambian el estado del servicio.
  • ejemplo BooksService con llamadas a Google.

2.Statefull (con estado)

  • Mantienen su estado, guardan su información
  • Al ejecutar sus métodos, cambian su estado interno y también pueden devolver valores.
  • ejemplo BooksService con información en memoria.

¿Cual utilizar?

  • Los stateless son más fáciles de implementar porque encapsulan las peticiones REST al backend.
  • Los stateless son menos eficientes ya que cada vez hay que volver a pedir la información al backend
  • Los statefull son mas difíciles ya que hay que definir una política de sincronización entre el frontend y backend.
  • Los statefull son mas eficientes, ya que solo consultan al backend cuando hace falta.

7. Otras consideraciones

Del ejemplo de Micael Gallego, aparecen en app.component.ts algunas cosas:

  • ngOnInit() { this.refresh;} // este método parece ser que es para inicializar el componente
  • this.ItemService.getItems().subscribe(result =>this.refresh()); // ignoramos el resultado del servicio.



No hay comentarios :

Publicar un comentario