lunes, 8 de julio de 2024

Patrón de Diseño: Estrategia

 En las últimas semanas me ha costado bastante trabajo darme tiempo para escribir porque recién acabo de mudarme, pero espero a partir de ahora poder retomar el ritmo.

La tercera parte de programación dinámica + máscaras de bits tendrá que esperar, ya que es algo bastante pesado de escribir y quiero tomarme un descanso abordando un tema más ligero y con el que he estado trabajando mucho recientemente, así que lo tengo fresco: el patrón "estrategia".

Supongamos que queremos hacer un programa que reciba una lista de instrucciones y ejecute cada una de ellas. Cada instrucción estaría dada por un objeto en formato json, habría un campo llamado `type` especificando el tipo de instrucción y el resto de los campos dependerían del tipo.

Una posible implementación de esto sería creando un método público que reciba el objeto, y varios métodos privados para cada tipo de accion:



interface Action {
	type : string;
}

const DIRECTIONS : ReadonlyArray<{x : number, y : number}> = [
	{x : 1, y : 0},
	{x : 0, y : 1},
	{x : -1, y : 0},
	{x : 0, y : -1}
];

class Karel {
    position = { x : 0, y : 0};
    direction = 0;
    beepers_in_bag = 100;
    beepers_in_world = new Map>();
    performAction(action : Interface) : void {
    	switch(action) {
            case 'putBeeper':
            	return this.putBeeper();	
            case 'pickBeeper':
            	return this.pickBeeper();
            case 'turnLeft':
            	return this.turnLeft();
            case 'move':
            	return this.move();
            default:
            	console.error("Unknown command: ", action.type);
        }
    	if (action === 'putBeeper') {
        	this.putBeeper();
        }
    }
    private turnLeft() : void {
    	this.direction = (this.direction + 1) % 4;
    }
    private move() : void {
    	this.position = {
            x : this.position.x + DIRECTIONS[this.direction].x,
            y : this.position.y + DIRECTIONS[this.direction].y
	};
    }
    private putBeeper() : void {
    	if (this.beepers_in_bag === 0) {
        	return;
        }
        this.beepers_in_bag--;
    	let map_x = this.beepers_in_world.get(this.position.x);
        if (!map_x) {
        	this.beepers_in_world.set(this.position.x, new Map());
        }
        map_x = this.beepers_in_world.get(this.position.x);
        map_x.set(this.position.y, map_x.get(this.position.x ?? 0 + 1);
    }
    private pickBeeper() : void {
    	const beepers_in_position = this.beepers_in_world.get(this.position.x)?.get(this.position.y);
        if (!beepers_in_position) {
        	return;
        }
        this.beepers_in_world.get(this.position.x).set(this.position.y, beepers_in_position - 1);
        this.beepers_in_bag++;
    }
}
Para algo tan simple podría parecer que esta es una solución decente, pero ahora vamos a imaginar que además de las instrucciones 'putBeeper', 'pickBeeper', 'turnLeft' y 'move', se agregan 20 instrucciones más.

Si sucediera algo así nos encontraríamos con una función extremadamente larga (la función `performAction`) y una clase con muchas posibles razones para modificarse (rompiendo el principio de única responsabilidad) y demasiadas dependencias; ¿cómo podríamos evitar este desastre?, pues por si no quedó claro con el titulo del post: con el patrón Estrategia.

La idea principal de este patrón es separar cada acción en su propia clase y tener una clase central con un map que mande a llamar a la clase correspondiente. Esto presenta de ventajas exactamente lo opuesto al problema que nos llevó a usarlo: reducir la cantidad de dependencias de una sola clase, permite delegar responsabilidades para así cumplir con el principio de responsabilidad única, lo cual hace el código más mantenible a la larga y facilita la creación de pruebas automáticas.
// KarelTypes.ts
export interface KarelAction {
    type : string;
}
export interface KarelContext {
    position : { x : number, y : number};
    direction : number;
    beepers_in_bag : number;
    beepers_in_world : Map>;
}

export interface KarelActionHandler {
    type : string;
    performAction(context : KarelContext, action: KarelAction);
}

// Karel.ts

const ACTION_HANDLERS = new Map;

export function addKarelActionHandlers(handlers : KarelActionHandlers[]) {
    for (const h of handlers) {
    	ACTION_HANDLERS.set(h.type, h);
    }
}

export class Karel {
    constructor(public context : KarelContext) {}
    performAction(action : Interface) : void {
    	const handler = ACTION_HANDLERS.get(action.type);
        if (!handler) {
            console.error("Unknown action:", action.type);
            return;
        }
        handler.performAction(this.context, action);
    }
}

// turnLeft.ts

export class TurnLeftHandler implements KarelActionHandler {
    type = "turnLeft";
    performAction(context : KarelContext/*, action: KarelAction*/) {
    	context.direction = (context.direction + 1) % 4;
    }
}
No puse los otros handlers porque creo que con uno es suficiente para entender la idea.

Sobre las desventajas del patrón; hasta el momento la única desventaja clara que he encontrado es que cuando las acciones requieren parámetros propios, puede resultar un tanto engorroso verificar que los parametros recibidos sigan el esquema correcto.

Así que como conclusión diría que este patrón es ideal a utilizar cuando se recibe una lista de instrucciones por archivo, por web o por medios similares; pero no vale la pena generar ésa lista para utilizar este patrón si las acciones no se tienen en ése formato para empezar (por ejemplo, una lista de undo/redo en un programa de edición presenta más alternativas que utilizar el patrón estrategia de esta manera).

No hay comentarios.:

Publicar un comentario

De StackOverflow a ChatGPT: El Arte de Pegar sin Pensar

 Es curioso cómo apenas en Agosto del año pasado critiqué duramente la práctica de copiar código de páginas de internet para pegarlo dentro ...