El patrón Strategy en Angular
Vamos a ver el patrón Strategy en Angular perteneciente a los patrones de comportamiento mediante un ejemplo práctico. Nos pondremos con las manos en el código para llevar a cabo este patrón de diseño en Angular (typescript).
Para no quedarnos solo en este patrón, lo combinaremos con el patrón “Template method”, donde cada estrategia derivará de una clase abstracta para encapsular parte común del código.
Preliminares
Para implementar el patrón necesitaremos preguntarnos:
- Se aplica solamente una estrategia cada vez? o se necesita aplicar varias?
- En caso de poder aplicarse varias estrategias, se aplicaría solo la primera que cumpla la condición? o todas?
- Debe haber un orden de aplicación de estrategias?
Ejemplos:
- Ejecutar una operación CRUD → El usuario en un momento dado solo crea o borra o modifica o consulta los datos.
- Obtener las preposiciones y artículos de un texto → El usuario quiere saber las preposiciones y artículos de un texto.
- Dada una fórmula, se aplican varias estrategias teniendo en cuenta el orden de precedencia de los operadores.
Cuando se sabe la respuesta a estas preguntas se obtiene el operador utilizar:
- Usaremos un “find” sobre la lista de estrategias (devuelve solo una estrategia)
- Utilización de un “filter” sobre la lista de estrategias (retorna un array con varias estrategias)
- Emplearemos un “sort” sobre la lista de estrategias.
Las estrategias estarán en una lista (array) cumpliendo una interfaz porque así contendrán los siguientes métodos o propiedades:
- canApply → Decidir si esa estrategia aplica o no.
- apply → Aplicación de la estrategia
- order → Orden de aplicación de las estrategias. Puede ser directamente uno especificado o bien se calcule en función de algún parámetro. Si no, pues en el orden que ocupen en el array.
Quizás te pueda gustar más usar “execute” en vez de “apply”, pero esto ya es cuestión de naming.
La propia estrategia implementa la condición de si debe o no aplicarse (canApply). De esta manera se evita que en algún sitio, la decisión de qué estrategia ejecutar se transforme en el típico bloque if/else if/else o switch para devolver una u otra estrategia.
Será la clase contexto (Context) la que obtenga qué estrategia aplicar preguntando a cada una si puede o no ejecutarse.
Caso práctico
En un huerto hay instalado un sistema de riego para las plantas. Mediante sensores, el sistema comprueba una vez al día la humedad de la tierra, regando un tiempo según su porcentaje, siendo:
- Menos del 20% els sistema se activa durante 15 minutos.
- Entre 20% y 50% se activa durante 10 minutos.
- Más del 50% y hasta el 80% se activa durante 5 minutos.
El sistema se puede instalar en zonas según el nivel de precipitaciones: seco, subhúmedo o húmedo/lluvioso (clasificación climática), de manera que se intentaría aplicar en un clima seco la primera estrategia, en clima medio la segunda y con lluvias la tercera estrategia.
Llevando este ejemplo a nuestro dominio, vemos claramente que existen 3 estrategias de riego en función de un parámetro de entrada que sería la humedad. Además, el orden búsqueda de una estrategia depende de la zona climática donde está instalado el riego. El orden aquí no es muy relevante pero sí puede ser cuando se tienen que aplicar varias estrategias y el resultado final dependa de este orden de aplicación. En este caso se aplica sólo una estrategia cada vez.
Definiremos una interfaz que contenga la configuración del sistema representando los datos de entrada. A esta interfaz la podemos denominar IConfiguration:
export interface IConfiguration {
zone: ClimateZone;
humidity: number;
}
La definición del enumerado ClimateZone tiene el siguiente aspecto:
export enum ClimateZone {
Dry,
SubHumid,
Humid
}
IStrategy es la interfaz que deberán cumplir todas las estrategias:
export interface IStrategy {
canApply(value: IConfiguration): boolean;
apply(value: IConfiguration): void;
order(value: IConfiguration): number;
}
En nuestro ejemplo la estrategia ejecuta un código sin devolver nada (void) pero es posible que en otros casos sí se necesite que devuelva algún valor, objeto, u otro tipo de entidad.
Dado que todas las estrategias deben regar de la misma forma, heredarán de una clase abstracta para que todas ellas usen el mismo sistema de riego. Con esto implementamos el patrón «Template Method»:
export abstract class StrategyTemplate {
protected irrigate(minutes: number): void {
console.log(`irrigation duration: ${minutes}`);
}
}
A partir de aquí, creamos las tres estrategias:
Estrategia riego 1 (humedad < 20% → 15 minutos de riego):
@Injectable()
export class DryStrategy extends StrategyTemplate implements IStrategy {
public order(value: IConfiguration): number {
return value.zone === ClimateZone.Dry ? -1 : 0;
}
public canApply(value: IConfiguration): boolean {
return value.humidity < 0.2;
}
public apply(value: IConfiguration): void {
this.irrigate(15);
}
}
Estrategia riego 2 (humedad > 20% y <= 50% → 10 minutos de riego):
@Injectable()
export class SubHumidStrategy extends StrategyTemplate implements IStrategy {
public order(value: IConfiguration): number {
return value.zone === ClimateZone.SubHumid ? -1 : 0;
}
public canApply(value: IConfiguration): boolean {
return value.humidity > 0.2 && value.humidity <= 0.5;
}
public apply(value: IConfiguration): void {
this.irrigate(10);
}
}
Estrategia riego 3 (humedad > 50% y <= 80% → 5 minutos de riego):
@Injectable()
export class HumidStrategy extends StrategyTemplate implements IStrategy {
public order(value: IConfiguration): number {
return value.zone === ClimateZone.Humid ? -1 : 0;
}
public canApply(value: IConfiguration): boolean {
return value.humidity > 0.5 && value.humidity <= 0.8;
}
public apply(value: IConfiguration): void {
this.irrigate(5);
}
}
La clase context buscará entre todas las estrategias la que se debe aplicar:
@Injectable()
export class ContextService {
private _strategies: Array<strategies.IStrategy> = [];
public constructor(
private _dryStrategy: strategies.DryStrategy,
private _subHumidStrategy: strategies.SubHumidStrategy,
private _humidStrategy: strategies.HumidStrategy
) {
this._strategies = [
this._dryStrategy,
this._subHumidStrategy,
this._humidStrategy
];
}
public applyStrategy(value: IConfiguration): void {
const strategyFound: strategies.IStrategy = this._strategies
.sort(
(strategyA: strategies.IStrategy, strategyB: strategies.IStrategy) => {
return strategyA.order(value);
}
)
.find((strategy: strategies.IStrategy) => {
return strategy.canApply(value);
});
if (strategyFound) {
strategyFound.apply(value);
}
}
}
Emplearemos un «filter» en vez de un «find» y posteriormente el «sort» para que finalmente podamos recorrer la lista con el «forEach»:
public applyStrategy(value: IConfiguration): void {
const strategiesFounded: Array<strategies.IStrategy> = this._strategies
.filter((strategy: strategies.IStrategy) => {
return strategy.canApply(value);
})
.sort(
(strategyA: strategies.IStrategy, strategyB: strategies.IStrategy) => {
return strategyA.order(value);
}
);
(strategiesFounded || []).forEach((strategy: strategies.IStrategy) => {
strategy.apply(value);
});
}
Espero que este caso sirva para aprender este patrón strategy y pueda ser útil utilizarlo en los diferentes desarrollos. ¿Qué te ha parecido?
Puedes descargar el código del artículo en GitHub