Gestión de parámetros en la aplicación OOP

15

Estoy escribiendo una aplicación OOP de tamaño mediano en C ++ como una forma de practicar los principios OOP.

Tengo varias clases en mi proyecto, y algunas de ellas necesitan acceder a los parámetros de configuración en tiempo de ejecución. Estos parámetros se leen desde varias fuentes durante el inicio de la aplicación. Algunos se leen desde un archivo de configuración en el directorio de inicio de los usuarios, otros son argumentos de línea de comandos (argv).

Así que creé una clase ConfigBlock . Esta clase lee todas las fuentes de parámetros y las almacena en una estructura de datos apropiada. Algunos ejemplos son los nombres de ruta y de archivo que pueden ser cambiados por el usuario en el archivo de configuración, o el indicador de CLI --verbose. Luego, se puede llamar a ConfigBlock.GetVerboseLevel() para leer este parámetro específico.

Mi pregunta: ¿es una buena práctica recopilar todos estos datos de configuración de tiempo de ejecución en una clase?

Entonces, mis clases necesitan acceso a todos estos parámetros. Puedo pensar en varias maneras de lograr esto, pero no estoy seguro de cuál tomar. Un constructor de clase puede ser una referencia dada a mi ConfigBlock, como

public:
    MyGreatClass(ConfigBlock &config);

O simplemente incluyen un encabezado "CodingBlock.h" que contiene una definición de mi CodingBlock:

extern CodingBlock MyCodingBlock;

Entonces, solo el archivo de clases .cpp debe incluir y usar las cosas de ConfigBlock.
El archivo .h no introduce esta interfaz al usuario de la clase. Sin embargo, la interfaz de ConfigBlock todavía está allí, sin embargo, está oculta del archivo .h.

¿Es bueno ocultarlo de esta manera?

Quiero que la interfaz sea lo más pequeña posible, pero al final, creo que cada clase que necesita parámetros de configuración tiene que estar conectada a mi ConfigBlock. Pero, ¿cómo debería ser esta conexión?

    
pregunta lugge86 14.12.2015 - 14:24
fuente

4 respuestas

9

Soy bastante pragmático, pero mi principal preocupación aquí es que podrías permitir que ConfigBlock domine tus diseños de interfaz de una manera posiblemente mala. Cuando tienes algo como esto:

explicit MyGreatClass(const ConfigBlock& config);

... una interfaz más apropiada podría ser así:

MyGreatClass(int foo, float bar, const string& baz);

... en lugar de solo seleccionar estos campos foo/bar/baz de un ConfigBlock masivo.

Diseño de interfaz perezoso

En el lado positivo, este tipo de diseño facilita el diseño de una interfaz estable para su constructor, por ejemplo, ya que si termina por necesitar algo nuevo, simplemente puede cargarlo en un ConfigBlock (posiblemente sin ningún código cambios) y luego elija lo nuevo que necesita sin ningún tipo de cambio de interfaz, solo un cambio en la implementación de MyGreatClass .

Por lo tanto, es tanto una especie de profesional como de estafa que te libera de diseñar una interfaz mejor pensada que solo acepta las entradas que realmente necesita. Aplica la mentalidad de, "Solo dame esta gran cantidad de datos, seleccionaré lo que necesito" a diferencia de algo más como, "Estos parámetros precisos son los que esta interfaz debe funcionar ".

Definitivamente, hay algunos pros aquí, pero los contras pueden superarlos considerablemente.

Acoplamiento

En este escenario, todas las clases que se están construyendo a partir de una instancia ConfigBlock terminan teniendo sus dependencias así:

EstopuedeconvertirseenunPITA,porejemplo,sidesearealizarunapruebaunitariadeClass2enestediagramadeformaaislada.EsposiblequetengaquesimularsuperficialmentevariasentradasdeConfigBlockquecontienenloscamposrelevantesqueClass2estáinteresadoenpoderprobarlobajounavariedaddecondiciones.

Encualquiertipodecontextonuevo(yaseapruebadeunidadoproyectocompletamentenuevo),cualquierclasedeestetipopuedeterminarsiendomásunacargapara(re)utilizar,yaquesiempretenemosquellevarConfigBlockalolargoparaelmontar,yconfigurarloenconsecuencia.

Reusability/Deployability/Testability

Encambio,sidiseñasestasinterfacesdemaneraapropiada,podemosdesacoplarlasdeConfigBlockyterminarconalgocomoesto:

Si observa en este diagrama anterior, todas las clases se vuelven independientes (sus acoplamientos aferentes / salientes se reducen en 1).

Esto conduce a muchas más clases independientes (al menos independientes de ConfigBlock ) que pueden ser mucho más fáciles de (re) usar / probar en nuevos escenarios / proyectos.

Ahora, este código Client termina siendo el que tiene que depender de todo y ensamblarlo todo junto. La carga termina siendo transferida a este código de cliente para leer los campos apropiados de un ConfigBlock y pasarlos a las clases apropiadas como parámetros. Sin embargo, dicho código de cliente generalmente está diseñado de manera limitada para un contexto específico, y su potencial de reutilización normalmente será de cero a cero o de todos modos (podría ser la función de punto de entrada main de su aplicación o algo así).

Desde el punto de vista de la reutilización y las pruebas, puede ayudar a que estas clases sean más independientes. Desde el punto de vista de la interfaz para aquellos que usan sus clases, también puede ayudar a establecer explícitamente qué parámetros necesitan en lugar de un solo ConfigBlock masivo que modela todo el universo de campos de datos necesarios para todo.

Conclusión

En general, este tipo de diseño orientado a la clase que depende de un monolito que tiene todo lo necesario tiende a tener este tipo de características. Su aplicabilidad, implementabilidad, reutilización, probabilidad, etc. pueden degradarse significativamente como resultado. Sin embargo, pueden simplificar el diseño de la interfaz si intentamos darle un giro positivo. Depende de usted medir esos pros y sus contras y decidir si las compensaciones valen la pena. Por lo general, es mucho más seguro cometer errores en este tipo de diseño en el que está escogiendo a un monolito en clases que generalmente están diseñadas para modelar un diseño más general y de amplia aplicación.

Por último, pero no menos importante:

  

extern CodingBlock MyCodingBlock;

... esto es potencialmente incluso peor (¿más inclinado?) en términos de las características descritas anteriormente que el enfoque de inyección de dependencia, ya que termina acoplando sus clases no solo a ConfigBlocks , sino directamente a un instancia específica de ella. Eso degrada aún más la aplicabilidad / despliegue / testabilidad.

Mi consejo general sería errar en el diseño de interfaces que no dependen de este tipo de monolitos para proporcionar sus parámetros, al menos para las clases más generalmente aplicables que diseñas. Y evite el enfoque global sin inyección de dependencia si puede, a menos que tenga una razón muy sólida y segura para no evitarlo.

    
respondido por el user204677 19.12.2015 - 20:22
fuente
1

Por lo general, la configuración de una aplicación se consume principalmente por los objetos de fábrica. Cualquier objeto basado en la configuración debe generarse a partir de uno de esos objetos de fábrica. Puede utilizar el Abstract Factory Pattern para implementar una clase que incluya todo el objeto ConfigBlock . Esta clase expondría métodos públicos para devolver otros objetos de fábrica, y solo pasaría a la parte del ConfigBlock relevante para ese objeto de fábrica en particular. De esa manera, los ajustes de configuración "se filtran" desde el objeto ConfigBlock a sus miembros, y desde la fábrica de fábrica a las fábricas.

Usaré C # ya que conozco mejor el idioma, pero esto debería ser fácilmente transferible a C ++.

public class ConfigBlock
{
    public ConfigBlock()
    {
        // Load config data and
        // connectionSettings = new ConnectionConfig();
        // connectionSettings...
    }

    private ConnectionConfig connectionSettings;

    public ConnectionConfig GetConnectionSettings()
    {
        return connectionSettings;
    }
}

public class FactoryProvider
{
    public FactoryProvider(ConfigBlock config)
    {
        this.config = config;
    }

    private ConfigBlock config;

    public ConnectionFactory GetConnectionFactory()
    {
        ConnectionConfig connectionSettings = config.GetConnectionSettings();

        return new ConnectionFactory(connectionSettings);
    }
}

public class ConnectionFactory
{
    public ConnectionFactory(ConnectionConfig settings)
    {
        this.settings = settings;
    }

    private ConnectionConfig settings;

    public Connection GetConnection()
    {
        return new Connection(settings.Hostname, settings.Port, settings.Username, settings.Password);
    }
}

Después de eso, necesitas algún tipo de clase que actúe como la "aplicación" que se ejemplifica en tu procedimiento principal:

// Your main procedure (yeah I'm bending the rules of C# a tad here,
// but you get the point).
int Main(string[] args)
{
    Application app = new Application();

    app.Run();
}

public class Application
{
    public Application()
    {
        config = new ConfigBlock();
        factoryProvider = new FactoryProvider(config);
    }

    private ConfigBlock config;
    private FactoryProvider factoryProvider;

    public void Run()
    {
        ConnectionFactory connections = factoryProvider.GetConnectionFactory();
        Connection connection = connections.GetConnection();

        connection.Connect();

        // Enter into your main loop and do what this program is meant to do
    }
}

Como una última nota, esto se conoce como "objeto proveedor" en .NET. Los objetos del proveedor en .NET parecen casar los datos de configuración con los objetos de fábrica, que es esencialmente lo que quiere hacer aquí.

Vea también Patrón del proveedor para principiantes . Nuevamente, esto está orientado hacia el desarrollo de .NET, pero dado que C # y C ++ son lenguajes orientados a objetos, el patrón debería ser transferible entre los dos.

Otra buena lectura relacionada con este patrón: The Provider Model .

Por último, una crítica de este patrón: El proveedor no es un patrón

    
respondido por el Greg Burghardt 14.12.2015 - 15:23
fuente
0
  

Primera pregunta: ¿es una buena práctica recopilar todos estos datos de configuración de tiempo de ejecución en una clase?

Sí. Es mejor centralizar las constantes y valores del tiempo de ejecución y el código para leerlos.

  

Un constructor de clase puede ser una referencia dada a mi ConfigBlock

Esto es malo: la mayoría de los constructores no necesitarán la mayoría de los valores. En su lugar, cree interfaces para todo lo que no es trivial de construir:

código antiguo (su propuesta):

MyGreatClass(ConfigBlock &config);

nuevo código:

struct GreatClassData {/*...*/}; // initialization data for MyGreatClass
GreatClassData ConfigBlock::great_class_values();

crear una instancia de MyGreatClass:

auto x = MyGreatClass{ current_config_block.great_class_values() };

Aquí, current_config_block es una instancia de su clase ConfigBlock (la que contiene todos sus valores) y la clase MyGreatClass recibe una instancia GreatClassData . En otras palabras, solo pase a los constructores los datos que necesitan, y agregue facilidades a su ConfigBlock para crear esos datos.

  

O simplemente incluyen un encabezado "CodingBlock.h" que contiene una definición de mi CodingBlock:

 extern CodingBlock MyCodingBlock;
     

Entonces, solo el archivo de clases .cpp debe incluir y usar las cosas de ConfigBlock. El archivo .h no introduce esta interfaz al usuario de la clase. Sin embargo, la interfaz de ConfigBlock todavía está allí, sin embargo, está oculta del archivo .h. ¿Es bueno ocultarlo de esta manera?

Este código sugiere que tendrá una instancia global de CodingBlock. No hagas eso: normalmente debes tener una instancia declarada globalmente, en cualquier punto de entrada que use tu aplicación (función principal, DllMain, etc.) y pasarla como un argumento donde sea necesario (pero como se explicó anteriormente, no debes pasar) toda la clase, solo exponga las interfaces alrededor de los datos, y pase esos).

Además, no vincules tus clases de clientes (tu MyGreatClass ) al tipo de CodingBlock ; Esto significa que, si su MyGreatClass toma una cadena y cinco enteros, será mejor que pase esa cadena y los enteros, que lo que pasará en un CodingBlock .

    
respondido por el utnapistim 14.12.2015 - 15:41
fuente
0

Respuesta corta:

Usted no necesita todos los ajustes para cada uno de los módulos / clases en su código. Si lo hace, entonces hay algo mal con su diseño orientado a objetos. Especialmente en el caso de que las pruebas unitarias configuren todas las variables que no necesita y que pasar ese objeto no ayudaría con la lectura o el mantenimiento.

    
respondido por el Dawid Pura 14.12.2015 - 16:17
fuente

Lea otras preguntas en las etiquetas