Gestione dei parametri nell'applicazione OOP

15

Sto scrivendo un'applicazione OOP di medie dimensioni in C ++ come metodo per mettere in pratica i principi OOP.

Ho diverse classi nel mio progetto, e alcune hanno bisogno di accedere ai parametri di configurazione del tempo di esecuzione. Questi parametri vengono letti da diverse fonti durante l'avvio dell'applicazione. Alcuni vengono letti da un file di configurazione nella directory home dell'utente, alcuni sono argomenti della riga di comando (argv).

Quindi ho creato una classe ConfigBlock . Questa classe legge tutte le fonti dei parametri e la memorizza in una struttura dati appropriata. Gli esempi sono path e nomi di file che possono essere modificati dall'utente nel file di configurazione o dal flag --verbose CLI. Quindi, si può chiamare ConfigBlock.GetVerboseLevel() per leggere questo parametro specifico.

La mia domanda: è buona pratica raccogliere tutti questi dati di configurazione runtime in una classe?

Quindi, le mie classi hanno bisogno di accedere a tutti questi parametri. Posso pensare a diversi modi per raggiungere questo obiettivo, ma non sono sicuro di quale prendere. Un costruttore di classe può avere un riferimento al mio ConfigBlock, come

public:
    MyGreatClass(ConfigBlock &config);

Oppure includono solo un'intestazione "CodingBlock.h" che contiene una definizione del mio CodingBlock:

extern CodingBlock MyCodingBlock;

Quindi, solo il file delle classi .cpp deve includere e usare la roba di ConfigBlock.
Il file .h non introduce questa interfaccia all'utente della classe. Tuttavia, l'interfaccia di ConfigBlock è ancora lì, tuttavia, è nascosta dal file .h.

È bello nascondere in questo modo?

Voglio che l'interfaccia sia il più piccola possibile, ma alla fine credo che ogni classe che ha bisogno di parametri di configurazione debba avere una connessione al mio ConfigBlock. Ma come dovrebbe essere questa connessione?

    
posta lugge86 14.12.2015 - 14:24
fonte

4 risposte

9

Sono piuttosto pragmatico, ma la mia preoccupazione principale qui è che potresti permettere a questo ConfigBlock di dominare i tuoi progetti di interfaccia in un modo forse peggiore. Quando hai qualcosa di simile:

explicit MyGreatClass(const ConfigBlock& config);

... un'interfaccia più appropriata potrebbe essere così:

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

... anziché limitarsi a selezionare questi campi con foo/bar/baz su un massiccio ConfigBlock .

Lazy Interface Design

Sul lato positivo, questo tipo di design semplifica la progettazione di un'interfaccia stabile per il tuo costruttore, ad esempio, dal momento che se hai bisogno di qualcosa di nuovo, puoi caricarlo in un ConfigBlock (probabilmente senza alcun codice modifiche) e quindi selezionale tutte le nuove cose di cui hai bisogno senza alcun tipo di cambio di interfaccia, solo una modifica all'implementazione di MyGreatClass .

Quindi è allo stesso tempo un tipo di pro e contro che questo ti libera dalla progettazione di un'interfaccia più attentamente pensata che accetta solo gli input di cui ha effettivamente bisogno. "Dammi solo questo enorme numero di dati, selezionerò quello che mi serve" al contrario di qualcosa di più simile, "Questi precisi parametri sono ciò che questa interfaccia deve funzionare. "

Quindi ci sono sicuramente alcuni professionisti qui, ma potrebbero essere pesantemente superati dai cons.

Accoppiamento

In questo scenario, tutte le classi costruite da un'istanza ConfigBlock finiscono per avere le loro dipendenze in questo modo:

QuestopuòdiventareunPITA,adesempio,sesidesideratestarel'unitàClass2inquestodiagrammaseparatamente.PotrebbeesserenecessariosimularesuperficialmentevariConfigBlockdiinputcontenentiicampipertinentiacuiClass2èinteressatoperpoterlotestareinunavarietàdicondizioni.

Inqualsiasitipodinuovocontesto(chesiauntestunitarioouninteronuovoprogetto),taliclassipossonodiventarepiùdiunpesodariutilizzare,poichéallafinedobbiamosempreportareConfigBlockperilguidareeconfigurarlodiconseguenza.

Riutilizzabilità/schierabilità/Testability

Inveceseprogettatequesteinterfacceinmodoappropriato,possiamodisaccoppiarledaConfigBlockefinireconqualcosadelgenere:

Se noti in questo diagramma sopra, tutte le classi diventano indipendenti (i loro accoppiamenti afferenti / uscenti si riducono di 1).

Ciò porta a classi molto più indipendenti (almeno indipendenti da ConfigBlock ) che possono essere molto più facili da (ri) utilizzare / testare in nuovi scenari / progetti.

Ora questo codice Client finisce per essere quello che deve dipendere da tutto e assemblarlo tutto insieme. Il carico finisce per essere trasferito su questo codice cliente per leggere i campi appropriati da un ConfigBlock e passarli nelle classi appropriate come parametri. Tuttavia, tale codice client è generalmente progettato in modo restrittivo per un contesto specifico, e il suo potenziale di riutilizzo sarà in genere pari a zero o comunque (potrebbe essere la funzione main entry point dell'applicazione o qualcosa del genere)

Quindi, dal punto di vista della riusabilità e dei test, può aiutare a rendere queste classi più indipendenti. Dal punto di vista dell'interfaccia per chi usa le classi, può anche aiutare a specificare in modo esplicito quali parametri hanno bisogno invece di un solo ConfigBlock che modella l'intero universo di campi dati necessari per tutto.

Conclusione

In generale, questo tipo di design orientato alla classe che dipende da un monolite che ha tutto il necessario tende ad avere questo tipo di caratteristiche. La loro applicabilità, schierabilità, riutilizzabilità, testabilità, ecc. Possono essere notevolmente ridotte di conseguenza. Tuttavia possono semplificare il design dell'interfaccia se proviamo a fare un giro positivo. Spetta a te misurare quei pro e contro e decidere se valgono i trade-off. In genere è molto più sicuro sbagliare contro questo tipo di design in cui si sta selezionando da un monolite in classi che sono generalmente destinate a modellare un progetto più generale e ampiamente applicabile.

Ultimo ma non meno importante:

extern CodingBlock MyCodingBlock;

... questo è potenzialmente anche peggiore (più distorto?) in termini delle caratteristiche sopra descritte rispetto all'approccio basato sull'input della dipendenza, poiché finisce per accoppiare le classi non solo a ConfigBlocks , ma direttamente a un specifica istanza di esso. Ciò degrada ulteriormente l'applicabilità / deployability / testability.

Il mio consiglio generale sarebbe di sbagliare dal progettare interfacce che non dipendono da questi tipi di monoliti per fornire i loro parametri, almeno per le classi più generalmente applicabili che progettate. Evita l'approccio globale senza un'iniezione di dipendenza, se puoi, a meno che tu non abbia davvero una ragione molto strong e sicura per non evitarlo.

    
risposta data 19.12.2015 - 20:22
fonte
1

Di solito la configurazione di un'applicazione viene consumata principalmente da oggetti di fabbrica. Qualsiasi oggetto che si basa sulla configurazione dovrebbe essere generato da uno di quegli oggetti di fabbrica. Puoi utilizzare il modello di fabbrica astratto per implementare una classe che occupa l'intero oggetto ConfigBlock . Questa classe esporrà metodi pubblici per restituire altri oggetti factory e passerebbe solo nella parte di ConfigBlock relativa a quel particolare oggetto factory. In questo modo le impostazioni di configurazione "gocciolano" dall'oggetto ConfigBlock ai suoi membri e dalla fabbrica di fabbrica alle fabbriche.

Userò C # poiché conosco meglio la lingua, ma dovrebbe essere facilmente trasferibile in 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);
    }
}

Dopodiché avrai bisogno di una sorta di classe che funga da "applicazione" che viene istanziata nella tua procedura principale:

// 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
    }
}

Come ultima nota, questo è noto come "oggetto provider" in .NET speak. Gli oggetti provider in .NET sembrano sposare i dati di configurazione con gli oggetti factory, che è essenzialmente ciò che si vuole fare qui.

Vedi anche Pattern provider per principianti . Ancora una volta, questo è orientato verso lo sviluppo .NET, ma con C # e C ++ essendo entrambi linguaggi orientati agli oggetti, il pattern dovrebbe essere principalmente trasferibile tra i due.

Un'altra buona lettura correlata a questo modello: Il modello fornitore .

Infine, una critica di questo modello: Provider non è un modello

    
risposta data 14.12.2015 - 15:23
fonte
0

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

Sì. È meglio centralizzare costanti e valori di runtime e il codice per leggerli.

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

Questo è male: la maggior parte dei costruttori non avrà bisogno della maggior parte dei valori. Invece, crea interfacce per tutto ciò che non è banale da costruire:

vecchio codice (la tua proposta):

MyGreatClass(ConfigBlock &config);

nuovo codice:

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

istanzia un MyGreatClass:

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

Qui current_config_block è un'istanza della tua classe ConfigBlock (quella che contiene tutti i tuoi valori) e la classe MyGreatClass riceve un'istanza GreatClassData . In altre parole, passa ai costruttori solo i dati di cui hanno bisogno e aggiungi strutture al tuo ConfigBlock per creare tali dati.

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?

Questo codice suggerisce di disporre di un'istanza di CodingBlock globale. Non farlo: normalmente dovresti avere un'istanza dichiarata globalmente, in qualunque punto di ingresso usi la tua applicazione (funzione principale, DllMain, ecc.) E passarla come argomento ovunque ti serva (ma come spiegato sopra, non dovresti passare l'intera classe in giro, basta esporre le interfacce attorno ai dati e passare quelli).

Inoltre, non legare le classi dei tuoi clienti (il tuo MyGreatClass ) al tipo di CodingBlock ; Ciò significa che, se il MyGreatClass contiene una stringa e cinque numeri interi, sarà meglio passare la stringa e gli interi, quindi passerai in CodingBlock .

    
risposta data 14.12.2015 - 15:41
fonte
0

Risposta breve:

non hai bisogno di tutte le impostazioni per ciascun modulo / classe nel tuo codice. Se lo fai, allora c'è qualcosa di sbagliato nel tuo design orientato agli oggetti. Soprattutto in caso di test dell'unità che imposti tutte le variabili che non ti servono e il passaggio di quell'oggetto non aiuta a leggere o mantenere.

    
risposta data 14.12.2015 - 16:17
fonte

Leggi altre domande sui tag