¿Es un solo objeto de configuración una mala idea?


43

En la mayoría de mis aplicaciones, tengo un objeto "config" singleton o estático, a cargo de leer varias configuraciones del disco. Casi todas las clases lo usan, para varios propósitos. Esencialmente es solo una tabla hash de pares de nombre / valor. Es de solo lectura, así que no me ha preocupado demasiado el hecho de que tengo tanto estado global. Pero ahora que estoy comenzando con las pruebas unitarias, está empezando a convertirse en un problema.

Un problema es que generalmente no desea probar con la misma configuración con la que se ejecuta. Hay un par de soluciones para esto:

  • Dele al objeto de configuración un setter que SOLO se usa para pruebas, para que pueda pasar en diferentes configuraciones.
  • Continúe usando un único objeto de configuración, pero cámbielo de un singleton a una instancia que pase por todas partes donde sea necesario. Luego puede construirlo una vez en su aplicación, y una vez en sus pruebas, con diferentes configuraciones.

Pero de cualquier manera, todavía te queda un segundo problema: casi cualquier clase puede usar el objeto de configuración. Entonces, en una prueba, debe configurar la configuración para la clase que se está probando, pero también TODAS sus dependencias. Esto puede hacer que su código de prueba sea feo.

Estoy empezando a llegar a la conclusión de que este tipo de objeto de configuración es una mala idea. ¿Qué piensas? ¿Cuáles son algunas alternativas? ¿Y cómo comienzas a refactorizar una aplicación que usa la configuración en todas partes?


La configuración obtiene su configuración leyendo un archivo en el disco, ¿verdad? Entonces, ¿por qué no simplemente tener un archivo "test.config" que pueda leer?
Anon

Eso resuelve el primer problema, pero no el segundo.
JW01

@ JW01: ¿Todas sus pruebas necesitan una configuración marcadamente diferente, entonces? Tendrás que configurar esa configuración en algún lugar durante tu prueba, ¿no?
Anon

Cierto. Pero si sigo usando un solo grupo de configuraciones (con un grupo diferente para las pruebas), entonces todas mis pruebas terminan usando la misma configuración. Y dado que es posible que desee probar el comportamiento con diferentes configuraciones, esto no es ideal.
JW01

"Entonces, en una prueba, debe configurar la configuración para la clase que se está probando, pero también TODAS sus dependencias. Esto puede hacer que su código de prueba sea feo". ¿Tienes un ejemplo? Tengo la sensación de que no es la estructura de toda la aplicación que se conecta a la clase de configuración, sino más bien la estructura de la clase de configuración lo que hace que las cosas sean "feas". ¿No debería configurar la configuración de una clase configurar automáticamente sus dependencias?
AndrewKS

Respuestas:


35

No tengo ningún problema con un solo objeto de configuración, y puedo ver la ventaja de mantener todas las configuraciones en un solo lugar.

Sin embargo, usar ese único objeto en todas partes dará como resultado un alto nivel de acoplamiento entre el objeto de configuración y las clases que lo usan. Si necesita cambiar la clase de configuración, puede que tenga que visitar todas las instancias en las que se usa y verificar que no haya roto nada.

Una forma de lidiar con esto es crear múltiples interfaces que expongan partes del objeto de configuración que son necesarias para diferentes partes de la aplicación. En lugar de permitir que otras clases accedan al objeto de configuración, pasa instancias de una interfaz a las clases que lo necesitan. De esa manera, las partes de la aplicación que usan configuración dependen de la colección más pequeña de campos en la interfaz en lugar de toda la clase de configuración. Finalmente, para las pruebas unitarias, puede crear una clase personalizada que implemente la interfaz.

Si desea explorar más estas ideas, le recomiendo leer sobre principios SÓLIDOS , particularmente el Principio de segregación de interfaz y el Principio de inversión de dependencia .


7

Separo grupos de configuraciones relacionadas con interfaces.

Algo como:

public interface INotificationEmailSettings {
   public string To { get; set; }
}

public interface IMediaFileSettings {
    public string BasePath { get; set; }
}

etc.

Ahora, para un entorno real, una sola clase implementará muchas de estas interfaces. Esa clase puede extraerse de un DB o la configuración de la aplicación o lo que sea que tenga, pero a menudo sabe cómo obtener la mayoría de las configuraciones.

Pero la segregación por interfaz hace que las dependencias reales sean mucho más explícitas y detalladas, y esta es una mejora obvia para la capacidad de prueba.

Pero ... no siempre quiero ser forzado a inyectar o proporcionar la configuración como una dependencia. Hay un argumento que podría hacerse que debería, por coherencia o explicidad. Pero en la vida real parece una restricción innecesaria.

Por lo tanto, utilizaré una clase estática como fachada, proporcionando una entrada fácil desde cualquier lugar a la configuración, y dentro de esa clase estática, ubicaré la implementación de la interfaz y obtendré la configuración.

Sé que la ubicación del servicio se aleja rápidamente de la mayoría, pero seamos sinceros, proporcionar una dependencia a través de un constructor es un peso, y a veces ese peso es más de lo que me gustaría soportar. Service Location es la solución para mantener la capacidad de prueba a través de la programación de interfaces y permitir múltiples implementaciones, a la vez que proporciona la conveniencia (en situaciones cuidadosamente medidas y de manera apropiada) de un punto de entrada estático singleton.

public static class AllSettings {
    public INotificationEmailSettings NotificationEmailSettings {
        get {
            return ServiceLocator.Get<INotificationEmailSettings>();
        }
    }
}

Creo que esta mezcla es la mejor de todos los mundos.


3

Sí, como se dio cuenta, un objeto de configuración global dificulta las pruebas unitarias. Tener un setter "secreto" para las pruebas unitarias es un truco rápido, que aunque no es agradable, puede ser muy útil: le permite comenzar a escribir pruebas unitarias, para que pueda refactorizar su código hacia un mejor diseño con el tiempo.

(Referencia obligatoria: Trabajar eficazmente con código heredado . Contiene este y muchos más trucos invaluables para probar el código heredado).

En mi experiencia, lo mejor es tener la menor dependencia posible de la configuración global. Sin embargo, lo que no es necesariamente cero, todo depende de las circunstancias. Tener unas pocas clases de "organizador" de alto nivel que accedan a la configuración global y transmitan las propiedades de configuración reales a los objetos que crean y usan pueden funcionar bien, siempre y cuando los organizadores solo hagan eso, por ejemplo, no contengan mucho código comprobable. Esto le permite probar la mayoría o la totalidad de la funcionalidad importante comprobable de la unidad de la aplicación, sin interrumpir por completo el esquema de configuración física existente.

Sin embargo, esto ya se está acercando a una solución de inyección de dependencia genuina, como Spring para Java. Si puede migrar a dicho marco, está bien. Pero en la vida real, y especialmente cuando se trata de una aplicación heredada, a menudo la refactorización lenta y meticulosa hacia la DI es el mejor compromiso posible.


Realmente me gustó su enfoque y no sugeriría que la idea de un setter se mantenga "secreta" o se considere un "truco rápido". Si acepta la postura de que las pruebas unitarias son un usuario / consumidor / parte importante de su aplicación, entonces tener test_atributos y métodos ya no es tan malo, ¿verdad? Estoy de acuerdo en que la solución genuina al problema es emplear un marco DI, pero en ejemplos como el suyo, cuando se trabaja con código heredado u otros casos simples, el código de prueba no alienante se aplica limpiamente.
Filip Dupanović

1
@kRON, estos son trucos inmensamente útiles y prácticos para hacer comprobable la unidad de código heredada, y también los estoy usando ampliamente. Sin embargo, idealmente, el código de producción no debe contener ninguna característica introducida únicamente con el propósito de probar. A la larga, aquí es donde estoy refactorizando.
Péter Török

2

En mi experiencia, en el mundo de Java, esto es lo que Spring significa. Crea y gestiona objetos (beans) en función de ciertas propiedades que se establecen en tiempo de ejecución y, de lo contrario, es transparente para su aplicación. Puedes ir con el archivo test.config que Anon. menciona o puede incluir algo de lógica en el archivo de configuración que Spring manejará para establecer propiedades basadas en alguna otra clave (por ejemplo, nombre de host o entorno).

Con respecto a su segundo problema, puede solucionarlo mediante una nueva arquitectura, pero no es tan grave como parece. Lo que esto significa en su caso es que, por ejemplo, no tendría un objeto de configuración global que usan otras clases. Simplemente configuraría esas otras clases a través de Spring y luego no tendría ningún objeto de configuración porque todo lo que necesitaba configuración lo logró a través de Spring, por lo que puede usar esas clases directamente en su código.


2

Esta pregunta es realmente sobre un problema más general que la configuración. Tan pronto como leí la palabra "singleton", pensé de inmediato en todos los problemas asociados con ese patrón, entre los cuales destaca la poca capacidad de prueba.

El patrón Singleton es "considerado dañino". Es decir, no siempre es lo incorrecto, pero generalmente lo es. Si alguna vez está considerando usar un patrón singleton para algo , deténgase a considerar:

  • ¿Alguna vez necesitarás subclasificarlo?
  • ¿Alguna vez necesitarás programar en una interfaz?
  • ¿Necesita ejecutar pruebas unitarias en su contra?
  • ¿Necesitarás modificarlo con frecuencia?
  • ¿Su aplicación está destinada a admitir múltiples plataformas / entornos de producción?
  • ¿Te preocupa un poco el uso de la memoria?

Si su respuesta es "sí" a alguna de estas (y probablemente a varias otras cosas en las que no pensé), entonces probablemente no quiera usar un singleton. Las configuraciones a menudo necesitan mucha más flexibilidad de la que puede proporcionar un singleton (o, para el caso, cualquier clase sin instancia).

Si desea casi todos los beneficios de un singleton sin nada de dolor, use un marco de inyección de dependencia como Spring o Castle o lo que esté disponible para su entorno. De esa forma, solo tiene que declararlo una vez y el contenedor proporcionará automáticamente una instancia a lo que lo necesite.


0

Una forma de manejar esto en C # es tener un objeto singleton que se bloquee durante la creación de la instancia e inicialice todos los datos a la vez para que los utilicen todos los objetos del cliente. Si este singleton puede manejar pares clave / valores, puede almacenar cualquier tipo de datos, incluidas claves complejas para su uso por muchos clientes y tipos de clientes diferentes. Si desea mantenerlo dinámico y cargar nuevos datos del cliente según sea necesario, puede verificar la existencia de una clave principal del cliente y, si falta, cargar los datos de ese cliente y agregarlos al diccionario principal. También es posible que el singleton de configuración primaria pueda contener conjuntos de datos del cliente donde los múltiplos del mismo tipo de cliente usan los mismos datos a los que se accede a través del singleton de configuración primaria. Gran parte de esta organización de objetos de configuración puede depender de cómo sus clientes de configuración necesitan acceder a esta información y si esa información es estática o dinámica. He utilizado configuraciones de clave / valor, así como API de objetos específicos, según lo que fuera necesario.

Uno de mis singletons de configuración puede recibir mensajes para recargar desde una base de datos. En este caso, cargo en una segunda colección de objetos y solo bloqueo la colección principal para intercambiar colecciones. Esto evita el bloqueo de lecturas en otros hilos.

Si se carga desde archivos de configuración, podría tener una jerarquía de archivos. La configuración en un archivo puede especificar cuál de los otros archivos cargar. He usado este mecanismo con servicios de C # Windows que tienen múltiples componentes opcionales, cada uno con su propia configuración. Los patrones de estructura de archivos fueron los mismos en todos los archivos, pero se cargaron o no según la configuración del archivo principal.


0

La clase que estás describiendo suena como un antipatrón de objeto de Dios, excepto con datos en lugar de funciones. Eso es un problema. Probablemente debería tener los datos de configuración leídos y almacenados en el objeto apropiado mientras solo lee los datos nuevamente si es necesario por alguna razón.

Además, está utilizando un Singleton por una razón que no es apropiada. Los Singletons solo deben usarse cuando la existencia de múltiples objetos creará un mal estado. Usar un Singleton en este caso es inapropiado ya que tener más de un lector de configuración no debería causar un estado de error de inmediato. Si tiene más de uno, probablemente lo esté haciendo mal, pero no es absolutamente necesario que solo tenga uno de un lector de configuración.

Finalmente, crear un estado global de este tipo es una violación de la encapsulación, ya que permite que más clases de las que necesita saber accedan a los datos directamente.


0

Creo que si hay muchas configuraciones, con "grupos" de otras relacionadas, tiene sentido dividirlas en interfaces separadas con implementaciones respectivas, luego inyectar esas interfaces donde sea necesario con DI. Obtiene capacidad de prueba, menor acoplamiento y SRP.

Joshua Flanagan de Los Techies me inspiró en este asunto; escribió un artículo sobre el asunto hace algún tiempo.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.