El patrón Strategy en Angular
Vamos a ver un ejemplo práctico de la aplicación del patrón estrategia ó “Strategy pattern” el cual pertenece a los patrones de comportamiento y del que existen bastantes páginas explicando la teoría junto con su definición, diagramas, cuando usarlo y un largo etcétera. En nuestro caso, vamos a ponernos 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, vamos a combinarlo junto con el patrón “Template method”, donde cada estrategia derivará de una clase abstracta para encapsular parte común del código.
Para implementar el patrón «strategy», necesitaremos definir el caso preguntándonos:
- 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:
- Caso de aplicar solo una estrategia: Aplicar una operación CRUD → El usuario en un momento dado solo crea o borra o modifica o consulta los datos.
- Caso de aplicar varias estrategias: Dado un texto obtener las preposiciones y artículos → El usuario quiere saber las preposiciones y artículos de un texto.
- Caso de aplicar varias estrategias con un orden: Dada una fórmula, se aplican varias estrategias teniendo en cuenta el orden de precedencia de los operadores.
Cuando se saben estas preguntas sabremos que:
- Si solo se aplica una estrategia usaremos un “find” sobre la lista de estrategias (devuelve solo una estrategia)
- Si se pueden aplicar varias usaremos un “filter” sobre la lista de estrategias (devuelve array con varias estrategias)
- Si existe un orden, usaremos un “sort” sobre la lista de estrategias.
Como habrás podido intuir, las estrategias las pondremos en una lista (array) de estrategias y cumpliendo una interfaz que contenga 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…
Como podrás ver, la propia estrategia implementa la condición si debe o no aplicarse (canApply). De esta manera se evita que en algún sitio, la decisión de qué estrategia aplicar se transforme en el típico bloque if/else if/else o switch para devolver una u otra estrategia.
Será en 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 (humedad). También podemos observar que el orden búsqueda de una estrategia depende de la zona donde está instalado el riego. Este orden aquí no es muy relevante pero sí puede ser cuando se tienen que aplicar varias estrategias y el resultado final depende del orden de aplicación. En este caso se aplica sólo una estrategia cada vez.
Por lo tanto definiremos una interfaz que contenga la configuración del sistema (datos de entrada) llamada IConfiguration de la siguiente forma:
export interface IConfiguration {
zone: ClimateZone;
humidity: number;
}
ClimateZone es simplemente un enumerado de la siguiente forma:
export enum ClimateZone {
Dry,
SubHumid,
Humid
}
y también 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 este caso, aplicar la estrategia ejecuta un código sin devolver nada (void) pero quizás 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);
}
}
}
En el caso de tener que aplicar varias estrategias, en vez de un «find» usaremos un «filter» (el orden lo aplicamos una vez hecho el «filter») y luego un «forEach» para aplicar cada una:
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