configuration - ¿Gestión de configuración idiomática en clojure?
configure configuration-management (3)
¿Cuál es una forma idiomática de manejar la configuración de la aplicación en clojure?
Hasta ahora uso este entorno:
;; config.clj
{:k1 "v1"
:k2 2}
;; core.clj
(defn config []
(let [content (slurp "config.clj")]
(binding [*read-eval* false]
(read-string content))))
(defn -main []
(let [config (config)]
...))
Que tiene muchas desventajas:
- La ruta a
config.clj
no siempre se resuelve correctamente - No hay una forma clara de estructurar secciones de configuración para bibliotecas / marcos usados
- No accesible globalmente (
@app/config
) (lo que, por supuesto, puede verse como una buena forma de estilo funcional, pero hace que el acceso a la configuración a través del archivo fuente sea tedioso.
Los proyectos de código abierto más grandes como storm parecen usar YAML en lugar de Clojure y hacen que la configuración sea accesible globalmente a través de un truco poco feo: (eval ``(def ~(symbol new-name) (. Config ~(symbol name)))).
He hecho un poco de esto durante el último mes para trabajar. Para los casos en los que no es aceptable pasar una configuración, entonces hemos usado un mapa de configuración global en un átomo. Al inicio de la aplicación, ¡la configuración var es swap!
Ed con la configuración cargada y después de eso queda solo. Esto funciona en la práctica porque es efectivamente inmutable durante la vida útil de la aplicación. Sin embargo, este enfoque puede no funcionar bien para las bibliotecas.
No estoy seguro de lo que quiere decir con "No hay una forma clara de estructurar las secciones de configuración para bibliotecas / marcos de trabajo usados". ¿Quieres que las bibliotecas tengan acceso a la configuración? En cualquier caso, creé una tubería de cargadores de configuración que se asigna a la función que configura la configuración en el inicio. Esto me permite separar la configuración según la biblioteca y la fuente.
No me molestaría ni siquiera mantener los mapas de configuración como recursos en un archivo separado (para cada entorno). Confijulate ( https://github.com/bbbates/confijulate , sí, este es un proyecto personal) le permite definir toda su configuración para cada entorno dentro de un único espacio de nombres y cambiar entre ellos a través de las propiedades del sistema. Pero si necesita cambiar los valores sobre la marcha sin reconstruir, Confijulate también le permitirá hacer eso.
Primero use clojure.edn y en particular clojure.edn / read. P.ej.
(use ''(clojure.java [io :as io]))
(defn from-edn
[fname]
(with-open [rdr (-> (io/resource fname)
io/reader
java.io.PushbackReader.)]
(clojure.edn/read rdr)))
Con respecto a la ruta de config.edn que usa io / resource es solo una forma de lidiar con esto. Como es probable que desee guardar un config.edn modificado durante el tiempo de ejecución, es posible que desee confiar en el hecho de que la ruta para los lectores y escritores de archivos se construyó con un nombre de archivo no calificado como
(io/reader "where-am-i.edn")
por defecto a
(System/getProperty "user.dir")
Teniendo en cuenta que es posible que desee cambiar la configuración durante el tiempo de ejecución, podría implementar un patrón como este (bosquejo aproximado)
;; myapp.userconfig
(def default-config {:k1 "v1"
:k2 2})
(def save-config (partial spit "config.edn"))
(def load-config #(from-edn "config.edn")) ;; see from-edn above
(let [cfg-state (atom (load-config))]
(add-watch cfg-state :cfg-state-watch
(fn [_ _ _ new-state]
(save-config new-state)))
(def get-userconfig #(deref cfg-state))
(def alter-userconfig! (partial swap! cfg-state))
(def reset-userconfig! #(reset! cfg-state default-config)))
Básicamente, este código envuelve un átomo que no es global y proporciona establecer y obtener acceso a él. Puedes leer su estado actual y alterarlo como átomos con algo. me gusta (alter-userconfig! assoc :k2 3)
. Para pruebas globales, puede restablecer! el userconfig y también inyecte varios userconfigs en su aplicación (alter-userconfig! (constantly {:k1 300, :k2 212}))
.
Las funciones que necesitan userconfig se pueden escribir como (defn do-sth [cfg arg1 arg2 arg3] ...) Y se prueban con varias configuraciones como default-userconfig, testconfig1,2,3 ... Funciones que manipulan el userconfig como en un panel de usuario usaría el comando get / alter ..! funciones
Además, lo anterior permite ajustar un reloj en userconfig que actualiza automáticamente el archivo .edn cada vez que se cambia userconfig. Si no quieres hacer esto, ¡puedes agregar un método de guardar-userconfig! Función que escupe el contenido de los átomos en config.edn. Sin embargo, es posible que desee crear una forma de agregar más relojes al átomo (como volver a renderizar la GUI después de que se haya cambiado un tamaño de fuente personalizado) que, en mi opinión, rompería el molde del patrón anterior.
En cambio, si estuviera tratando con una aplicación más grande, un mejor enfoque sería definir un protocolo (con funciones similares como en el bloque let) para userconfig e implementarlo con varios constructores para un archivo, una base de datos, un átomo (o lo que sea). necesidad de pruebas / diferentes escenarios de uso) utilizando reify o defrecord. Se podría pasar una instancia de esto en la aplicación y cada función / io que manipule el estado debería usarla en lugar de cualquier cosa global.