La solución es una aplicación ASP.NET Core que utiliza una caché de dos niveles para almacenar datos de productos. La caché se puede actualizar automáticamente cada 5 minutos y también manualmente a través de un comando específico. La arquitectura se basa en el uso de un solo endpoint para manejar diferentes tipos de solicitudes HTTP y comandos.
La clase TwoLevelCache
implementa una caché de dos niveles utilizando un diccionario concurrente para el caché local y Redis para el caché distribuido. Las funciones de obtención y almacenamiento de datos están desacopladas utilizando funciones de alto orden.
ConcurrentDictionary<TKey, TValue>
es una estructura de datos diseñada para permitir el acceso concurrente seguro desde múltiples hilos, manejando internamente la sincronización de datos. Se utiliza para el almacenamiento en caché en memoria, asegurando que las operaciones de lectura y escritura sean seguras en entornos multihilo.
Utilizamos currificación para la función BuildCommand para permitir una invocación secuencial de parámetros en diferentes momentos.
Se utilizan funciones de alto orden para componer comportamientos específicos de serialización, deserialización y obtención de datos. Esto permite que la lógica de la caché sea flexible y extensible.
En lugar de usar constructores o propiedades para la inyección de dependencias, se utilizan funciones de alto orden. Esto mejora la flexibilidad y facilita las pruebas.
La función PreBuild se utiliza para crear comandos a partir de datos deserializados. Está currificada para permitir la invocación secuencial de parámetros.
Se utiliza un único endpoint para despachar diferentes comandos según el método HTTP y otros parámetros de la solicitud. Cada comando encapsula una operación que se debe realizar.
La función BuildCommand está memoizada para optimizar el rendimiento al evitar cálculos repetitivos.
ConcurrentDictionary
en TwoLevelCache
actúa como una caché local singleton, manteniendo una única instancia de caché en memoria.
Los validadores (Validator
) son implementados como funciones delegadas, lo que permite definir diferentes estrategias de validación para diferentes campos y comandos. Este patrón facilita la adición de nuevos tipos de validadores sin modificar el código existente.
La clase ValidationSchema
sigue el patrón Builder, permitiendo la construcción de un esquema de validación mediante la adición de validadores de manera fluida (fluent interface). Este patrón facilita la creación y configuración de objetos complejos paso a paso.
La clase ValidationResult
y su método Combine implementan una forma de patrón Composite, donde múltiples resultados de validación se pueden combinar en un solo resultado, acumulando los mensajes de error. Esto permite que las validaciones sean compuestas de manera jerárquica.
Los métodos estáticos en Validations como Required
, OfType
, MinLength
y GreaterThan
actúan como métodos de fábrica para crear diferentes tipos de validadores. Esto permite crear instancias de validadores sin conocer los detalles específicos de su implementación.
Utilización de delegados (Validator
) para definir la firma de las funciones de validación. Esto permite pasar funciones como parámetros y almacenar colecciones de funciones, facilitando la modularidad y flexibilidad del código.
TwoLevelCache
actúa como un proxy para la caché distribuida, controlando el acceso a los datos almacenados en Redis y proporcionando un intermediario que maneja la lógica de caché.
La lógica de actualización de caché en TwoLevelCache
puede considerarse una forma de decorator, añadiendo funcionalidad adicional (caché local) a una funcionalidad existente (caché distribuida).
TwoLevelCache
implementa métodos con lógica fija (como GetAsync y SetAsync) que utilizan funciones inyectadas para personalizar partes específicas del algoritmo (como deserialize, serialize, factory, etc.).
Cada clase y función tiene una única responsabilidad, lo que mejora la mantenibilidad y la claridad del código.
El sistema está diseñado para ser extensible sin modificar el código existente, permitiendo agregar nuevos comandos y tipos de caché sin cambios significativos.
Las dependencias se inyectan utilizando funciones de alto orden, lo que desacopla las clases y mejora la testabilidad. Las dependencias como las funciones de serialización y deserialización, y los métodos para obtener y establecer valores en la caché distribuida, se inyectan en TwoLevelCache
como parámetros de función. Esto desacopla la implementación de la caché de las dependencias específicas, facilitando la prueba y la modificación.
La lógica de caché, la deserialización, la serialización y la creación de comandos están claramente separadas, facilitando la comprensión y el mantenimiento del código.
El principio FCIS (Functional Core, Imperative Shell) se basa en la separación de la lógica pura (funcional) del código imperativo (efectos secundarios, interacciones con el entorno). En esta solución, aplicamos FCIS y el uso de intérpretes para desacoplar aún más las responsabilidades, mejorando la mantenibilidad y testabilidad del sistema.
El núcleo funcional contiene toda la lógica de negocio, escrita de manera pura y sin efectos secundarios. Este núcleo es fácil de probar y razonar porque no depende del estado externo ni produce efectos secundarios.
La capa imperativa se encarga de los efectos secundarios y las interacciones con el entorno, como la I/O, llamadas a bases de datos, etc. Esta capa delega la lógica al núcleo funcional y maneja los resultados.
La función GetAsync
en TwoLevelCache
aplica el principio FCIS al desacoplar la lógica de la caché de dos niveles en componentes funcionales y partes imperativas. La lógica central de decisión se implementa en la función pura Decide.GetAsync, mientras que la cáscara imperativa en GetAsync maneja las interacciones con la caché y los efectos secundarios.
La función Decide.GetAsync
encapsula la lógica de decisión sobre dónde obtener el valor de la caché (local o distribuida) y qué hacer si no se encuentra. Esta función es pura y no tiene efectos secundarios, lo que facilita las pruebas sobre la lógica. Actúa como un intérprete para la lógica de caché. En lugar de realizar las acciones directamente, devuelve una decisión y un valor, permitiendo que la cáscara imperativa (GetAsync) interprete esa decisión y realice las acciones correspondientes.