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, algunos son argumentos de línea de comandos (argv).

Entonces creé una clase ConfigBlock. Esta clase lee todas las fuentes de parámetros y las almacena en una estructura de datos apropiada. Los ejemplos son rutas y nombres de archivos que el usuario puede cambiar en el archivo de configuración, o el indicador --verbose CLI. Entonces, uno puede llamar ConfigBlock.GetVerboseLevel()para leer este parámetro específico.

Mi pregunta: ¿es una buena práctica recopilar todos esos 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 formas de lograr esto, pero no estoy seguro de cuál tomar. El constructor de una clase puede tener 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 .cpp de clases debe incluir y usar el material ConfigBlock.
El archivo .h no presenta esta interfaz al usuario de la clase. Sin embargo, la interfaz para 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 debe tener una conexión a mi ConfigBlock. Pero, ¿cómo debería ser esta conexión?

lugge86
fuente

Respuestas:

10

Soy bastante pragmático, pero mi principal preocupación aquí es que puedas permitir que esto ConfigBlockdomine 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 simplemente seleccionar estos foo/bar/bazcampos de forma masiva ConfigBlock.

Diseño de interfaz perezosa

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 necesitando algo nuevo, puede cargarlo en un ConfigBlock(posiblemente sin ningún cambio de código) y luego elija cualquier cosa nueva que necesite sin ningún tipo de cambio de interfaz, solo un cambio en la implementación de MyGreatClass.

Por lo tanto, es tanto una ventaja como una desventaja que esto le libere de diseñar una interfaz más cuidadosamente pensada que solo acepte entradas que realmente necesita. Aplica la mentalidad de "Solo dame esta gran cantidad de datos, seleccionaré lo que necesito de ella" en lugar de algo más como "Estos parámetros precisos son lo que esta interfaz necesita para funcionar".

Así que definitivamente hay algunos profesionales aquí, pero podrían ser muy compensados ​​por los contras.

Acoplamiento

En este escenario, todas estas clases que se construyen a partir de una ConfigBlockinstancia terminan teniendo sus dependencias de la siguiente manera:

ingrese la descripción de la imagen aquí

Esto puede convertirse en un PITA, por ejemplo, si desea realizar una prueba unitaria Class2en este diagrama de forma aislada. Es posible que tenga que simular superficialmente varias ConfigBlockentradas que contienen los campos relevantes que Class2le interesan para poder probarlo en una variedad de condiciones.

En cualquier tipo de contexto nuevo (ya sea prueba unitaria o proyecto completamente nuevo), cualquiera de esas clases puede llegar a ser más una carga de (re) uso, ya que terminamos teniendo que llevar siempre ConfigBlockel viaje y configurarlo en consecuencia.

Reusabilidad / Implementabilidad / Testabilidad

En cambio, si diseña estas interfaces de manera adecuada, podemos desacoplarlas ConfigBlocky terminar con algo como esto:

ingrese la descripción de la imagen aquí

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

Esto lleva 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 Clientcódigo 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 a ConfigBlocky 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 generalmente será nulo o cerrado de todos modos (podría ser la mainfunción de punto de entrada de su aplicación o algo así).

Entonces, desde un punto de vista de reutilización y prueba, puede ayudar a hacer 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 establecer explícitamente qué parámetros necesitan en lugar de solo uno masivo ConfigBlockque modele todo el universo de campos de datos necesarios para todo.

Conclusión

En general, este tipo de diseño orientado a clases que depende de un monolito que tiene todo lo necesario tiende a tener este tipo de características. Como resultado, su aplicabilidad, capacidad de implementación, reutilización, capacidad de prueba, etc. pueden degradarse significativamente. Sin embargo, pueden simplificar el diseño de la interfaz si intentamos darle un giro positivo. Depende de usted medir esos pros y contras y decidir si las compensaciones valen la pena. Por lo general, es mucho más seguro equivocarse contra este tipo de diseño en el que está seleccionando un monolito en clases que generalmente están destinadas a modelar un diseño más general y ampliamente aplicable.

Por último, si bien no menos importante:

extern CodingBlock MyCodingBlock;

... esto es potencialmente peor (¿más sesgado?) 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 una instancia específica de la misma. Eso degrada aún más la aplicabilidad / implementabilidad / comprobabilidad.

Mi consejo general sería equivocarse al diseñar 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ñe. Y evite el enfoque global sin inyección de dependencia si puede, a menos que realmente tenga una razón muy fuerte y segura para no evitarlo.

marstato
fuente
1

Por lo general, la configuración de una aplicación la consumen principalmente los objetos de fábrica. Cualquier objeto que se base en la configuración debe generarse a partir de uno de esos objetos de fábrica. Puede utilizar el Patrón de fábrica abstracto para implementar una clase que abarque todo el ConfigBlockobjeto. Esta clase expondría métodos públicos para devolver otros objetos de fábrica, y solo pasaría la parte de lo ConfigBlockrelevante a ese objeto de fábrica en particular. De esa manera, la configuración de configuración "gotea" desde el ConfigBlockobjeto a sus miembros, y desde la fábrica Factory a las fábricas.

Usaré C # ya que conozco mejor el lenguaje, 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, necesita algún tipo de clase que actúe como la "aplicación" que se instancia en su 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 última nota, esto se conoce como un "objeto proveedor" en .NET speak. Los objetos de proveedor en .NET parecen unir los datos de configuración con los objetos de fábrica, que es esencialmente lo que desea hacer aquí.

Ver también Patrón de proveedores para principiantes . Nuevamente, esto está orientado al desarrollo de .NET, pero con C # y C ++ siendo ambos lenguajes orientados a objetos, el patrón debería ser mayormente transferible entre los dos.

Otra buena lectura que está relacionada con este patrón: El modelo de proveedor .

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

Greg Burghardt
fuente
Todo está bien, excepto los enlaces a los modelos de proveedores. La reflexión no es compatible con c ++, y eso no funcionará.
Bћовић
@ BЈовић: Correcto. La reflexión de clase no existe, sin embargo, puede construir una solución manual, que básicamente se convierte en una switchdeclaración o una ifprueba de declaración contra un valor leído de los archivos de configuración.
Greg Burghardt
0

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

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

El constructor de una clase puede tener una referencia dada a mi ConfigBlock

Esto es malo: la mayoría de sus constructores no necesitarán la mayoría de los valores. En cambio, 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_blockhay una instancia de su ConfigBlockclase (la que contiene todos sus valores) y la MyGreatClassclase recibe una GreatClassDatainstancia. En otras palabras, solo pase a los constructores los datos que necesitan, y agregue facilidades ConfigBlockpara 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 .cpp de clases debe incluir y usar el material ConfigBlock. El archivo .h no presenta esta interfaz al usuario de la clase. Sin embargo, la interfaz para 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 haga eso: normalmente debe tener una instancia declarada globalmente, en cualquier punto de entrada que use su aplicación (función principal, DllMain, etc.) y pasarla como un argumento donde lo necesite (pero como se explicó anteriormente, no debe pasar toda la clase, solo exponga las interfaces alrededor de los datos y pase esas).

Además, no ate sus clases de cliente (su MyGreatClass) al tipo de CodingBlock; Esto significa que, si MyGreatClasstoma una cadena y cinco enteros, será mejor que pase esa cadena y los enteros, que pasará a CodingBlock.

utnapistim
fuente
Creo que es una buena idea desacoplar las fábricas de la configuración. No es satisfactorio que la implementación de la configuración sepa cómo instanciar componentes, ya que esto necesariamente da como resultado una dependencia bidireccional donde anteriormente solo existía una dependencia unidireccional. Esto tiene grandes implicaciones al extender su código, especialmente cuando se usan bibliotecas compartidas donde las interfaces realmente importan
Joel Cornett
0

Respuesta corta:

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

Dawid Pura
fuente
De esta manera puedo reunir el código del analizador (analizar la línea de comando y los archivos de configuración) en una ubicación central. Luego, cada clase puede seleccionar sus parámetros relevantes desde allí. ¿Cuál es un buen diseño en tu opinión?
lugge86
Tal vez lo escribí mal, quiero decir que tiene (y es una buena práctica) tener una abstracción general con todos los ajustes obtenidos de las variables de entorno / archivo de configuración, que podría ser su ConfigBlockclase. El punto aquí es no proporcionar todo, en este caso, el contexto del estado del sistema, solo los valores requeridos en particular para hacerlo.
Dawid Pura