martes, 18 de noviembre de 2008

ConfiguratorPattern

OBJETIVO

En todos las aplicaciones hay funcionalidades básicas e imprescindibles que tienen su mayor defecto la dispersión (funcionalidades no agrupadas que se utilizan en todas partes, en todos los módulos funcionales): trazas, control de excepciones, configuración, multilenguaje...

En este artículo vamos a hacer hincapié en la configuración de una aplicación, que es la base de todo producto, y el mayor problema a la hora de desplegar en diferentes máquinas (quitando problemas de infraestructura) y de administrar versiones según el entorno de destino.

Vamos a intentar plantear un patrón de configuración (ConfiguratorPattern), basado en Java, utilizando buenas prácticas y la experiencia adquirida a lo largo de los años.

Como objetivos voy a intentar que se cumplan los siguientes:
  1. Acceso a cualquier variable de configuración de forma rápida y centralizada.
  2. Permitir el cacheo de la configuración en memoria, para un acceso más rápido.
  3. Permitir el refresco de dicha caché dado un tiempo determinado.
  4. Permitir la instalación y actualización de la aplicación de una manera rápida y lo más independiente del entorno posible.
  5. Impedir que cualquier cambio que se realice en el módulo de configuración pueda afectar al resto de código en el que se encuentra dispersa esta funcionalidad.
     - Estos objetivos implican reducción del acoplamiento y aumento de la cohesión interna del componente -

La configuración de cualquier aplicación basada en tecnología Java puede ser homogenea o heterogenea, es decir, puede estar basada en el mismo soporte (por ejemplo, un fichero xml) o en varios soportes de diferente naturaleza (ficheros de texto o properties, xml o una tabla en base de datos).

Echando a un lado aquellos módulos predefinidos que tienen una forma de generar su configuración de una manera predefinida (por ejemplo, log4j.properties o log4j.xml en trazas de log4j), se debe intentar que el resto de configuración propia del proyecto sea lo más homogenea posible, utilizando siempre el mismo soporte, esto quiere decir utilizar siempre ficheros de propiedades, un fichero xml o una tabla en base de datos.


IMPLEMENTACIÓN

Para cumplir los objetivos que nos hemos impuesto necesitamos una clase como la siguiente:

Datos de configuración obligatorios:
  • allowRefresh: true o false indicando si se debe autorefrescar la configuración
  • refreshTime: Número de minutos entre cada refresco (se actualizará cuando se llame la próxima vez a cualquier variable de configuración)

public final class Configurator {
 // ~ Campos estáticos / Constantes //
 // ------------------------------------------
 private static Map<String, Object> propertiesMap = new HashMap<>();
 private static long refreshTime = 60;
 // En minutos, por defecto una hora
 private static long lastRefreshTime = 0;
 private static boolean allowRefresh = false;

 static { // Se puede inicializar estaticamente o desde alguna clase o listener
  try {
   initialize();
  } catch (Exception ignored) {
  }
 }

 // ~ Métodos //
 // ----------------------------------------------------------------
 /**
  * * Comprueba si es necesario actualizar la configuracion *
  * 
  * @throws ConfigurationException
  */
 private static void refreshConfiguration() throws Exception {
  long currentTime = System.currentTimeMillis();
  if (allowRefresh) {
   if (currentTime > (refreshTime + lastRefreshTime)) {
    initialize();
   }
  }
 }

 /**
  * * Obtiene un subconjunto de propiedades dado un patrón de inicio Por *
  * ejemplo ('animales'): animales.leon animales.cabra animales.camaleon *
  * * @param key * @param pattern * * @return ArrayList * @throws
  * ConfigurationException
  */
 public static List getFilterKeySet(String key, String pattern) {
  List res = new ArrayList();
  try {
   refreshConfiguration();
   if (!pattern.endsWith(".")) {
    pattern = pattern + ".";
   }
   Set keys = ((Map) propertiesMap.get(key)).keySet();
   Iterator e = keys.iterator();
   while (e.hasNext()) {
    String akey = (String) e.next();
    if (akey.startsWith(pattern)) {
     res.add(akey);
    }
   }
  } catch (Exception e) {
   res = new ArrayList();
  }
  return res;
 }

 /**
  * Obtiene un parámetro de configuración del sistema como cadena *
  * @param group 
  * @param key 
  * @return String
  */
 public static String getPropertyAsString(String group, String key) {
  String value = null;
  refreshConfiguration();
  value = (String) ((Map) propertiesMap.get(group)).get(key);
  return value;
 }
 
 /**
  * Obtiene un parámetro de configuración del sistema como [TIPO], * , las
  * que se necesiten de este tipo u otro * * @param group * @param key *
  * @return [TIPO]
  */
 public static TIPO getPropertyAsTIPO(String group, String key){
  TIPO value = null;
  refreshConfiguration();
  value = new TIPO(((Map)propertiesMap.get(group)).get(key).toString());
  return value;
 }

 /** * Inicialización de la configuración de la aplicación */ 
 public static void initialize() throws Exception {
  try {    
   propertiesMap = new HashMap();    
   //CARGAR LOS DATOS DE CONFIGURACIÓN, desde la fuente de datos,    
   // y utilizando un mapa doble y agrupando por grupo    
   while (hayElementos) { 
    propertiesMap.add [....]   ;
   }    
   lastRefreshTime = System.currentTimeMillis();
  } catch (Exception e) {    
    System.out.println("Error al cargar la configuración de la aplicación: " + e);   
    throw e;  
  } 
 }
}


Se aconseja utilizar como soporte de la configuración una tabla en base de datos (la conexión se debe configurar por jndi utilizando la configuración del servidor de aplicaciones: pool de conexiones gestionado por contenedor), de este tipo:

CONFIGURACION (GRUPO, VARIABLE, VALOR, DESCRIPCION) FK (GRUPO, VARIABLE);

De esta forma, en cada entorno con su propia base de datos se podrá desplegar y redesplegar la aplicación las veces que se requieran sin tener que modificar la configuración en dicho entorno (únicamente la primera instalación y breves cambios). Esto viene muy bien si se trata de un producto en continuo desarrollo y se pueden tener diferentes versiones en diferentes clientes.

Hay muchos posibles variantes para este patrón:
  • Utilizando el patrón Factory para crear y obtener la instancia.
  • Utilizando el patrón Singleton en vez de una clase estática.
  • Añadiendo cuantas funciones se necesiten.
  • Cualquiera que no afecte al rendimiento ni la eficiencia de esta funcionalidad.
  • Cargando a la vez las variables allowRefresh y refreshTime desde configuración y actualizando.

Imágenes Dockers

Estoy aprendiendo bastante sobre el tema de docker y docker swarm , gestión de Stacks, Servicios, Containers, Volumenes, .. y sus configurac...