miércoles, 31 de julio de 2024

El Patron Observador(Eventos y Suscriptores) y su Relación con Redux

El patrón observador es otro patrón de diseño que nos encontramos muy a menudo, y para bien o para mal, muchas veces tenemos que usarlo de manera forzosa en muchos APIs (sobre todo en aquellos que utilizan funciones de red).

¿De qué se trata este patrón?, pues en resumen consta de 3 elementos principales:

  • Eventos. Se trata de objetos que contienen una etiqueta (generalmente por medio de una cadena de caracteres) y además de éso pueden contener cualquier información (la etiqueta suele utilizarse para diferenciar unos eventos de otros, casi como el tipo de una estructura lo hace).
    Algunos sistemas de eventos requieren que la información dentro del evento sea serializable, pero no siempre es el caso.
  • Emisores.Los emisores son objetos que pueden emitir eventos. Es decir, tienen una función para emitir que recibe un evento.
  • Suscriptores. Los suscriptores son callbacks que se añaden a los emisores, y son llamados cada que un emisor emite un evento con una etiqueta específica.

Por ejemplo, en un servidor HTTP se podría recibir un evento cuando un cliente intenta visitar una URL dada; en una aplicación de escritorio se podría emitir un evento cuando el usuario presiona un botón; o en un videojuego se puede recibir un evento una vez que se terminaron de cargar las texturas.


Por si no quedó del todo claro, la idea es que cada evento represente un suceso del cual alguna parte de la aplicación debe de encargarse. Se utiliza un emisor para notificar cuando algún evento sucede mientras que los suscriptores son los encargados de tomar acción una vez que el evento ya sucedió.


Un uso no tan obvio, pero que me parece bastante útil (valga la redundancia) es evitar el antipatrón de efectos secundarios. Ya que es bastante común que parte de los requerimientos del proyecto incluyan realizar una acción B, cada que una acción A se realiza.

Por ejemplo, en un juego RPG, podría ser inmovilizar al personaje cada que se muestra un cuadro de dialogo.

MAL ❌
export function showDialog(text : string, sytle : DialogueStyle) : void {
    drawMessageBackground(style.backgroundStyle);
    drawText(text, style.textStyle);
    freezeCharacter();
}
...
showDialog("It's dangerous to go alone, take this");
Este código tiene el problema de que si alguien mira el lugar donde se manda llamar showDialog, no hay indicio alguno de que el presonaje se vaya a inmovilizar.
Por otro lado, si se optara por mandar llamar a freezeCharacter cada que vez que se manda llamar la función showDialog, rompería con el principio DRY y en consecuencia haría el código más propenso a errores(a alguien se le podría olvidar mandar llamar a freezeCharacter, o borrarlo por accidente o por un malentendido) y más difícil de mantener.
Por el lado de las pruebas, podría complicar las cosas ya que toda prueba unitaria que involucre mostrar un diálogo debería de configurar las dependencias para incluir al personaje también.
Por último, si se creara una función llamada showDialogAndFreezeCharacter , se rompería el principio de que una función debe hacer una y sólo una cosa.

MEJOR✅

export function showDialog(text : string, sytle : DialogueStyle) : void {
    drawMessageBackground(style.backgroundStyle);
    drawText(text, style.textStyle);
    events.emit(DIALOG_DISPLAYED);
}
...
events.on(DIALOG_DISPLAYED, freezeCharacter);
...
showDialog("It's dangerous to go alone, take this");
Esta implementación es más coherente porque la función showDialog, en sí misma no está haciendo otra cosa distinta a mostar el dialogo, mientras que la acción de "cada vez que se muestre un diálogo congela al personaje", está claramente descrita en un lugar a parte y el bajo nivel de acoplamiento permite escribir pruebas unitarias más fácilmente.

Ventajas

  • Un bajo nivel de acoplamiento. Dado que para utilizar un emisor no es necesario conocer información alguna sobre quienes van a ser los sucriptores y los suscriptores tampoco necesitan saber la información de quien emitió los eventos, el acoplamiento es extremadamente bajo.
    Lo cual se traduce en facilidad para crear pruebas automáticas (una prueba puede emitir eventos sin necesidad de crear un fake o un mock) y facilidad de modificar la implementación de una clase sin romper otras clases.

  • Facilidad para crear bitácoras. Es posible poner en una bitácora cada que se emite un evento y de ésa manera tener una idea de qué está sucediendo.

Desventajas

  • Cambiar el esquema de un evento puede ser muy costoso.

  • El stack pierde su utilidad. Normalmente basta con ver la pila de llamadas para poder saber cual fue la secuencia de pasos que llevaron a suceder una situación en particular (sobre todo cuando hay una excepción no cachada). El problema con esta arquitectura de eventos es que es bastante común que el stack muestre una clase encargada de desencolar los eventos del emisor, sin dejar pista alguna de cómo fue que se llegó a emitir ése evento.

  • Las llamadas asíncronas se vuelven más problemáticas en las pruebas. Cuando se quiere probar un procedimiento asíncrono que inicia con la emisión de un evento, no es trivial saber cuándo termina para poder verificar que el resultado fue correcto (a veces hay que crear funciones específicamente para que le notifiquen a la prueba cuando el resultado ya está listo).


Eventos y Redux

El resto del post habla únicamente de Redux, les aviso a quienes no estén familiarizados o no estén interesados en Redux, que probablemente ya leyeron todo lo que les puede interesar de esta publicación.

En la guía de estilo de Redux podemos leer esto:
De manera que idealmente cada acción de redux debería de describir algo que ocurrió en la aplicación, por ejemplo: el usuario hizo click en un botón, el usuario presionó cierta tecla, o incluso cosas más abstractas como que se pidió que una ventana se cerrara, o cambió el nombre de un objeto.

... en lugar de que las acciones de redux sean peticiones para cambiar el estado global (esto comparte los mismos problemas graves que dan las variables globales), las acciones simplemente deben de describir algo que ya ocurrió y dejar que sean los reducers los que se encarguen de cambiar el estado interno y los selectores los que se encarguen de leer el estado interno (el resto del proyecto no debería de necesitar siquiera saber cómo está estructurado el estado de Redux).

Todo muy bonito, pero hay un problema...

No le dedicaría un espacio tan grande en este post a simplemente regurgitar lo que ya dice la guía de estilo de Redux, y es que hace un tiempo me percaté de que Redux Toolkit parece tener construidos los slices de manera que hace imposible poder usar las acciones como eventos.

Redux Toolkit posee la función createSlice , a la cual se le pasa un estado inicial y una lista de reducers, y para cada reducer crea una acción para llamarlo. ¡Prácticamente crea un montón de variables globales con funciones para cambiarlas!, y para hacer las cosas peor, cada slice aisla su estado interno del resto del estado, de manera que no puede ser cambiado desde afuera(esto esto es malo porque pareciera que es imposible que una acción cambie el contenido de varios slices, impidiendo su funcionamiento como eventos).

Así que parece todo perdido pero...

¡Esto tiene una solución!

Naturalmente, los creadores de Redux Toolkit no diseñaron el framework pensando en romper la guía de estilos de Redux, sin embargo, me sorprendió lo mucho que me costó encontrar la solución para esto.

Resulta que createSlice tiene un parámetro opcional llamado extraReducers. Utilizando ése parámetro se pueden crear reducers que en lugar de que reaccionen a su propia acción creada por el slice, reaccionen a una acción definida en otro lado.

Su syntaxis es un poco rara, pero no es dificil de usar: extraReducers recibe un callback, y el callback recibe como parámetro un objeto que llamaremos armador, este armador tiene un método llamado addCase, el cual recibe como parámetro una acción ya existente y el reducer con el cual debería de reaccionar.

Suena muy enrevesado, pero viéndolo en acción no lo es tanto:

createSlice({
	name : "fanboy",
    initialState: {
    	name : "Juanito",
        surname : "Perez",
        emotion: "indiferent",
        usd: 100
    },
    reducers: {
    	changedHisName : (state, action: PayloadAction<{name: string, surname: string}>) => {
        	state.name = action.name;
            state.surname = action.surname;
        },
        earnedMoney : (state, action: PayloadAction<number>) => {
        	state.usd += action.payload;
            state.emotion = "happy";
        }
    },
    extraReducers: (builder) => {
    	builder.addCase(NINTENDO_RELEASED_GAME, (state, action: Payload<{metacritic_score : number}>) => {
        	if (action.payload.metacritic_score < 80) {
            	state.emotion = "angry";
                return;
            }
            if (action.payload.metacritic_score < 90) {
            	state.emotion = "indiferent";
                return;
            }
            if (state.usd < 60) {
            	state.emotion = "sad";
                return;
            }
            state.usd -= 60;
            state.emotion = "happy";
        }).addCase(SONY_RELEASED_GAME, (state, action: Payload<{metacritic_score : number}>) => {
        	if (action.payload.metacritic_score > 90) {
            	state.emotion = "angry";
            }
        });
    }
});
Por si no se entiende por el contexto, en este caso NINTENDO_RELEASED_GAME y SONY_RELEASED_GAME son acciones definidas fuera del slice, pero que una vez despachadas cambian el contenido del slice.

jueves, 18 de julio de 2024

¿Herencia? Composición FTW





 "Preferir Composición sobre Herencia" es uno de los lemas más comunes cuando se habla de patrones de diseño.

Desafortunadamente, en mi pequeña investigación que hice sobre los artículos en español que hablan de este tema encontré que todos tenían errores y ninguno hablaba de la ventaja de reducir la cantidad de dependencias. Así que tal vez yo sí pueda aportar algo con esto después de todo.

Voy a asumir que quien sea que lea esto ya sabe lo que es la herencia; y pese a ser una piedra angular de la programación orientada a objetos, presenta muchos problemas.

Problemas con la Herencia

En primer lugar la herencia múltiple presenta el problema del diamante, no me quiero detener a explicarlo porque es un problema bastante conocido.

¿...y que tal la herencia simple?, pues aquí sucede que no siempre es fácil estructurar las clases siguiendo un árbol de herencia simple. Aquí pongo un ejemplo muy sencillo, imagina que estamos haciendo un videojuego(RTS) y va a tener las siguientes clases:

  • GroundAlly puede caminar, disparar, refugiarse en una construcción aliada y además es controlada por indicaciones del jugador
  • FlyingAlly puede volar, disparar, refugiarse en una construcción aliada y es controlado por indicaciones del jugador.
  • GroundEnemy puede caminar, disparar y es controlado por una IA.
  • FlyingEnemy puede volar, disparar y es controlado por una IA.
Es tentador hacer una clase padre que sea MoveableObject, de la cual heredarían Ally y Enemy, pero luego habría que implementar 2 veces la cinematica en tierra y 2 veces la cinematica en aire.

De manera similar, podríamos hacer que las clases bases fueran FlyingObject y GroundObject, pero nos encontraríamos otra vez con el mismo problema.

Incluso si fuera posible estructurar las clases de manera que formen un árbol, aún se correría el riesgo de que tras algún cambio de planes se necesiten añadir más clases y echen a perder el diseño.

Otro problema bastante fuerte de la herencia es al escribir pruebas unitarias, si tienes una clase C, que hereda de una clase B, que a su vez hereda de una clase A, en el momento de escribir una prueba unitaria para la clase C, va a ser necesario crear fakes y mocks para todas las dependencias de las clases A y B, pese a que el código que se está probando es el de la clase C.

Hay una conferencia que me gustó mucho llamada "Inheritance is the Base Class of Evil", y les recomiendo verla, ya que muestra bastantes otros problemas que los aquí mencionados:

https://www.youtube.com/watch?v=bIhUE5uUFOA

... pero yo me voy a centrar en resolver lo que mencioné aquí.

Composición

Primero que nada hay que mencionar qué es la composición.

A groso modo se trata de que un objeto almacene en su interior una instancia de una interfaz y la mande llamar:

export interface IMoveableObject {
    getPosition() : {x : number, y : number};
    setVelocity(x : number, y : number) : void;
}
export class ArriveSteering {
	private target : {x : number, y : number};
    max_speed = 100;
    alpha = 10;
    constructor(private entity : IMoveableObject, target : {x : number, y : number}) {
    	this.target = {...target};
    }
    update() : void {
        const disp = sub(this.target, this.entity.getPosition());
        const velocity = clamp(0, this.max_speed, mult(this.alpha, disp));
        this.entity.setVelocity(velocity);
    }
}

Esto provee una manera de lograr polimorfismo sin tener que extender clases (ya que se puede cambiar la implementación de getPosition y setVelocity y hacer que ArriveSteering acabe funcionando diferente).

Una forma de lograr el primer ejemplo que se mencionó(el del RTS) usando composición sería:

  • Crear una interfaz llamda Behaviour, y haya 2 clases que implementen esta interfaz: una través de IA y la otra a través de órdenes del jugador.
  • Crear otra interfaz llamada Action, la cual sería implementada en clases para disparar, caminar y volar
  • Crear una clase llamada Entity, que reciba un arreglo de Action y un Behaviour.

Ventajas de la Composición sobre la Herencia

Las ventajas de la composición como sustituto de la herencia deberían estar claras:

  • Menos dependencias (esto sirve de mucho en las pruebas unitarias)
  • El árbol de tipos queda mucho más limpio.

¿Cuándo no usar composición?

Quizá se esté preguntando ¿hay algún caso en que convenga no usar composición?, ¿tiene alguna desventaja?, pues he aquí lo que me viene a la mente:
  • Es posible escribir una función que opere con un objeto que implementa una interfaz sin necesidad de crear una clase (esto NO es un caso de uso de la herencia sino para resaltar que a veces ni siquiera es necesario componer).
  • Está la posibilidad de usar agregación(crear objetos dentro de la clase) e implementar interfaces sin extender (cuando no se extiende no sucede el problema del diamante y se pueden hacer cosas parecidas a la herencia múltiple).
    En el ejemplo del RTS se podrían crear interfaces GroundObject, FlyingObject, AllyObject, EnemyObject, luego crear las 4 clases e implementar las interfaces correspondientes utilizando otras clases para reutilizar el código repetido.
  • A veces resulta un tanto raro que un montón de objetos diferentes sean de exactamente el mismo tipo (dado que su única diferencia es el contenido). Así que varias de estas veces los programadores acaban usando herencia simplemente para fijar los objetos que se componen (no sé si esto sea un anti-patrón).
Por último, creo que una pregunta obligatoria sería ¿hay algún caso en que sea preferible usar herencia?, fuera de que(desgraciadamente) muchas APIs exigen el uso de la herencia, sólo se me ocurren casos muy raros donde se quiera tomar ventaja de la evaluación de tipos del compilador en cuestión.

También algunos podrían señalar que la herencia requiere menos código que la composición, así que para casos pequeños podría resultar mas conveniente usar herencia. El problema es que en un proyecto grande las partes del proyecto que parecían que iban a ser pequeñas tienden a volverse más grandes de lo esperado. Quizá para proyectos pequeños podría resultar adecuada pero no me atrevo a recomendarla (le doy el beneficio de la duda, ya que la experiencia me ha enseñado que ser muy tajante no suele ser buena idea).

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).

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 ...