Gerenciamento de parâmetros no aplicativo OOP

15

Estou escrevendo um aplicativo OOP de tamanho médio em C ++ como uma maneira de praticar os princípios OOP.

Eu tenho várias classes no meu projeto, e algumas delas precisam acessar os parâmetros de configuração em tempo de execução. Esses parâmetros são lidos de várias fontes durante o início da aplicação. Alguns são lidos a partir de um arquivo de configuração no diretório inicial do usuário, alguns são argumentos de linha de comando (argv).

Então criei uma classe ConfigBlock . Esta classe lê todas as fontes de parâmetros e as armazena em uma estrutura de dados apropriada. Exemplos são nomes de caminho e de arquivo que podem ser alterados pelo usuário no arquivo de configuração ou o sinalizador CLI --verbose. Então, pode-se chamar ConfigBlock.GetVerboseLevel() para ler este parâmetro específico.

Minha pergunta: é uma boa prática coletar todos esses dados de configuração de tempo de execução em uma classe?

Então, minhas aulas precisam de acesso a todos esses parâmetros. Eu posso pensar em várias maneiras de conseguir isso, mas não tenho certeza qual delas tomar. Um construtor de classe pode ser uma referência ao meu ConfigBlock, como

public:
    MyGreatClass(ConfigBlock &config);

Ou eles apenas incluem um cabeçalho "CodingBlock.h", que contém uma definição do meu CodingBlock:

extern CodingBlock MyCodingBlock;

Em seguida, somente o arquivo .cpp de classes precisa incluir e usar o material do ConfigBlock.
O arquivo .h não introduz essa interface para o usuário da classe. No entanto, a interface para o ConfigBlock ainda está lá, no entanto, está oculta do arquivo .h.

É bom esconder isso dessa maneira?

Eu quero que a interface seja o menor possível, mas no final, eu acho que toda classe que precisa de parâmetros config tem que ter uma conexão com o meu ConfigBlock. Mas, como deve ser essa conexão?

    
por lugge86 14.12.2015 / 14:24
fonte

4 respostas

9

Sou bastante pragmático, mas minha principal preocupação aqui é que você pode permitir que esse ConfigBlock domine seus designs de interface de uma forma possivelmente ruim. Quando você tem algo assim:

explicit MyGreatClass(const ConfigBlock& config);

... uma interface mais apropriada pode ser assim:

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

... em vez de apenas escolher esses campos foo/bar/baz de uma% maciça deConfigBlock.

Design de interface preguiçoso

No lado positivo, esse tipo de design facilita a criação de uma interface estável para seu construtor, por exemplo, se você precisar de algo novo, basta carregar isso em ConfigBlock (possivelmente sem código mudanças) e, em seguida, selecione qualquer coisa nova que você precise sem qualquer tipo de mudança de interface, apenas uma mudança na implementação de MyGreatClass .

Portanto, é uma espécie de prós e contras que isso libera você de projetar uma interface mais cuidadosamente pensada que só aceita entradas de que realmente precisa. Aplica a mentalidade de, "Apenas me dê essa enorme bolha de dados, eu vou escolher o que eu preciso dela" ao invés de algo mais como, "Esses parâmetros precisos são o que essa interface precisa funcionar. "

Portanto, definitivamente existem alguns profissionais aqui, mas eles podem ser superados pelos contras.

Acoplamento

Nesse cenário, todas essas classes sendo construídas a partir de uma instância ConfigBlock acabam tendo suas dependências assim:

IssopodesetornarumPITA,porexemplo,sevocêquisertestaraunidadeClass2nestediagramaisoladamente.VocêpodeterquesimularsuperficialmenteváriosConfigBlockentradascontendooscamposrelevantesClass2estáinteressadoempodertestá-losobumavariedadedecondições.

Emqualquertipodenovocontexto(testedeunidadeouprojetototalmentenovo),qualquerdessasclassespodeacabarsetornandoumfardopara(re)usar,jáqueacabamostendoquesempretrazerConfigBlockparaomontareconfigurardeacordo.

Reutilização/Implantabilidade/testabilidade

Emvezdisso,sevocêprojetaressasinterfacesadequadamente,podemossepará-lasdeConfigBlockeacabarcomalgoparecidocomisto:

Se você notar neste diagrama acima, todas as classes se tornam independentes (seus acoplamentos aferentes / de saída reduzem em 1).

Isto leva a muito mais classes independentes (pelo menos independentes de ConfigBlock ), o que pode ser muito mais fácil de (re) usar / testar em novos cenários / projetos.

Agora, esse código Client acaba sendo o que precisa depender de tudo e reuni-lo. A carga acaba sendo transferida para esse código de cliente para ler os campos apropriados de um ConfigBlock e passá-los para as classes apropriadas como parâmetros. No entanto, esse código de cliente geralmente é projetado de forma restrita para um contexto específico, e seu potencial de reutilização normalmente será zilch ou fechar de qualquer maneira (pode ser a função de ponto de entrada main do aplicativo ou algo parecido).

Portanto, do ponto de vista de reutilização e teste, pode ajudar a tornar essas classes mais independentes. Do ponto de vista da interface para aqueles que usam suas classes, ele também pode ajudar a indicar explicitamente quais parâmetros eles precisam, em vez de apenas um ConfigBlock massivo que modela todo o universo de campos de dados necessários para tudo.

Conclusão

Em geral, esse tipo de design orientado a classes, que depende de um monolito que tem tudo o que é necessário, tende a ter esses tipos de características. Sua aplicabilidade, capacidade de implantação, capacidade de reutilização, capacidade de teste, etc. podem ser significativamente degradadas como resultado. No entanto, eles podem simplificar o design da interface, se tentarmos um giro positivo. Cabe a você avaliar esses prós e contras e decidir se os trade-offs valem a pena. Normalmente, é muito mais seguro errar contra esse tipo de design em que você está escolhendo um monolito em classes que geralmente têm como objetivo modelar um design mais geral e amplamente aplicável.

Por último, mas não menos importante:

extern CodingBlock MyCodingBlock;

... isso é potencialmente pior (mais inclinado?) em termos das características descritas acima do que a abordagem de injeção de dependência, pois acaba acoplando suas classes não apenas a ConfigBlocks , mas diretamente a um instância específica dele. Isso degrada ainda mais a aplicabilidade / capacidade de implantação / testabilidade.

Meu conselho geral seria errar ao projetar interfaces que não dependem desses tipos de monolitos para fornecer seus parâmetros, pelo menos para as classes mais aplicáveis em geral que você projeta. E evite a abordagem global sem injeção de dependência se puder, a menos que você realmente tenha uma razão muito strong e confiante para não evitá-la.

    
por 19.12.2015 / 20:22
fonte
1

Normalmente, a configuração de um aplicativo é consumida principalmente por objetos de fábrica. Qualquer objeto que depende da configuração deve ser gerado a partir de um desses objetos de fábrica. Você pode utilizar o Abstract Factory Pattern para implementar uma classe que considere todo o objeto ConfigBlock . Essa classe exporia métodos públicos para retornar outros objetos de fábrica e passaria apenas a parte do ConfigBlock relevante para esse objeto de fábrica específico. Dessa forma, as configurações de configuração "gotejam" do objeto ConfigBlock para seus membros e da fábrica da Fábrica para as fábricas.

Usarei C # desde que conheço melhor a linguagem, mas isso deve ser facilmente transferível para 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);
    }
}

Depois disso, você precisa de um tipo de classe que atue como o "aplicativo" que é instanciado no procedimento 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 uma última nota, isso é conhecido como "objeto de provedor" no .NET. Objetos de provedor no .NET parecem casar dados de configuração com objetos de fábrica, o que é essencialmente o que você quer fazer aqui.

Veja também Provider Pattern for Beginners . Novamente, isso é voltado para o desenvolvimento .NET, mas com C # e C ++ sendo ambos orientados a objetos, o padrão deve ser transferido entre os dois.

Outra boa leitura relacionada a esse padrão: O modelo do provedor .

Por fim, uma crítica a esse padrão: Provedor não é um padrão

    
por 14.12.2015 / 15:23
fonte
0

First question: is it good practise to collect all such runtime config data in one class?

Sim. É melhor centralizar constantes e valores de tempo de execução e o código para lê-los.

A class' constructor can be a given a reference to my ConfigBlock

Isso é ruim: a maioria de seus construtores não precisará da maioria dos valores. Em vez disso, crie interfaces para tudo que não é trivial para construir:

código antigo (sua proposta):

MyGreatClass(ConfigBlock &config);

novo código:

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

instanciar uma MyGreatClass:

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

Aqui, current_config_block é uma instância da sua classe ConfigBlock (aquela que contém todos os seus valores) e a classe MyGreatClass recebe uma instância GreatClassData . Em outras palavras, apenas passe para os construtores os dados de que eles precisam e adicione recursos ao seu ConfigBlock para criar esses dados.

Or they just include a header "CodingBlock.h" which contains a definition of my CodingBlock:

 extern CodingBlock MyCodingBlock;

Then, only the classes .cpp file needs to include and use the ConfigBlock stuff. The .h file does not introduce this interface to the user of the class. However, the interface to ConfigBlock is still there, however, it's hidden from the .h file. Is it good to hide it this way?

Esse código sugere que você terá uma instância global do CodingBlock. Não faça isso: normalmente você deve ter uma instância declarada globalmente, em qualquer ponto de entrada que seu aplicativo usa (função principal, DllMain, etc) e passar isso como um argumento onde você precisar (mas como explicado acima, você não deve passar toda a classe ao redor, apenas exponha interfaces em torno dos dados e passe-os).

Além disso, não vincule suas classes de clientes (seu MyGreatClass ) ao tipo de CodingBlock ; Isso significa que, se o seu MyGreatClass receber uma string e cinco inteiros, será melhor passar essa string e números inteiros, do que você passará em CodingBlock .

    
por 14.12.2015 / 15:41
fonte
0

Resposta curta:

Você não precisa de todas as configurações para cada um dos módulos / classes em seu código. Se você fizer isso, então há algo errado com seu design orientado a objetos. Especialmente no caso de testes unitários, definir todas as variáveis que você não precisa e passar esse objeto não ajudaria na leitura ou manutenção.

    
por 14.12.2015 / 16:17
fonte