Gerenciamento de parâmetros no aplicativo OOP

15

Estou escrevendo um aplicativo OOP de tamanho médio em C ++ como uma maneira de praticar princípios de 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 a inicialização do aplicativo. Alguns são lidos a partir de um arquivo de configuração no diretório home do usuário, outros são argumentos de linha de comando (argv).

Então eu criei uma classe ConfigBlock. Essa classe lê todas as fontes de parâmetros e as armazena em uma estrutura de dados apropriada. Os exemplos são nomes de caminho e arquivo que podem ser alterados pelo usuário no arquivo de configuração ou no sinalizador --verbose CLI. Então, pode-se chamarConfigBlock.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 classes precisam acessar todos esses parâmetros. Posso pensar em várias maneiras de conseguir isso, mas não tenho certeza de qual delas tomar. O construtor de uma classe pode receber 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, apenas o arquivo .cpp da classe precisa incluir e usar o material do ConfigBlock.
O arquivo .h não apresenta essa interface para o usuário da classe. No entanto, a interface para o ConfigBlock ainda está lá, no entanto, está oculta no arquivo .h.

É bom esconder dessa maneira?

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

lugge86
fonte

Respostas:

10

Sou bastante pragmático, mas minha principal preocupação aqui é que você possa permitir que isso ConfigBlockdomine seus designs de interface de uma maneira possivelmente ruim. Quando você tem algo parecido com isto:

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 foo/bar/bazcampos de uma forma massivaConfigBlock .

Design de Interface Preguiçoso

No lado positivo, esse tipo de design facilita o design de uma interface estável para o construtor, por exemplo, se você precisar de algo novo, basta carregá-lo em um ConfigBlock(possivelmente sem nenhuma alteração no código) e, em seguida, escolha qualquer coisa nova que você precise sem nenhum tipo de alteração na interface, apenas uma alteração na implementação do MyGreatClass.

Portanto, é um tanto prós e contras que isso o libera de projetar uma interface mais cuidadosa que aceita apenas as entradas de que realmente precisa. Ele aplica a mentalidade de "Apenas me dê esse enorme blob de dados, selecionarei o que preciso deles" em oposição a algo mais como preciso deles "Esses parâmetros precisos são o que essa interface precisa para funcionar".

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 ConfigBlockinstância acabam tendo suas dependências da seguinte maneira:

insira a descrição da imagem aqui

Isso pode se tornar uma PITA, por exemplo, se você quiser testar a unidade Class2neste diagrama isoladamente. Pode ser necessário simular superficialmente várias ConfigBlockentradas que contêm os campos relevantesClass2 está interessado para poder testá-lo sob uma variedade de condições.

Em qualquer tipo de novo contexto (seja teste de unidade ou projeto totalmente novo), essas classes podem acabar se tornando mais um fardo para (re) usar, pois acabamos sempre trazendo ConfigBlockconsigo o passeio e configurando-o adequadamente.

Reutilização / Implantação / Testabilidade

Em vez disso, se você projetar essas interfaces adequadamente, podemos dissociá-las ConfigBlocke terminar com algo assim:

insira a descrição da imagem aqui

Se você observar neste diagrama acima, todas as classes se tornam independentes (seus acoplamentos aferentes / de saída são reduzidos em 1).

Isso leva a muito mais classes independentes (pelo menos independentes de ConfigBlock ), que podem ser muito mais fáceis de (re) usar / testar em novos cenários / projetos.

Agora, esse Clientcódigo acaba sendo aquele que precisa depender de tudo e montar tudo junto. A carga acaba sendo transferida para esse código do cliente para ler os campos apropriados de a ConfigBlocke passá-los para as classes apropriadas como parâmetros. No entanto, esse código do cliente geralmente é projetado de maneira restrita para um contexto específico, e seu potencial de reutilização normalmente será fechado ou fechado de qualquer maneira (pode ser a mainfunção do ponto de entrada 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, também pode ajudar a declarar explicitamente quais parâmetros eles precisam, em vez de apenas um maciço ConfigBlockque modela todo o universo de campos de dados necessários para tudo.

Conclusão

Em geral, esse tipo de design orientado a classe que depende de um monólito que possui tudo o que é necessário tende a ter esses tipos de características. Sua aplicabilidade, implantação, reutilização, testabilidade etc. podem ficar significativamente degradadas como resultado. No entanto, eles podem simplificar o design da interface se tentarmos dar uma olhada positiva nela. Cabe a você medir esses prós e contras e decidir se as compensações valem a pena. Normalmente, é muito mais seguro errar nesse tipo de design em que você escolhe um monólito em classes que geralmente se destinam a modelar um design mais geral e amplamente aplicável.

Por último mas não menos importante:

extern CodingBlock MyCodingBlock;

... isso é potencialmente ainda pior (mais distorcido?) 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 ConfigBlocks, mas diretamente a uma instância específica . Isso degrada ainda mais a aplicabilidade / implantação / testabilidade.

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

marstato
fonte
1

Geralmente, 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 absorve todo o ConfigBlockobjeto. Essa classe exporia métodos públicos para retornar outros objetos de fábrica e passaria apenas a parte do ConfigBlockrelevante para esse objeto de fábrica específico. Dessa forma, as definições de configuração "escorrem" do ConfigBlockobjeto para seus membros e da fábrica para as fábricas.

Usarei C # desde que conheça 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 algum tipo de classe que atue como o "aplicativo" instanciado no seu 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 última observação, isso é conhecido como um "objeto provedor" no .NET. Os objetos de provedor no .NET parecem combinar dados de configuração com objetos de fábrica, que é essencialmente o que você deseja fazer aqui.

Consulte também Padrão do provedor para iniciantes . Novamente, isso é voltado para o desenvolvimento .NET, mas como C # e C ++ são linguagens orientadas a objetos, o padrão deve ser transferível principalmente entre os dois.

Outra boa leitura que está relacionada a esse padrão: o modelo de provedor .

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

Greg Burghardt
fonte
Tudo está bom, exceto os links para os modelos de provedores. A reflexão não é suportada pelo c ++ e isso não funcionará.
BЈовић
@ Bћовић: Correto. A reflexão de classe não existe, no entanto, você pode criar uma solução alternativa manual, que basicamente se resume a uma switchinstrução ou ifteste de instrução em relação a um valor lido nos arquivos de configuração.
Greg Burghardt
0

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

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

O construtor de uma classe pode receber uma referência ao meu ConfigBlock

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

código antigo (sua proposta):

MyGreatClass(ConfigBlock &config);

novo Código:

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

instanciar um MyGreatClass:

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

Aqui current_config_blockestá uma instância da sua ConfigBlockclasse (a que contém todos os seus valores) e a MyGreatClassclasse recebe uma GreatClassDatainstância. Em outras palavras, passe apenas para os construtores os dados de que precisam e adicione recursos ao seu ConfigBlockpara criar esses dados.

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

 extern CodingBlock MyCodingBlock;

Em seguida, apenas o arquivo .cpp da classe precisa incluir e usar o material do ConfigBlock. O arquivo .h não apresenta essa interface para o usuário da classe. No entanto, a interface para o ConfigBlock ainda está lá, no entanto, está oculta no arquivo .h. É bom esconder dessa maneira?

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

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

utnapistim
fonte
Eu acho que é uma boa idéia desacoplar fábricas da configuração. Não é satisfatório que a implementação da configuração saiba instanciar componentes, pois isso necessariamente resulta em uma dependência bidirecional, onde anteriormente existia apenas uma dependência unidirecional. Isto tem grandes implicações ao estender seu código, especialmente quando se utiliza bibliotecas compartilhadas, onde as interfaces realmente importa
Joel Cornett
0

Resposta curta:

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

Dawid Pura
fonte
Dessa forma, eu posso reunir o código do analisador (analisar a linha de comando e os arquivos de configuração) em um local central. Então, cada classe pode escolher seus parâmetros relevantes a partir daí. O que é um bom design na sua opinião?
lugge86
Talvez eu tenha escrito errado - quero dizer que você tem (e é uma boa prática) ter uma abstração geral com todas as configurações obtidas do arquivo de configuração / variáveis ​​de ambiente - que pode ser sua ConfigBlockclasse. O ponto aqui é não fornecer todo, neste caso, o contexto do estado do sistema, apenas os valores necessários para fazê-lo.
Dawid Pura