Tiempo estimado de lectura:

Supongamos que tenemos una aplicación web multiusuario desarrollada en Python con MongoDB y que para cada usuario, además de información no estructurada, guardamos un documento de configuración en MongoDB. Ese documento se genera a través de una función que recoje información de diversas fuentes (otros objetos, por ejemplo). El documento final en Python será un diccionario con otros diccionarios o listas anidados en su interior.

Supongamos que, además, en algún sitio tenemos un almacén de registros relacionados con los usuarios y que cada registro representa un par clave - valor para guardar configuraciones de todo tipo.

Vamos a verlo con un ejemplo concreto.

 1 config = {
 2     "registry": {
 3         "modules": ["bin", "starred", "comment"],
 4         "types": {
 5             "bin": "BaseBin",
 6             "starred": "BaseStarred",
 7             "comment": "BaseComment"
 8         }
 9     },
10     "something": {
11         # ...
12     },
13     # ...
14 }

En este caso tenemos un objeto de configuración en el que se definen unos módulos y unos nombres de clase para cada uno de esos módulos. En principio es estándar y no debería cambiar, pero como hemos diseñado nuestro sistema para que sea superparametrizable, pues tenemos que permitir hacerlo. Nosotros usaremos nuestro almacén de registros clave/valor.

Si queremos modificar el nombre de una clase para el módulo starred para poner otra menos estándar, podríamos guardar una clave registry con el valor JSON modificado para esa clave:

 1 # config['registry']
 2 
 3 {
 4     "modules": ["bin", "starred", "comment"],
 5     "types": {
 6         "bin": "BaseBin",
 7         "starred": "PrettyStarred",
 8         "comment": "BaseComment"
 9     }
10 }

Ahora, podríamos modificar nuestro algoritmo para aplicar las claves de configuración que encontrásemos en nuestro almacén de pares de claves a la configuración por defecto. De esa forma, para las claves registry, something o cualquier otra de primer nivel tendríamos una forma sencilla de modificar la configuración por defecto.

Creo que ya veis por dónde van los tiros.

El problema

Pasan los meses y estamos encantados con la extensibilidad de nuestro sistema parametrizable, hasta que un cambio en el esquema de configuración hace que todas las personalizaciones antiguas comiencen a dar error.

Resulta que ahora hay un módulo nuevo y, sin él, nada funciona. La configuración por defecto ahora es algo así:

 1 config = {
 2     "registry": {
 3         "modules": ["bin", "starred", "comment", "wow"],
 4         "types": {
 5             "bin": "BaseBin",
 6             "starred": "BaseStarred",
 7             "comment": "BaseComment",
 8             "wow": "BaseWowClass"
 9         }
10     },
11     "something": {
12         # ...
13     },
14     # ...
15 }

Cuando le decimos que tiene que actualizar a dedo 500 usuarios nuestro becario amenaza con pegarse un tiro.

La solución

Sin abandonar nuestro almacén de claves, sería estupendo poder cambiar de forma atómica la configuración de un usuario.

Para el ejemplo anterior, podríamos haber tenido una clave registry.types.starred con valor PrettyStarred y punto. De esa forma, nuevas incorporaciones al esquema de configuración por defecto no provocarían problemas y nuestras nuevas claves se añadirían de forma transparente para completar la configuración del usuario. En cambio, si desapareciese el módulo starred de la lista de módulos, a lo sumo, tendríamos configuraciones con claves obsoletas, pero seguramente nunca referenciadas. Aun así, se podría hacer una consulta para eliminar todas las personalizaciones con la clave registry.types.starred. Fácil y limpio.

Por ejemplo, teniendo la configuración en una variable que comprendiese rutas como la clave registry.types.starred podríamos hacer:

1 config['registry.types.starred'] = 'PrettyStarred'

Una posible implementación

Tómate el siguiente código como pseudocódigo basado en Python. Las funciones dot y dot_json se encargan de generar un objeto que se comporte como un diccionario o una lista con soporte para rutas de puntos a partir de un objeto o de una cadena de texto en formato JSON respectivamente.

 1 from dotted.utils import dot, dot_json
 2 
 3 def merged_settings(user, config_obj, keys=['registry', 'something', ...]):
 4     config = dot(config_obj)
 5 
 6     for key in keys:
 7         key_custom_configs = user.settings_set\
 8             .filter(Q(parameter=key) | Q(parameter__startswith="%s." % key))\
 9             .order_by("parameter")\
10             .all()
11         for cfg in key_custom_configs:
12             config[cfg.parameter] = dot_json(cfg.value)
13 
14     return config.to_python()

La función merged_settings recibiría como parámetro el usuario, el objeto de configuración por defecto y las claves a personalizar, si es el caso.

Dentro del bucle principal se obtendría, para cada clave, todas las personalizaciones de esa clave, es decir, los pares clave - valor que comenzasen por la clave o por la clave más un punto (registry, registry.types, …). Ordenando por clave de forma ascendente nos aseguramos de que se aplican primero las claves más cortas (es decir, genéricas) y luego las más concretas.

Este patrón está basado en una necesidad real de un proyecto real, y me he decidido a compartirlo con vosotros por si a alguien más le resulta de utilidad. He creado la librería dotted que proporciona la implementación de la parte más complicada de todo este asunto.

Para instalarla podéis ejecutar:

$ pip install dotted

y si queréis más información sobre su uso y la implementación, la tenéis en su página de Github.

Blog Logo

Carlos Escribano

Desarrollador Web desde hace 10 años. Me gusta resolver problemas de forma ingeniosa. Saber más.

Artículos de desarrollo web en español

nettoys.es

Volver al Inicio