martes, 10 de marzo de 2020

Composición de software - Inyeccion de dependencias funcional en la canalización de funciones.

La cosa viene de la I  y II parte. Pégale un vistazo si no lo has hecho todavía.

En esta entrada voy a demostrar lo fácilmente que se puede modificar y/o extender el comportamiento del workflow de la aplicación y además como se puede hacer inyectando las dependencias para conseguir una arquitectura con bajo acoplamiento.

Voy a modificar el ejemplo de las entradas anteriores para agregar una capa de infraestructura (en este caso; un repositorio externo) para que el workflow de la aplicación incluya el flujo necesario de principio a fin. Con esto demuestro que la canalización de funciones no es solo para el núcleo de la aplicación; si no que se puede usar para ofrecer una interacción de principio a fin con el sistema.

Lo primero es crear el repositorio; en este caso voy a crear 2 para demostrar la inyección de dependencias. No os preocupéis ahora sobre el control de errores o la gestión asíncrona para no tener que añadir mas ruido al asunto.

//module persistence
function redisRepo(config) {
  const redis = new Redis({
    ...config
  });
 
  return {
    getGameState(pk) {
      return redis.get(pk);
    },
    setGameState(pk, gameState) {
      return redis.set(pk, gameState);
    }
  };
}
 
function mongoRepo(config) {
  const MongoClient = require('mongodb').MongoClient;
 
  return {
    findGameState(pk) {
      return MongoClient.find(pj);
    },
    updateGameState(pk, gameState) {
      return MongoClient.updateOne(pk, gameState);
    }   }   const redisPersistence = redisRepo(config);   const mongoPersistence = mongoRepo(config);

Como se puede observar; los he creado con diferente interfaz para que, más adelante, se pueda apreciar que no es necesario mantener interfaces comunes; por lo cual aporta flexibilidad y contexto ahorrándose el tener que escribir formalmente la definición e implementacion de una interfaz, usar herencias y/o mixins.

Ahora vamos a agregar el código de orquestación canalizable encargado de leer y escribir de persistencia. Aquí tenemos que inyectar la dependencia al repositorio que vayamos a usar; pero, en vez de inyectar todo el objeto y que el código de orquestación tenga que conocer la interfaz de ese objeto para poderlo usar, vamos a inyectar simplemente la función/es que necesita el código de orquestación para realizar su operación.

//module orchestation code
const readGameState = (readerFnc, { gameStateId }) => {
  const gameState = readerFnc(gameStateId);
  return !gameState ?
    Either.left("no game found in persistence":
    Either.right({ gameState });
};

const writeGameState = (writerFnc, { gameStateId, gameState }) => {
  const result = writerFnc(gameStateId, gameState);
  return !result ?
    Either.left("unable to write gameState into persistence":
    Either.right({});
};

Y ya, a la hora de construir el workflow de la aplicación; simplemente tenemos que seleccionar las  funciones que vamos a usar:
const gameReader = redisPersistence.getGameState.bind(redisPersistence);
const gameWriter = redisPersistence.seGameState.bind(redisPersistence);




Y modificar el workflow de forma trivial poniendo al principio y al final las funciones de infraesctructura nuevas: que pueden ser reutilizadas como cualquier otra. Fijaos como ahora el valor de gameState es inyectado en la canalización por la función de orquestación readGameState y por lo tanto; el punto de entrada al workflow es simplemente el identificador en persistencia del gameState. Awesome!

const executeOperationWorkFlow = pipe(
    //pass the gameState reader to the orchestation function
  readGameState.bind(undefined, gameReader) 
  , checkIsNotPlayerTurn
  , obtainOperationCost
  , checkNotEnoughEnergy
  , executeOperation
    //pass the gameState writer to the orchestation function
  , writeGameState.bind(undefined, gameWriter)
);
 
const endTurnWorkFlow = pipe(
    //pass the gameState reader to the orchestation function
    readGameState.bind(undefined, gameReader)
    , checkIsNotPlayerTurn //reusing orchestation fncs for any workflow we need
    , endTurn
     //pass the gameState writer to the orchestation function
    , writeGameState.bind(undefined, gameWriter)
);
 
  //how to use
  let result = executeOperationWorkFlow({ gameStateId, player, operation, target });
result.cata(
  /*
   * outputs:  "Failed because no game found in persistence"
   *                             OR
   *            "Failed because it is not player turn" 
                                 OR 
              "Failed because player has not enough energy...."
                                 OR
          "Failed because unable to write gameState into persistence"
  */
  failure => `Failed because ${failure}`,
  success => `Great!. New state of the game after the operation ${success.gameState}`
);
 
 
result = endTurnWorkFlow({ gameStateId, player });
result.cata(
  /* 
   * outputs: "Failed because no game found in persistence"
                              OR
            "Failed because it is not player turn"
                              OR
          "Failed because unable to write gameState into persistence"
  */
  failure => `Failed because ${failure}`,
  success => `Great!. New state of the game after the operation ${success.gameState}`
);


Badaboom! Acabamos de agregar persistencia al workflow de nuestra aplicación modificando simplemente el punto de montaje de los workflows. Hemos tenido que tirar código nuevo claro, pero las modificaciones de lo existente se han mantenido al mínimo, centradas en un punto en concreto y con una única razón de peso para hacerlo.

Y la guinda del pastel es la inyección de dependencias. Si queremos cambiar la implementación de infraestructura utilizando un motor de persistencia distinto lo único que tenemos que hacer es cambiar la selección de las funciones que usa la orquestación:

//const gameReader = redisPersistence.getGameState.bind(redisPersistence); 
const gameReader = mongoPersistence.findGameState.bind(mongoPersistence);
//const gameWriter = redisPersistence.seGameState.bind(redisPersistence);
const gameWriter = mongoPersistence.updateGameState.bind(mongoPersistence);

Esto, señores, es SOLID; no es OOP fuertemente tipado pero es SOLID sin discursión alguna.

1 comentario:

  1. Como me han preguntado explicitamente y en persona sobre la gestion de la asincronia voy a dejar en este comentario el codigo para canalizar funciones sincronas y asincronas:

    const pipe = (…functions) => input => functions.reduce((chain, func) => chain.then(func), Promise.resolve(input));

    // Cada una de las funciones fn1, fn2, fn3 pueden ser sincronas o asincronas (retornando una promesa)
    pipe(fn1, fn2, fn3)(input).then(result => console.log(`Este es el resultado: ${result}`))

    Este codigo esta en su forma mas pura y bonita. Para nuesto caso habria que adaptarlo para nuestra necesidad de input incremental y la monada Either pero eso es trivial.

    ResponderEliminar