Let’s see an Angular version of the Adapter pattern.
Mapping allow us to decrease coupling between what arrives from a remote BE on our FE. We can take advantage of one of the Gang of Four’s design patterns (the Adapter pattern) to transform the external response in what we want.
Adapter pattern in models
Every model will have its personal Adapter. We need the adapt method to have the same keys of the external service, as input.
// app/models/Person.ts
export class Person {
constructor(
public id: number,
public firstname: string,
public surname: string,
public birthDate: Date,
) { }
static adapt(item: any): Person {
return new Person(
item.uid,
item.name,
item.lastname,
new Date(item.birth),
);
}
}
As you can see above, the adapt
method allows you to insert even very different object properties in the Person
object that we will generate. It is also possible to do a mapping
or a filter
, in case you have lists of some kind.
It is even possible to nest further calls to adapt
methods of other objects, within our own adapt
.
Adapter in services
And in our service we can call the model adapt method when mapping on external datas.
// app/services/person.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Person } from '../models/Person';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class PersonService {
private _apiUrl = 'http://api.myapp.com/persons';
constructor(private readonly _http: HttpClient) {}
/** For a list of persons */
listPersons(): Observable<Person[]> {
return this.http.get<any[]>(this._apiUrl).pipe(
map(data => data.map(Person.adapt))
);
}
/** For a unique person */
getPerson(): Observable<Person> {
return this.http.get(this._apiUrl).pipe(
map(data => Person.adapt(data.info))
);
}
}
Our Backend service will expose a service like this:
// listPersons
[
{
"uid": 1,
"name": "John",
"lastname": "Doe",
"birth": "02/23/1900"
},
{
"uid": 2,
"name": "Mary",
"lastname": "Doe",
"birth": "03/23/2000"
},
]
Or this:
// onePerson
{
"info": {
"uid": 1,
"name": "John",
"lastname": "Doe",
"birth": "02/23/1900"
}
}
You may notice that the object keys in the service are different from the properties of our internal object.
This is the way to use it in your component:
// person.component.ts
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Subscription} from 'rxjs';
import {PersonService} from '../app/services/person.service';
@Component({
selector: 'app-person',
templateUrl: './person.component.html',
})
export class PersonComponent implements OnInit, OnDestroy {
private sub: Subscription;
public person: Person;
public error: boolean;
constructor(private _personService: PersonService) {}
ngOnInit(): void {
this.sub = this._personService.listPersons().subscribe({
next: person => {
this.person = person;
},
error: err => {}
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
If we have simpler situations can use the basic mapping, with generics:
getPersons(): Observable<Person[]> {
return this._http.get(this._apiUrl + 'persons').pipe(
map<any, Person[]>(response => response.result.items)
);
}
Or maybe we have only one instance of the object in output, to be mapped:
getPerson(): Observable<Person> {
return this._http.get<Person>(this._apiUrl + 'persons');
}
This is my version of the adapter pattern in Angular.
Try it at home!