Parameterverwaltung in der OOP-Anwendung

15

Ich schreibe eine mittelgroße OOP-Anwendung in C ++, um die OOP-Prinzipien zu üben.

In meinem Projekt gibt es mehrere Klassen, von denen einige auf Laufzeitkonfigurationsparameter zugreifen müssen. Diese Parameter werden beim Start der Anwendung aus mehreren Quellen gelesen. Einige werden aus einer Konfigurationsdatei im Home-Verzeichnis des Benutzers gelesen, andere sind Befehlszeilenargumente (argv).

Also habe ich eine Klasse erstellt ConfigBlock. Diese Klasse liest alle Parameterquellen und speichert sie in einer geeigneten Datenstruktur. Beispiele sind Pfad- und Dateinamen, die vom Benutzer in der Konfigurationsdatei geändert werden können, oder das --verbose CLI-Flag. Dann kann man aufrufen ConfigBlock.GetVerboseLevel(), um diesen spezifischen Parameter zu lesen.

Meine Frage: Ist es empfehlenswert, alle derartigen Laufzeitkonfigurationsdaten in einer Klasse zu sammeln?

Dann benötigen meine Klassen Zugriff auf alle diese Parameter. Ich kann mir verschiedene Wege vorstellen, um dies zu erreichen, aber ich bin mir nicht sicher, welchen ich nehmen soll. Einem Klassenkonstruktor kann ein Verweis auf meinen ConfigBlock gegeben werden, wie z

public:
    MyGreatClass(ConfigBlock &config);

Oder sie enthalten einfach einen Header "CodingBlock.h", der eine Definition meines CodingBlocks enthält:

extern CodingBlock MyCodingBlock;

Dann muss nur die CPP-Datei der Klassen das ConfigBlock-Zeug enthalten und verwenden.
Die .h-Datei bietet dem Benutzer der Klasse keine Einführung in diese Schnittstelle. Die Schnittstelle zu ConfigBlock ist zwar noch vorhanden, jedoch vor der .h-Datei verborgen.

Ist es gut, es so zu verstecken?

Ich möchte, dass die Schnittstelle so klein wie möglich ist, aber letztendlich muss jede Klasse, die Konfigurationsparameter benötigt, eine Verbindung zu meinem ConfigBlock haben. Aber wie soll diese Verbindung aussehen?

lugge86
quelle

Antworten:

10

Ich bin ein ziemlicher Pragmatiker, aber mein Hauptanliegen ist, dass Sie möglicherweise zulassen, dass dies ConfigBlockIhre Schnittstellendesigns auf möglicherweise schlechte Weise dominiert. Wenn Sie so etwas haben:

explicit MyGreatClass(const ConfigBlock& config);

... eine passendere Schnittstelle könnte so aussehen:

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

... im Gegensatz dazu, diese foo/bar/bazFelder einfach nur aus einer Masse herauszusuchen ConfigBlock.

Lazy Interface Design

Auf der positiven Seite macht es diese Art von Design einfach, eine stabile Schnittstelle für Ihren Konstruktor zu entwerfen, z. B. wenn Sie etwas Neues benötigen, können Sie dies einfach in eine laden ConfigBlock(möglicherweise ohne Codeänderungen) und dann die Wählen Sie die neuen Elemente aus, die Sie benötigen, ohne dass sich die Benutzeroberfläche ändert, sondern nur die Implementierung von MyGreatClass.

Es ist also sowohl ein Pro als auch ein Contra, dass Sie keine sorgfältig durchdachte Benutzeroberfläche entwickeln müssen, die nur die tatsächlich benötigten Eingaben akzeptiert. Es wendet die Denkweise an: "Gib mir nur diesen riesigen Datenblock, ich werde herausfinden, was ich brauche" und nicht etwa: "Diese genauen Parameter sind das, was diese Schnittstelle benötigt, um zu funktionieren."

Es gibt also definitiv einige Vorteile, die jedoch durch die Nachteile stark aufgewogen werden könnten.

Kupplung

In diesem Szenario haben alle Klassen, die aus einer ConfigBlockInstanz erstellt werden, folgende Abhängigkeiten:

Bildbeschreibung hier eingeben

Dies kann beispielsweise zu einem PITA werden, wenn Sie Class2in diesem Diagramm einen Einzeltest durchführen möchten . Möglicherweise müssen Sie verschiedene ConfigBlockEingaben, die die relevanten Felder enthalten , oberflächlich simulieren Class2, um sie unter verschiedenen Bedingungen testen zu können.

In jedem neuen Kontext (ob Komponententest oder ganz neues Projekt) können solche Klassen zu einer größeren Belastung für die (Wiederver-) Verwendung werden, da wir sie immer ConfigBlockfür die Fahrt mitnehmen und einrichten müssen entsprechend.

Wiederverwendbarkeit / Bereitstellbarkeit / Testbarkeit

Wenn Sie diese Schnittstellen entsprechend gestalten, können wir sie abkoppeln ConfigBlockund so etwas erreichen:

Bildbeschreibung hier eingeben

Wenn Sie in diesem obigen Diagramm feststellen, werden alle Klassen unabhängig (ihre afferenten / ausgehenden Kopplungen verringern sich um 1).

Dies führt zu viel mehr unabhängigen Klassen (zumindest unabhängig von ConfigBlock), die in neuen Szenarien / Projekten viel einfacher (wieder) zu verwenden / zu testen sind.

Nun ist dieser ClientCode derjenige, der von allem abhängen und alles zusammensetzen muss. Die Last wird letztendlich auf diesen Clientcode übertragen, um die entsprechenden Felder von a zu lesen ConfigBlockund sie als Parameter an die entsprechenden Klassen weiterzuleiten. Ein derartiger Client-Code ist jedoch im Allgemeinen eng auf einen bestimmten Kontext zugeschnitten, und die Wiederverwendung kann in der Regel ohnehin nur eingeschränkt oder nur eingeschränkt erfolgen (dies kann beispielsweise die mainEinstiegsfunktion Ihrer Anwendung sein ).

Unter dem Gesichtspunkt der Wiederverwendbarkeit und des Testens kann es daher hilfreich sein, diese Klassen unabhängiger zu machen. Vom Standpunkt der Benutzeroberfläche aus kann es für diejenigen, die Ihre Klassen verwenden, auch hilfreich sein, explizit anzugeben, welche Parameter sie benötigen, anstatt nur einer einzigen Masse, ConfigBlockdie das gesamte Universum der für alles erforderlichen Datenfelder modelliert.

Fazit

Im Allgemeinen neigt diese Art von klassenorientiertem Design, das von einem Monolithen abhängt, der alles Notwendige hat, dazu, diese Art von Eigenschaften aufzuweisen. Ihre Anwendbarkeit, Bereitstellbarkeit, Wiederverwendbarkeit, Testbarkeit usw. können dadurch erheblich beeinträchtigt werden. Sie können jedoch das Schnittstellendesign vereinfachen, wenn wir versuchen, es positiv zu beeinflussen. Es liegt an Ihnen, die Vor- und Nachteile zu messen und zu entscheiden, ob sich die Kompromisse lohnen. In der Regel ist es viel sicherer, sich gegen diese Art von Design zu irren, wenn Sie in Klassen, die im Allgemeinen ein allgemeineres und allgemein anwendbares Design modellieren sollen, aus einem Monolithen heraussuchen.

Zu guter Letzt:

extern CodingBlock MyCodingBlock;

... dies ist potenziell noch schlimmer (mehr schief?) in Bezug auf die oben beschriebenen Merkmale als der Ansatz der Abhängigkeitsinjektion, da Ihre Klassen nicht nur an ConfigBlocks, sondern direkt an eine bestimmte Instanz davon gekoppelt werden. Dies verschlechtert die Anwendbarkeit / Bereitstellbarkeit / Testbarkeit weiter.

Mein allgemeiner Rat wäre, sich beim Entwerfen von Schnittstellen, die nicht von solchen Monolithen abhängig sind, zu irren, um deren Parameter bereitzustellen, zumindest für die allgemein anwendbaren Klassen, die Sie entwerfen. Und vermeiden Sie den globalen Ansatz ohne Abhängigkeitsinjektion, wenn Sie dies nicht können, es sei denn, Sie haben wirklich einen sehr starken und sicheren Grund, ihn nicht zu vermeiden.

marstato
quelle
1

Normalerweise wird die Konfiguration einer Anwendung hauptsächlich von Factory-Objekten verwendet. Jedes Objekt, das auf der Konfiguration beruht, sollte aus einem dieser Factory-Objekte generiert werden. Sie können das abstrakte Factory-Muster verwenden , um eine Klasse zu implementieren, die das gesamte ConfigBlockObjekt aufnimmt . Diese Klasse würde öffentliche Methoden verfügbar machen, um andere Factory-Objekte zurückzugeben, und würde nur den Teil des ConfigBlockrelevanten Objekts an dieses bestimmte Factory-Objekt übergeben. Auf diese Weise "sickern" die Konfigurationseinstellungen vom ConfigBlockObjekt zu seinen Mitgliedern und von der Factory-Factory zu den Fabriken.

Ich werde C # verwenden, da ich die Sprache besser kenne, aber dies sollte leicht auf C ++ übertragbar sein.

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);
    }
}

Danach benötigen Sie eine Art Klasse, die als "Anwendung" fungiert, die in Ihrer Hauptprozedur instanziiert wird:

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

Als letzte Anmerkung wird dies in .NET speak als "Provider-Objekt" bezeichnet. Provider-Objekte in .NET scheinen Konfigurationsdaten mit Factory-Objekten zu verbinden, was Sie hier im Wesentlichen tun möchten.

Siehe auch Provider Pattern für Einsteiger . Auch dies ist auf die .NET-Entwicklung ausgerichtet, aber da C # und C ++ beide objektorientierte Sprachen sind, sollte das Muster hauptsächlich zwischen den beiden übertragbar sein.

Eine weitere gute Lektüre, die mit diesem Muster zusammenhängt: Das Anbietermodell .

Zuletzt eine Kritik dieses Musters: Provider ist kein Muster

Greg Burghardt
quelle
Alles ist gut, bis auf die Links zu den Anbietermodellen. Die Reflektion wird von c ++ nicht unterstützt, und das wird nicht funktionieren.
BЈовић
@ BЈовић: Richtig. Es gibt keine Klassenreflexion, Sie können jedoch eine manuelle Problemumgehung erstellen, die sich im Wesentlichen auf eine switchAnweisung oder eine ifAnweisung bezieht, die anhand eines aus den Konfigurationsdateien gelesenen Werts getestet wird.
Greg Burghardt
0

Erste Frage: Ist es empfehlenswert, alle derartigen Laufzeitkonfigurationsdaten in einer Klasse zu sammeln?

Ja. Es ist besser, Laufzeitkonstanten und -werte sowie den Code zum Lesen zu zentralisieren.

Einem Klassenkonstruktor kann ein Verweis auf meinen ConfigBlock gegeben werden

Das ist schlecht: Die meisten Konstruktoren werden die meisten Werte nicht benötigen. Erstellen Sie stattdessen Schnittstellen für alles, was nicht einfach zu konstruieren ist:

alter Code (Ihr Vorschlag):

MyGreatClass(ConfigBlock &config);

neuer Code:

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

Instanziiere eine MyGreatClass:

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

Hier current_config_blockist eine Instanz Ihrer ConfigBlockKlasse (die, die alle Ihre Werte enthält) und die MyGreatClassKlasse erhält eine GreatClassDataInstanz. Mit anderen Worten, übergeben Sie nur die benötigten Daten an Konstruktoren und fügen Sie Funktionen ConfigBlockzum Erstellen dieser Daten hinzu.

Oder sie enthalten einfach einen Header "CodingBlock.h", der eine Definition meines CodingBlocks enthält:

 extern CodingBlock MyCodingBlock;

Dann muss nur die CPP-Datei der Klassen das ConfigBlock-Zeug enthalten und verwenden. Die .h-Datei bietet dem Benutzer der Klasse keine Einführung in diese Schnittstelle. Die Schnittstelle zu ConfigBlock ist zwar noch vorhanden, jedoch vor der .h-Datei verborgen. Ist es gut, es so zu verstecken?

Dieser Code deutet darauf hin, dass Sie eine globale CodingBlock-Instanz haben. Tun Sie das nicht: Normalerweise sollten Sie eine Instanz global deklarieren lassen, unabhängig davon, welchen Einstiegspunkt Ihre Anwendung verwendet (Hauptfunktion, DllMain usw.), und diese als Argument übergeben, wo immer Sie dies benötigen (aber wie oben erläutert, sollten Sie sie nicht übergeben die gesamte Klasse herum, nur Schnittstellen um die Daten verfügbar machen und diese übergeben).

Binden Sie Ihre Client-Klassen (Ihre MyGreatClass) auch nicht an die Art von CodingBlock; Wenn Sie also MyGreatClasseine Zeichenfolge und fünf Ganzzahlen eingeben, ist es besser, diese Zeichenfolge und Ganzzahlen einzugeben, als a CodingBlock.

utnapistim
quelle
Ich denke, es ist eine gute Idee, Fabriken von der Konfiguration zu entkoppeln. Es ist nicht zufriedenstellend, dass die Konfigurationsimplementierung wissen sollte, wie Komponenten instanziiert werden, da dies notwendigerweise zu einer bidirektionalen Abhängigkeit führt, bei der zuvor nur eine bidirektionale Abhängigkeit bestand. Dies hat große Auswirkungen auf die Erweiterung Ihres Codes, insbesondere wenn Sie gemeinsam genutzte Bibliotheken verwenden, bei denen es auf die Schnittstellen ankommt
Joel Cornett,
0

Kurze Antwort:

Sie benötigen nicht alle Einstellungen für die einzelnen Module / Klassen in Ihrem Code. Wenn Sie dies tun, stimmt etwas nicht mit Ihrem objektorientierten Design. Insbesondere im Falle von Unit-Tests würde das Setzen aller Variablen, die Sie nicht benötigen, und das Übergeben dieses Objekts beim Lesen oder Verwalten nicht helfen.

Dawid Pura
quelle
Auf diese Weise kann ich den Parser-Code (Syntaxanalyse-Befehlszeile und Konfigurationsdateien) an einem zentralen Ort sammeln. Dann kann jede Klasse ihre relevanten Parameter von dort auswählen. Was ist Ihrer Meinung nach ein gutes Design?
lugge86
Vielleicht habe ich es einfach falsch geschrieben - ich meine, Sie müssen (und es ist eine gute Praxis) eine allgemeine Abstraktion mit allen Einstellungen haben, die aus Konfigurationsdatei / Umgebungsvariablen stammen - was möglicherweise Ihre ConfigBlockKlasse ist. Hier geht es darum, in diesem Fall nicht alle für den Systemstatus erforderlichen Werte anzugeben, sondern nur bestimmte.
Dawid Pura