Services y dependency injection: Guía completa – Parte 1

En esta serie de posts, con un ejercicio bonus al final de la parte #3, vamos a tratar los servicios y la inyección de dependencias en profundidad. Si acabas de aterrizar aquí y estos temas de Angular te suenan a chino, puedes echar un vistazo a este artículo antes, donde te doy las claves para entenderlo y empezar a usarlo.

servicios = services

inyección de dependencias = dependecy injection

Qué son los services y para qué sirven

Los services sirven para evitarnos repetir código y para centralizar tareas comunes. Una tarea común es, por ejemplo, el gestionar adecuadamente el almacenamiento de datos (en inglés, data storage).

Supongamos que tenemos una app con varios componentes, en dos de esos componentes tenemos un método que sirve para imprimir ciertos datos en la consola, mientras que en otros componente tenemos un método que sirve para guardar datos que el usuario inserte.

Un service es sencillamente otra parte de tu app, que construimos a través de una clase normal de TypeScript (en adelante, TS), 

y que actúa como un repositorio central, un almacén donde podemos centralizar nuestro código.
introducción a los services

Estado inicial de nuestra app

Para entender bien los services, vamos a construir una pequeña demo-app con un par de componentes que se comunican entre sí sin usar services, para reflejar lo complejo que puede volverse todo en una app relativamente pequeña. Posteriormente la refactorizaremos usando services. 

Este es su aspecto final, para que nos hagamos una idea:

app demo services aspecto inicial

Nuestra app está inspirada en el esquema de arriba, desarrollado. Será una app que mostrará tres "cuentas de usuario" por defecto, y un campo para añadir más cuentas. Podremos cambiar el estado (en inglés, status) de las cuentas, alternando entre "activo", "inactivo" y "desconocido". Aquí es donde entrará en juego un código para imprimir por consola el status al que haya cambiado el usuario. 

También imprimiremos por consola el status de cada nuevo usuario que registremos, a la par que quedará registrado en nuestro código. Es decir, como se muestra en el esquema, usaremos un método para imprimir por consola un estado y otro para almacenar datos de registro de usuarios. Este es un caso típico donde podríamos centralizar dichos métodos y convertirlos en services. ¡Vamos a ello! 💪

1. Creamos una app con Angular CLI y le añadimos Bootstrap para tener una UI decente a la velocidad del rayo.

2. Creamos dos componentes dentro de la carpeta app: account new-accountNuestro appComponent será el componente que abarcará los dos anteriores (el parent component).

3. Creamos el esqueleto inicial del app.component.html.

<div class="container my-5">
  <div class="row">
    <div class="col-md-8">
      <app-new-account></app-new-account>
      <hr>
      <app-account></app-account>
    </div>
  </div>
</div>

4. Creamos tres cuentas hardcoded en el app.component.ts y un método para añadir una cuenta nueva a un array de cuentas que posteriormente utilizaremos.

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  accounts = [
    {
      name: 'Master Account',
      status: 'active'
    },
    {
      name: 'Testaccount',
      status: 'inactive'
    },
    {
      name: 'Hidden Account',
      status: 'unknown'
    }
  ];

  onAccountAdded(newAccount: { name: string, status: string }) {
    this.accounts.push(newAccount);
  }
}

5. Iteramos (a través de un loop) todas las cuentas en el app.component.html y añadimos un custom event que luego configuraremos.

<app-account *ngFor="let acc of accounts; let i = index"   
             (statusChanged)="onStatusChanged($event)">
</app-account>

Ese custom event se disparará cuando hagamos clic sobre alguno de estos botones para cambiar el estado:

botones services demo app

Así que ya que estamos, vamos a crearlos (en el account.component.html), donde también añadimos un click event vinculado a un método que capturará ese cambio de estado.

<div class="row border my-4 py-4">
  <div class="col">
    <button class="btn btn-outline-secondary" 
            (click)="onSetTo('active')">Set to 'active'</button>
    <button class="btn btn-outline-secondary mx-2"
            (click)="onSetTo('inactive')">Set to 'inactive'</button>
    <button class="btn btn-outline-secondary" 
            (click)="onSetTo('unknown')">Set to 'unknown'</button>
  </div>
</div>

6. Configuramos nuestro custom event (statusChangeden el account.component.ts, que emitimos a través del método onSetTo.

Recuerda que en el appComponent extraemos la posición de cada account del array "accounts" usando el index, actuando así como un ID al que se le asignará un número empezando desde 0. Ese ID quedará asignado desde el appComponent, por tanto, podemos crear una propiedad de TS en el account.component.ts, llamada id, que reciba ese dato desde ahí. Para avisar a Angular de esto, decoramos la propiedad con el @Input.

Creamos también otra propiedad llamada account que recibirá su valor desde su parent component, por tanto, la decoramos con el @Input.

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-account',
  templateUrl: './account.component.html',
  styleUrls: ['./account.component.css']
})
export class AccountComponent {

  @Input() id: number;
  @Input() account: {name: string, status: string};
  @Output() statusChanged = new EventEmitter<{ id: number, newStatus: string }>();

  onSetTo(status: string) {
    this.statusChanged.emit({ id: this.id, newStatus: status });
    console.log('An account status changed, new status: ' + status);
  }
}

La propiedad account es de tipo objeto y contiene sus propias propiedades (name status), así que las vinculamos mediante string interpolation en el account.component.html.

<div class="col">
    <h5>{{account.name}}</h5>
    <p>This account is {{account.status}} </p>
    ...
</div>

7. Añadimos las propiedades account y id al app.component.html mediante property binding, y las vinculamos al index y a la variable local del loop.

<app-account *ngFor="let acc of accounts; let i = index"
             (statusChanged)="onStatusChanged($event)" [id]="i" [account]="acc">
</app-account>

8. Configuramos el método onStastusChanged en el app.component.ts.

  onStatusChanged(updateInfo: { id: number, newStatus: string }) {
    this.accounts[updateInfo.id].status = updateInfo.newStatus;
  }

9. Creamos el esqueleto inicial del new-account.component.html.

<div class="row">
  <div class="col">
    <div class="form-group">
      <label>Account Name</label>
      <input type="text" class="form-control">
    </div>
    <div class="form-group">
      <select class="form-control">
        <option value="active">Active</option>
        <option value="inactive">Inactive</option>
        <option value="hidden">Hidden</option>
      </select>
    </div>
    <button class="btn btn-primary"> Add Account </button>
  </div>
</div>

10. En el botón, añadimos un click event que escuche lo que ocurre cuando el usuario crea una cuenta nueva. Lo vinculamos a un método que se encargue de eso, llamado onCreateAccountque se ayudará de local references para capturar el nombre y el estado de la nueva cuenta.

      <input type="text" class="form-control" #accountName>
    ...
      <select class="form-control" #status>
        ...
      </select>
   ...
    <button class="btn btn-primary"
           (click)="onCreateAccount(accountName.value, status.value)">
      Add Account
    </button>

11. Configuramos otro custom event (esta vez en el new-account.component.ts) para emitir los datos que el usuario introduzca al crear una nueva cuenta. 😣

import { Component, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'app-new-account',
  templateUrl: './new-account.component.html',
  styleUrls: ['./new-account.component.css']
})
export class NewAccountComponent {
  @Output() accountAdded = new EventEmitter<{name: string, status: string}>();

  onCreateAccount(accountName: string, accountStatus: string) {
    this.accountAdded.emit({
      name: accountName,
      status: accountStatus
    });
    console.log('An account status changed, new status: ' + accountStatus);
  }
}

Esos datos los capturará el app.component.html, por medio del custom event "accountAdded"  y el método onAccountAdded.

      <app-new-account (accountAdded)="onAccountAdded($event)"></app-new-account>

¡Nuestra app está terminada! 🤯 Como verás, ocurren dos cosas:

  • cada vez que cambiamos el estado de una cuenta, el cambio se imprime en la consola.
  • cada vez que creamos una cuenta nueva, la cuenta queda registrada y su estado se imprime por consola.

¡Uuufff...! 🥴 Como habrás comprobado, todo esto funciona, pero es innecesariamente complejo. Así que aquí entran en juego los services. Veamos a continuación cómo podemos utilizarlos a nuestro favor para simplificar y mejorar nuestro código. 😃

Cómo crear un service que imprima datos en la consola

Vamos a crear un servicio que imprima datos (en inglés, log data) en la consola, como ya lo estábamos haciendo en el new-account.component.ts y en el account.component.ts. Ya que este servicio se hará cargo de una tarea bastante genérica, lo lógico es colocarlo dentro de la carpeta raíz (la carpeta app).

1. Creamos un archivo manualmente dentro de la carpeta app, llamado log.service.ts. Lo de "service" no es obligatorio, sino una convención para poder identificar rápidamente qué contiene un archivo. Además, al añadir esa coletilla, Visual Studio Code detectará que es un service y le dará al archivo un icono amarillo distintivo.

Una vez creado, vamos al archivo y exportamos una clase de TS a la que llamamos LogService.

Al contrario que un componente o una directiva, un servicio es una clase de TS normal y corriente que no necesita un decorador para funcionar ni ser reconocida por Angular como tal.

2. Creamos un método para imprimir por consola un cambio en el status de una cuenta. Por parámetro le pasamos un status, que será de tipo string. Hacemos un console.log del parámetro para conseguir nuestro objetivo.

export class LogService {
  logStatusChange(status: string) {
    console.log('An account status changed, new status: ' + status);
  }
}

Con este sencillo paso, ya podemos decir que nuestro código para imprimir datos por consola está centralizado en un solo sitio. ¡Estupendo! 👍

Cómo NO usar un service

Una vez construido nuestro servicio, vamos a usarlo en nuestros componentes. Con nociones sobre databindingpodríamos llegar a la conclusión de que la forma de usar un servicio podría ser la siguiente:

1. Importando el archivo en el componente donde queramos usarlo (cojamos el new-account.component.ts como ejemplo).

2. Creando una instancia de la clase LogService, llamando al método logStatusChange y pasándole por parámetro la propiedad del método onCreateAccount (accountStatus).

Lo chocante de esta manera, es que si comentamos el console.log, nuestro código funcionaría perfectamente. 🙃

import { LogService } from '../log.service';

...
    const service = new LogService();
    service.logStatusChange(accountStatus);
    // console.log('An account status changed, new status: ' + accountStatus);
  }

Pruébalo y verás. Sin embargo, esta manera de instanciar servicios manualmente no es la concebida por Angular para usar los servicios. Por tanto, está considerada una mala práctica. ☠ ¡Aléjate de ella! 💩 

Más adelante a lo largo de esta serie de posts explicaremos las razones. De momento, si quieres descubrir cómo usar los servicios y acceder a ellos correctamente, ¡no te pierdas la parte #2 de esta serie! Si te queda alguna duda, ¡nos vemos en los comentarios!

Sobre la autora de este post

Soy Rocío, una abogada reconvertida en programadora. Soy una apasionada de aprender cosas nuevas y ferviente defensora de que la única manera de ser feliz es alcanzando un equilibrio entre lo que te encanta hacer y lo que te saque de pobre. Mi historia completa, aquí. 

Más recursos de aprendizaje

En mi experiencia, la manera más eficaz para aprender Angular es combinando varias vías de aprendizaje. Uno de mis métodos favoritos son los vídeo-cursos y mi plataforma predilecta para eso es Udemy. He hecho varios cursos pero sólo recomiendo aquellos que verdaderamente me han sido útiles. Aquí van:

angular the complete guide - curso Max S.

  Max Schwarzmüller

curso angular fernando herrera

   Fernando Herrera

Si necesitas apoyo en forma de libro, puede que éstos te sirvan de ayuda:

libro 1 angular
libro 2 angular

La programación es un mundo que evoluciona a una velocidad de vértigo. Los autores de estos libros lo saben, por eso suelen encargarse de actualizar su contenido regularmente. Asegúrate de que así sea antes de adquirirlos 😌.

Participo en el programa de afiliados de Udemy y Amazon, lo que significa que, si compras alguno de estos cursos y/o libros, yo me llevaré una pequeña comisión y a ti no costará nada extra. Vamos, lo que se dice un win-win 😊.

Otros artículos que pueden interesarte

Cómo aprendí a programar cuando estaba «programada» para ser de letras
[tcb-script src="https://player.vimeo.com/api/player.js"][/tcb-script]A nadie le gusta su trabajo. Eso es lo que me decía a mí misma cuando conseguí mi primer[...]
Días del 160 al 203 – ¡Primer objetivo conseguido!
“A veces podemos pasarnos años sin vivir en absoluto, y de pronto toda nuestra vida se concentra en un solo[...]
Claves para entender Angular. Qué es y cómo se utiliza
Angular es un framework creado por Google que nos permite construir Single Page Applications (SPA, por sus siglas en inglés).Frameworks¿Pero qué es[...]
Si crees que este post puede serle útil a alguien, ¡compártelo!:

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Esta web utiliza cookies para asegurar que se da la mejor experiencia al usuario. Si continúas utilizando este sitio se asume que estás de acuerdo. más información

Los ajustes de cookies en esta web están configurados para «permitir las cookies» y ofrecerte la mejor experiencia de navegación posible. Si sigues usando esta web sin cambiar tus ajustes de cookies o haces clic en «Aceptar», estarás dando tu consentimiento a esto.

Cerrar