Microcódigo en mi código

El micro-código corresponde tradicionalmente con la forma más “pura” de firmware, instrucciones al fin y al cabo, que controlan una máquina. Si no te suenan los términos, puede ser una lectura interesante si quieres conocer cómo funciona un procesador.

Pero no es de hardware de lo que quiero hablar, sino de una forma (curiosa u obvia, de ti depende) de escribir ciertas partes de nuestros programas, que toma la idea básica de las lógicas micro-programadas.


Generalizar o no generalizar una solución

Muchas veces cuando analizamos la mejor forma de cumplir los requisitos que se nos imponen, a los analistas se nos presenta una interesante encrucijada: hacer nuestro programa flexible y potente pero complejo de usar, o bien, hacerlo rígido e inflexible pero sencillo de usar.

La elección no siempre es fácil, haciéndolo flexible y potente, nos anticipamos a necesidades que, no estando en los requisitos, ya cubrimos antes incluso de que se den. Haciéndolo rígido e inflexible, un pequeño cambio en los requisitos, hará que tengamos que modificar parte del sistema.

Un ejemplo típico podría ser la forma en la que una empresa calcula los precios de sus productos. Contrastando visualmente las opciones mencionadas, tendríamos:

Obviamente desde el punto de vista del usuario, cabe la posibilidad de implementar la compleja y mostrar la rígida; si deciden que necesitan una nueva fórmula de calcular precios, añadiríamos (nosotros) la fórmula en algún sitio para que apareciera en el combo, un interface específico de “configuración avanzada”, etc… todo eso da igual, lo importante es que debemos decidir si generalizar la solución (capacidad para procesar fórmulas) o no (escribir en el código las cuatro fórmulas concretas) y el coste de hacer una u otra, no se parecen en nada.

No siempre ceñirse a los requisitos es la mejor opción, anticiparte a un requisito futuro, te permite cubrirlo a coste cero y hay muchas situaciones en que merece la pena valorar esa opción.

Reglas de negocio, complejas

La cosa se pone todavía más interesante si, además, las reglas de negocio que debemos codificar son complejas. Como contra-ejemplo, el caso de las fórmulas de cálculo de precio son sencillísimas. La forma de codificarlo dependerá mucho del contexto, pero por ejemplo:

Si en algún momento hace falta una nueva forma de calcular los precios, añadimos un elemento a la enumeración y una nueva fórmula en el selector.

Pero, ¿qué pasa si las reglas de negocio que debemos codificar son draconianas?, por supuesto seguimos pudiendo codificar “a pelo” las reglas en el código, pero por mucho cuidado que pongamos, mantener ese trozo de código va a ser de todo menos agradable, cualquier cambio en las reglas obligará al programador a dar un buen repaso al código escrito. Espagueti o no (bien classificado) leer este tipo de códigos es un lío (yo aquí prefiero un espagueti que no un follón de clases e interfaces). ¡Ah! y si no sabes lo que son las pruebas unitarias, aquí tienes una buena escusa para ponerlas en práctica ¡éste es el escenario perfecto para ellas!.

Micro-código y lógicas micro-programadas

La idea e implicaciones de micro-código (o lógica micro-programada) son muy interesantes y amplias, si no lo conoces te animo a leer sobre el tema. De forma muy tosca, se podría decir que cada instrucción de la arquitectura (mov, push, add, …) de un procesador, cuando se trata de una arquitectura micro-programada, es internamente un pequeño programa de unas instrucciones más básicas (todavía) que operan con la estructura íntima del procesador.

Por poner un ejemplo (impreciso pero que creo transmite la idea), muchas instrucciones llevan como argumento una dirección de memoria con la que hay que operar, así, un trozo de micro-código que llevan estas instrucciones es el mismo en todas, algo parecido a: “…, poner la dirección en el bus de direcciones, marcar el destino del bus de datos (eg. un registro), hacer fetch de carga de datos desde memoria, …”.

Nuestra intención entonces, es definir un conjunto de micro-instrucciones para dar solución fácilmente a una gran variedad de problemas similares dentro de un contexto específico. Es decir, en lugar de codificar directamente las reglas de negocio, vamos a codificar una micro-arquitectura, con ella, codificaremos las reglas de negocio.

Complejo de abstraer, complejo de codificar

Si las reglas son complejas, el código que debemos escribir si las escribimos directamente resultará feo y difícil de mantener. Además, al ser las reglas complejas, probablemente resulte también difícil establecer un esquema abstracto que permita resolver (aunque difícil de usar para el usuario) el problema. El ejemplo anterior era muy fácil de abstraer ¡poniendo un parser de ecuaciones!.

Es decir, estamos justo justo, en ese punto realmente incómodo en donde el analista se revuelve entre las sábanas intentando tomar la mejor opción.

Déjame poner un ejemplo lo suficientemente sencillo (no te quejes de que es sencillo) para que este post no se vuelva infumable, pero lo suficientemente complejo (haz un pequeño esfuerzo por entenderlo) para que la mejor opción (o una de las mejores) sea precisamente la que propongo (¡eh! si conoces otra ¡compártela!).

El problema

Supón que vendemos productos, de los que tenemos una cantidad limitada que llamaremos <A>lmacén (3 tomates, 4 lechugas, etc…), pero por otro lado, tenemos diferentes tarifas, de las cuales ahora me da igual su precio, lo interesante es el hecho de que pueden acceder de diferente forma a las unidades que hay en el almacén. Es decir, cuando alguien usa una tarifa, podría tener disponibles más o menos unidades que las que hay en el almacén. Así, cada tarifa tiene asignado un número que llamaremos <C>antidad y que actúa de las siguientes formas:

  • Tarifa SEPARADA, da igual las unidades en A, únicamente están disponibles las unidades indicadas en C. Es útil si la empresa ha pactado con un cliente que siempre le servirá una cantidad fija de producto, ni más, ni menos, así estas unidades están separadas.

  • Tarifa LIMITADA, la tarifa vende unidades de A, pero como máximo podrá vender C unidades, no más. Es útil si la empresa pone unidades limitadas en oferta.

  • Tarifa EXTRA, la tarifa vende unidades de A, pero si ya no quedan en el almacén, entonces se pueden vender otras C unidades adicionales. Es útil si la empresa quiere dar exclusividad al cliente, asegurándole que, cuando ya nadie puede comprar unidades (el almacén está vacío) él podrá seguir comprando más.

  • Tarifa FIRST, el cliente primero consume C unidades que tiene reservadas, si se agotan y quedan en el almacén, aún puede comprar las de A.

  • Tarifa MÍNIMA, el cliente consume las de A, pero se le asegura que, aunque ya no haya unidades en el almacén, podrá comprar como mínimo C unidades. Si ha comprado C unidades, aún puede comprar del almacén, si hay.

¡Menudo follón!, las reglas parecen similares, pero cada una actúa de diferente forma. Además, si las codificamos directamente, hay una serie de procesos que son “casi iguales” en todas como: restar unidades de aquí, comparar las que quedan allá, etc… si tenemos cuidado, puede quedar un código “legible”, pero el problema es que, de un vistazo no se infiere cual es el esquema implementado y el código auxiliar que podamos escribir estará fuertemente acoplado a cada regla, etc… vaya, que si a uno le dan el código, tiene que estudiarlo para comprender qué hace cada parte. Es decir, dependemos de herramientas adicionales para asegurar la eficacia del código existente (pruebas unitarias, métricas del código, análisis de cobertura de código, etc…) lo cual, como premisa, es pésimo (es como no saber conducir y confiar que el airbag funcione…).

La solución, desacoplando la lógica empresarial

Voy a dividir el problema en dos, por un lado, definiremos una micro-arquitectura que, aparentemente, no tiene relación directa con ninguna de las reglas. La ventaja es que tendrá significado propio (por tanto cualquiera puede entenderla sin conocer el resto del código) y será sencilla, pues sólo nos hacen falta seis micro-instrucciones (cuando las reglas de negocio ya son cinco).

Fíjate en las siguientes micro-instrucciones:

  • “c”, extraer las unidades pendientes que pueda de C y resta de las pendientes (si se han solicitado 4 unidades y es C=2, quedará C=0 y pendientes otras 2, porque sólo hemos podido extraer de C 2 unidades).

  • “a”, extraer las unidades pendientes que pueda de A y resta de las pendientes. Es como la instrucción anterior, pero con las unidades del almacén.

  • “C”, extrae todas las unidades pendientes de C haya suficientes o no (y no quedarán pendientes). La cuestión es que no lo intenta, si nos piden 4 unidades y tenemos C=2, quedará C=-2.

  • “A”, extrae todas las unidades pendientes de A haya suficientes o no (y no quedarán pendientes). Como la anterior pero con el almacén.

  • “s”, salva la cantidad de unidades pendientes en memoria (sólo se puede anidar una vez, como una pila de tamaño 1). Esto es como un push.

  • “r”, restaura la cantidad de unidades pendientes de la memoria. Esto es como un pop.

¡Ya está!, no tenemos ni idea de para que pueden servir estas instrucciones, pero su funcionamiento es muy sencillo. Además, codificarlo es tan fácil que puede hacerse directamente, el léxico son ¡6 letras! y la gramática, bueno ¡no existe!. Como lenguaje desde luego, no puede ser más sencillo.

Un kata, podría ser implementar éste u otro “micro-lenguaje” en tu lenguaje preferido. Como el otro día me dijeron que recorrer una cadena en C era de viejos, aquí va mi kata que implementa el “micro-lenguaje” anterior:

¡Reconócelo! ¿a que pensabas que sería más largo? y eso que está en C. Aquí tienes el código completo listo para compilar.

Veamos un programa de ejemplo: “sCra”wow 4 letras!). ¿Qué hace? bueno, supongamos que se han pedido 4 unidades, que la cantidad de la tarifa es 2 y que en el almacén hay 3, entonces la instrucción “s” almacena el valor 4 en memoria, luego la instrucción “C” toma esas 4 unidades restándolas a la cantidad de la tarifa (que quedará en -2 unidades) quedando pendientes 0 unidades, pero luego viene “r” que restaura el 4 guardado previamente tras lo cual la instrucción “a” intenta extraer 4 unidades pendientes, pero como sólo puede 3, el almacén queda a 0 y pendientes 1.

En este último ejemplo, tenemos que la cantidad de la tarifa ha quedado a -2, indicador de que no hay unidades suficientes para esa tarifa, por otro lado las unidades pendientes han quedado a 1, otro indicado de que no hay unidades suficientes para esa tarifa. En general, si las unidades del almacén y/o cantidad de tarifa quedan en negativo o bien las unidades pendientes en positivo es que no hay unidades suficientes para esa tarifa.

Implementando la lógica empresarial

Ahora que tenemos nuestro propio lenguaje, podemos implementar las reglas de negocio. ¿Y son complejas?, míralo tú mismo (el código, va entre comillas dobles):

  • Tarifa SEPARADA, “C”: quitar unidades de la tarifa (si no puede, falla).

  • Tarifa LIMITADA, “sArC”: quitar de la tarifa (si no puede, falla) y del almacén (si no puede, falla).

  • Tarifa EXTRA, “aC”: quitar del almacén, si hace falta, de la tarifa (si no puede, falla).

  • Tarifa FIRST, “cA”: quitar de la tarifa, si hace falta, del almacén (si no puede, falla).

  • Tarifa MÍNIMA, “sarC”, quitamos del almacén las que podamos (si no puede, no falla) y luego quitamos todas de la tarifa (si no puede, falla).

En total, el código para codificar todas nuestras reglas iniciales ocupa: ¡13 caracteres!.

¿Y si cambian o añaden reglas?

Nuestro lenguaje está perfectamente definido, por tanto no hay confusión posible en cuanto lo que hace (y su implementación es trivial). Por otro lado, la codificación de las reglas es también trivial y tremendamente fácil de seguirlas (si conocemos nuestro micro-lenguaje).

Si modifican o cambian una regla, únicamente tenemos que ver si con las instrucciones de que disponemos podemos codificar esa regla (o cambio de). Si sí podemos, listo. Si no podemos, nos plantearíamos qué nuevas instrucciones tenemos que añadir a nuestra micro-arquitectura para poder cubrirla.

Conclusión

Hemos transformado un dolor de cabeza en unas pocas líneas de código muy fáciles de seguir y mantener. Además, somos capaces de cumplir los requisitos y adaptarnos fácilmente a cambios en los mismos. Por poder, podríamos permitir que el cliente usara nuestro micro-lenguaje (en una “configuración avanzada”, etc…).

Al menos por esta vez, podemos dormir tranquilos…

Portada de Genbeta