Gestione dei parametri nell'applicazione OOP


15

Sto scrivendo un'applicazione OOP di medie dimensioni in C ++ come un modo per praticare i principi OOP.

Ho diverse classi nel mio progetto e alcune devono accedere ai parametri di configurazione di runtime. Questi parametri vengono letti da diverse fonti durante l'avvio dell'applicazione. Alcuni vengono letti da un file di configurazione nella home-dir dell'utente, altri sono argomenti della riga di comando (argv).

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

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

Quindi, le mie lezioni 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 una classe può essere 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 .cpp delle classi deve includere e usare le cose di ConfigBlock.
Il file .h non introduce questa interfaccia per l'utente della classe. Tuttavia, l'interfaccia per ConfigBlock è ancora lì, tuttavia, è nascosta dal file .h.

È bello nasconderlo in questo modo?

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

Risposte:


10

Sono abbastanza pragmatico, ma la mia principale preoccupazione qui è che potresti consentire a questo ConfigBlockdi dominare i tuoi progetti di interfaccia in un modo probabilmente negativo. Quando hai qualcosa del genere:

explicit MyGreatClass(const ConfigBlock& config);

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

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

... al contrario della semplice raccolta di questi foo/bar/bazcampi da un campo enorme ConfigBlock.

Lazy Interface Design

Tra i lati positivi, 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 semplicemente caricarlo in un ConfigBlock(possibilmente senza alcuna modifica del codice) e quindi ciliegia- scegli qualsiasi nuovo materiale di cui hai bisogno senza alcun tipo di modifica dell'interfaccia, solo una modifica all'implementazione di MyGreatClass.

Quindi è sia un tipo di pro che un contro che questo ti libera di progettare un'interfaccia più attentamente pensata che accetta solo input di cui ha effettivamente bisogno. Applica la mentalità di "Dammi solo questo enorme blocco di dati, sceglierò ciò di cui ho bisogno da esso" invece di qualcosa di più simile a "Questi parametri precisi sono ciò di cui questa interfaccia ha bisogno per funzionare".

Quindi ci sono sicuramente alcuni pro qui, ma potrebbero essere pesantemente compensati dai contro.

accoppiamento

In questo scenario, tutte queste classi che vengono costruite da ConfigBlockun'istanza finiscono per avere le loro dipendenze così:

inserisci qui la descrizione dell'immagine

Questo può diventare un PITA, ad esempio, se si desidera Class2isolare l' unità in questo diagramma. Potrebbe essere necessario simulare superficialmente vari ConfigBlockinput contenenti i campi rilevanti a cui Class2è interessato per poterlo testare in una varietà di condizioni.

In qualsiasi tipo di nuovo contesto (sia che si tratti di unit test o di un intero nuovo progetto), tali classi possono finire per diventare più un peso da (ri) utilizzare, poiché alla fine dobbiamo sempre portare ConfigBlockcon noi il viaggio e impostarlo di conseguenza.

Riutilizzabilità / schierabilità / Testability

Invece, se si progettano queste interfacce in modo appropriato, possiamo separarle ConfigBlocke finire con qualcosa del genere:

inserisci qui la descrizione dell'immagine

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

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

Ora questo Clientcodice finisce per essere quello che deve dipendere da tutto e assemblare tutto insieme. L'onere finisce per essere trasferito a questo codice client per leggere i campi appropriati da a ConfigBlocke passarli nelle classi appropriate come parametri. Tuttavia, tale codice client è generalmente progettato in modo restrittivo per un contesto specifico e il suo potenziale per il riutilizzo sarà in genere zilch o chiuso comunque (potrebbe essere la mainfunzione del punto di ingresso dell'applicazione o qualcosa del genere).

Quindi, dal punto di vista della riutilizzabilità e dei test, può aiutare a rendere queste classi più indipendenti. Dal punto di vista dell'interfaccia per coloro che usano le tue classi, può anche aiutare a dichiarare esplicitamente quali parametri sono necessari invece di un solo enorme ConfigBlockche modella l'intero universo di campi dati richiesti 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. Di conseguenza, la loro applicabilità, schierabilità, riusabilità, testabilità, ecc. Possono subire un degrado significativo. Eppure possono in qualche modo semplificare il design dell'interfaccia se tentiamo una rotazione positiva su di esso. Sta a te misurare quei pro e contro e decidere se valgono i compromessi. In genere è molto più sicuro sbagliare contro questo tipo di design in cui stai scegliendo un monolite in classi che generalmente intendono modellare un design più generale e ampiamente applicabile.

Ultimo, ma non per importanza:

extern CodingBlock MyCodingBlock;

... questo è potenzialmente anche peggio (più distorto?) in termini di caratteristiche sopra descritte rispetto all'approccio dell'iniezione di dipendenza, in quanto finisce per accoppiare le tue classi non solo ConfigBlocks, ma direttamente a una sua specifica istanza . Ciò degrada ulteriormente l'applicabilità / schierabilità / testabilità.

Il mio consiglio generale potrebbe errare sul lato della progettazione di interfacce che non dipendono da questo tipo di monoliti per fornire i loro parametri, almeno per le classi più generalmente applicabili progettate. Ed evita l'approccio globale senza iniezione di dipendenza se puoi, a meno che tu non abbia davvero una ragione molto forte e sicura di non evitarlo.


1

Di solito la configurazione di un'applicazione viene consumata principalmente dagli oggetti di fabbrica. Qualsiasi oggetto basato sulla configurazione dovrebbe essere generato da uno di quegli oggetti factory. È possibile utilizzare il modello astratto di fabbrica per implementare una classe che comprende l'intero ConfigBlockoggetto. Questa classe esporrà metodi pubblici per restituire altri oggetti factory e passerebbe solo la parte del ConfigBlockrelativo a quel particolare oggetto factory. In questo modo le impostazioni di configurazione "gocciolano" ConfigBlockdall'oggetto ai suoi membri e dalla fabbrica della fabbrica alle fabbriche.

Userò C # poiché conosco meglio il linguaggio, ma questo 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);
    }
}

Dopo di che hai bisogno di una sorta di classe che funge 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 oggetti factory, che è essenzialmente ciò che si desidera fare qui.

Vedi anche Provider Pattern per principianti . Ancora una volta, questo è orientato allo sviluppo di .NET, ma con C # e C ++ entrambi linguaggi orientati agli oggetti, il modello dovrebbe essere principalmente trasferibile tra i due.

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

Infine, una critica a questo modello: il provider non è un modello


Va tutto bene, tranne i collegamenti ai modelli di provider. La riflessione non è supportata da c ++ e non funzionerà.
BЈовић,

@ BЈовић: corretto. La riflessione di classe non esiste, tuttavia è possibile creare una soluzione manuale, che sostanzialmente si trasforma in switchun'istruzione o in una ifverifica dell'istruzione rispetto a un valore letto dai file di configurazione.
Greg Burghardt,

0

Prima domanda: è buona norma raccogliere tutti questi dati di configurazione di runtime in una classe?

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

Il costruttore di una classe può essere considerato un riferimento al mio ConfigBlock

Questo è negativo: 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();

creare un'istanza di MyGreatClass:

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

Qui current_config_blockc'è un'istanza della tua ConfigBlockclasse (quella che contiene tutti i tuoi valori) e la MyGreatClassclasse riceve GreatClassDataun'istanza. In altre parole, passa ai costruttori solo i dati di cui hanno bisogno e aggiungi funzionalità ConfigBlockper creare tali dati.

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

 extern CodingBlock MyCodingBlock;

Quindi, solo il file .cpp delle classi deve includere e usare le cose di ConfigBlock. Il file .h non introduce questa interfaccia per l'utente della classe. Tuttavia, l'interfaccia per ConfigBlock è ancora lì, tuttavia, è nascosta dal file .h. È bello nasconderlo in questo modo?

Questo codice suggerisce che avrai un'istanza globale di CodingBlock. Non farlo: normalmente dovresti avere un'istanza dichiarata a livello globale, in qualsiasi punto di ingresso utilizzato dalla tua applicazione (funzione principale, DllMain, ecc.) E passarla come argomento ovunque sia necessario (ma come spiegato sopra, non dovresti passare l'intera classe in giro, basta esporre le interfacce attorno ai dati e passare quelle).

Inoltre, non associare le classi client (le tue MyGreatClass) al tipo di CodingBlock; Questo significa che, se MyGreatClassprendi una stringa e cinque numeri interi, farai meglio a passare quella stringa e numeri interi, piuttosto che passare a CodingBlock.


Penso che sia una buona idea separare le fabbriche dalla configurazione. Non è soddisfacente che l'implementazione della configurazione sappia come creare un'istanza dei componenti, poiché ciò comporta necessariamente una dipendenza a 2 vie in cui in precedenza esisteva solo una dipendenza a 1 via. Ciò ha grandi implicazioni nell'estensione del codice, specialmente quando si usano librerie condivise in cui le interfacce contano davvero
Joel Cornett,

0

Risposta breve:

Non hai bisogno di tutte le impostazioni per ciascuno dei moduli / classi nel tuo codice. Se lo fai, allora c'è qualcosa che non va nel tuo design orientato agli oggetti. Soprattutto in caso di unit test che imposta tutte le variabili che non ti servono e che passa quell'oggetto non aiuterebbe con la lettura o il mantenimento.


In questo modo posso raccogliere il codice parser (analizzare la riga di comando e i file di configurazione) in una posizione centrale. Quindi, ogni classe può scegliere da lì i propri parametri rilevanti. Qual è un buon design secondo te?
lugge86,

Forse ho appena scritto male - voglio dire che devi (ed è una buona pratica) avere un'astrazione generale con tutte le impostazioni ottenute dal file di configurazione / variabili di ambiente - che potrebbe essere la tua ConfigBlockclasse. Il punto qui è di non fornire tutti, in questo caso, il contesto dello stato del sistema, solo particolari, valori richiesti per farlo.
Dawid Pura,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.