it-swarm.dev

Ist ein einzelnes Konfigurationsobjekt eine schlechte Idee?

In den meisten meiner Anwendungen habe ich ein einzelnes oder statisches "config" -Objekt, das für das Lesen verschiedener Einstellungen von der Festplatte zuständig ist. Fast alle Klassen verwenden es für verschiedene Zwecke. Im Wesentlichen handelt es sich nur um eine Hash-Tabelle mit Name/Wert-Paaren. Es ist schreibgeschützt, daher habe ich mich nicht allzu sehr darum gekümmert, dass ich so viel globalen Staat habe. Aber jetzt, wo ich mit Unit-Tests beginne, wird es langsam zu einem Problem.

Ein Problem ist, dass Sie normalerweise nicht mit derselben Konfiguration testen möchten, mit der Sie ausgeführt werden. Hierfür gibt es mehrere Lösungen:

  • Geben Sie dem Konfigurationsobjekt einen Setter, der NUR zum Testen verwendet wird, damit Sie verschiedene Einstellungen übergeben können.
  • Verwenden Sie weiterhin ein einzelnes Konfigurationsobjekt, ändern Sie es jedoch von einem Singleton in eine Instanz, die Sie überall dort weitergeben, wo es benötigt wird. Dann können Sie es einmal in Ihrer Anwendung und einmal in Ihren Tests mit unterschiedlichen Einstellungen erstellen.

In beiden Fällen bleibt jedoch noch ein zweites Problem: Fast jede Klasse kann das Konfigurationsobjekt verwenden. In einem Test müssen Sie also die Konfiguration für die zu testende Klasse, aber auch ALLE ihre Abhängigkeiten einrichten. Dies kann Ihren Testcode hässlich machen.

Ich komme zu dem Schluss, dass diese Art von Konfigurationsobjekt eine schlechte Idee ist. Was denken Sie? Was sind einige Alternativen? Und wie fängt man an, eine Anwendung umzugestalten, die überall Konfiguration verwendet?

45
JW01

Ich habe kein Problem mit einem einzelnen Konfigurationsobjekt und kann den Vorteil erkennen, alle Einstellungen an einem Ort zu behalten.

Die Verwendung dieses einzelnen Objekts überall führt jedoch zu einem hohen Grad an Kopplung zwischen dem Konfigurationsobjekt und den Klassen, die es verwenden. Wenn Sie die Konfigurationsklasse ändern müssen, müssen Sie möglicherweise jede Instanz besuchen, in der sie verwendet wird, und überprüfen, ob Sie nichts kaputt gemacht haben.

Eine Möglichkeit, damit umzugehen, besteht darin, mehrere Schnittstellen zu erstellen, die Teile des Konfigurationsobjekts verfügbar machen, die von verschiedenen Teilen der App benötigt werden. Anstatt anderen Klassen den Zugriff auf das Konfigurationsobjekt zu ermöglichen, übergeben Sie Instanzen einer Schnittstelle an die Klassen, die sie benötigen. Auf diese Weise hängen die Teile der App, die config verwenden, von der kleineren Sammlung von Feldern in der Benutzeroberfläche ab und nicht von der gesamten Konfigurationsklasse. Schließlich können Sie für Unit-Tests eine benutzerdefinierte Klasse erstellen, die die Schnittstelle implementiert.

Wenn Sie diese Ideen weiter untersuchen möchten, empfehle ich, über SOLID Prinzipien zu lesen, insbesondere über das Interface Segregation Principle und das Abhängigkeitsinversionsprinzip .

39
Kramii

Ich trenne Gruppen verwandter Einstellungen mit Schnittstellen.

Etwas wie:

public interface INotificationEmailSettings {
   public string To { get; set; }
}

public interface IMediaFileSettings {
    public string BasePath { get; set; }
}

usw.

In einer realen Umgebung implementiert eine einzelne Klasse viele dieser Schnittstellen. Diese Klasse kann aus einer Datenbank oder der App-Konfiguration oder was Sie haben, aber oft weiß sie, wie man die meisten Einstellungen erhält.

Durch die Trennung nach Schnittstellen werden die tatsächlichen Abhängigkeiten jedoch viel expliziter und feinkörniger, und dies ist eine offensichtliche Verbesserung für die Testbarkeit.

Aber .. Ich möchte nicht immer gezwungen sein, die Einstellungen als Abhängigkeit einzufügen oder bereitzustellen. Es könnte ein Argument vorgebracht werden, das ich aus Gründen der Konsistenz oder der Aussagekraft sollte. Aber im wirklichen Leben scheint es eine unnötige Einschränkung zu sein.

Also werde ich eine statische Klasse als Fassade verwenden, die einen einfachen Zugriff von überall auf Einstellungen ermöglicht, und innerhalb dieser statischen Klasse werde ich die Implementierung der Schnittstelle lokalisieren und die Einstellung abrufen.

Ich weiß, dass der Service-Standort von den meisten einen schnellen Daumen nach unten bekommt, aber seien wir ehrlich, die Bereitstellung einer Abhängigkeit durch einen Konstruktor ist ein Gewicht, und manchmal ist dieses Gewicht mehr, als ich tragen möchte. Service Location ist die Lösung zur Aufrechterhaltung der Testbarkeit durch Programmierung auf Schnittstellen und Ermöglichen mehrerer Implementierungen, während gleichzeitig der Komfort (in sorgfältig gemessenen und angemessen wenigen Situationen) eines statischen Singleton-Einstiegspunkts geboten wird.

public static class AllSettings {
    public INotificationEmailSettings NotificationEmailSettings {
        get {
            return ServiceLocator.Get<INotificationEmailSettings>();
        }
    }
}

Ich finde diese Mischung die beste aller Welten.

8
quentin-starin

Ja, wie Sie festgestellt haben, erschwert ein globales Konfigurationsobjekt das Testen von Einheiten. Ein "geheimer" Setter für Unit-Tests ist ein schneller Hack, der zwar nicht nett ist, aber sehr nützlich sein kann: Er ermöglicht es Ihnen, Unit-Tests zu schreiben, damit Sie Ihren Code im Laufe der Zeit in Richtung eines besseren Designs umgestalten können.

(Obligatorische Referenz: Effektiv mit Legacy-Code arbeiten . Es enthält diesen und viele weitere unschätzbare Tricks zum Testen von Legacy-Code.)

Nach meiner Erfahrung ist es am besten, so wenig Abhängigkeiten wie möglich von der globalen Konfiguration zu haben. Was aber nicht unbedingt Null ist - alles hängt von den Umständen ab. Einige "Organisator" -Klassen auf hoher Ebene, die auf die globale Konfiguration zugreifen und die tatsächlichen Konfigurationseigenschaften an die von ihnen erstellten und verwendeten Objekte weitergeben, können gut funktionieren, solange die Organisatoren nur dies tun, z. enthalten nicht viel testbaren Code selbst. Auf diese Weise können Sie die meisten oder alle vom Gerät testbaren wichtigen Funktionen der App testen, ohne das vorhandene physische Konfigurationsschema vollständig zu stören.

Dies nähert sich jedoch bereits einer echten Abhängigkeitsinjektionslösung wie Spring für Java. Wenn Sie auf ein solches Framework migrieren können, ist das in Ordnung. Im wirklichen Leben und insbesondere beim Umgang mit einer Legacy-App ist das langsame und sorgfältige Refactoring in Richtung DI häufig der am besten erreichbare Kompromiss.

4
Péter Török

Nach meiner Erfahrung ist Spring in der Welt Java) genau dafür gedacht. Es erstellt und verwaltet Objekte (Beans) basierend auf bestimmten Eigenschaften, die zur Laufzeit festgelegt wurden, und ist ansonsten für Ihre transparent Sie können die von Anon erwähnte Datei test.config verwenden oder eine Logik in die Konfigurationsdatei aufnehmen, die Spring verarbeitet, um Eigenschaften basierend auf einem anderen Schlüssel (z. B. Hostname oder Umgebung) festzulegen.

Was Ihr zweites Problem betrifft, können Sie dies durch eine neue Architektur umgehen, aber es ist nicht so schwerwiegend, wie es scheint. In Ihrem Fall bedeutet dies, dass Sie beispielsweise kein globales Konfigurationsobjekt haben würden, das von verschiedenen anderen Klassen verwendet wird. Sie würden diese anderen Klassen nur über Spring konfigurieren und dann kein Konfigurationsobjekt haben, da alles, was konfiguriert werden musste, über Spring abgerufen wurde, sodass Sie diese Klassen direkt in Ihrem Code verwenden können.

2
Justin Beal

Bei dieser Frage geht es wirklich um ein allgemeineres Problem als die Konfiguration. Sobald ich das Wort "Singleton" las, dachte ich sofort an alle Probleme, die mit diesem Muster verbunden sind, nicht zuletzt an die schlechte Testbarkeit.

Das Singleton-Muster wird als "schädlich" angesehen. Das heißt, es ist nicht immer das Falsche, aber es normalerweise ist. Wenn Sie jemals in Betracht ziehen, ein Singleton-Muster für irgendetwas zu verwenden, hören Sie auf zu überlegen:

  • Müssen Sie es jemals unterordnen?
  • Müssen Sie jemals auf eine Schnittstelle programmieren?
  • Müssen Sie Unit-Tests dagegen durchführen?
  • Müssen Sie es häufig ändern?
  • Soll Ihre Anwendung mehrere Produktionsplattformen/-umgebungen unterstützen?
  • Sind Sie überhaupt ein wenig besorgt über die Speichernutzung?

Wenn Ihre Antwort auf eine dieser Fragen "Ja" lautet (und wahrscheinlich auf einige andere Dinge, an die ich nicht gedacht habe), möchten Sie wahrscheinlich keinen Singleton verwenden. Konfigurationen erfordern oft viel mehr Flexibilität, als ein Singleton (oder eine instanzlose Klasse) bieten kann.

Wenn Sie fast alle Vorteile eines Singletons ohne Schmerzen nutzen möchten, verwenden Sie ein Abhängigkeitsinjektionsframework wie Spring oder Castle oder was auch immer für Ihre Umgebung verfügbar ist. Auf diese Weise müssen Sie es immer nur einmal deklarieren, und der Container stellt automatisch eine Instanz für alle Anforderungen bereit.

2
Aaronaught

Eine Möglichkeit, wie ich dies in C # behandelt habe, besteht darin, ein Singleton-Objekt zu haben, das während der Instanzerstellung gesperrt wird und alle Daten gleichzeitig für die Verwendung durch alle Clientobjekte initialisiert. Wenn dieser Singleton Schlüssel/Werte-Paare verarbeiten kann, können Sie alle Arten von Daten speichern, einschließlich komplexer Schlüssel, die von vielen verschiedenen Clients und Client-Typen verwendet werden. Wenn Sie es dynamisch halten und nach Bedarf neue Clientdaten laden möchten, können Sie das Vorhandensein eines Client-Hauptschlüssels überprüfen und, falls dies fehlt, die Daten für diesen Client laden und an das Hauptwörterbuch anhängen. Es ist auch möglich, dass der primäre Konfigurations-Singleton Sätze von Client-Daten enthält, wobei Vielfache desselben Client-Typs dieselben Daten verwenden, auf die alle über den primären Konfigurations-Singleton zugegriffen wird. Ein Großteil dieser Konfigurationsobjektorganisation hängt möglicherweise davon ab, wie Ihre Konfigurationsclients auf diese Informationen zugreifen müssen und ob diese Informationen statisch oder dynamisch sind. Ich habe sowohl Schlüssel-/Wertekonfigurationen als auch spezifische Objekt-APIs verwendet, je nachdem, welche benötigt wurden.

Einer meiner Konfigurations-Singletons kann Nachrichten zum erneuten Laden aus einer Datenbank empfangen. In diesem Fall lade ich in eine zweite Objektsammlung und sperre nur die Hauptsammlung, um Sammlungen auszutauschen. Dies verhindert das Blockieren von Lesevorgängen in anderen Threads.

Wenn Sie aus Konfigurationsdateien laden, können Sie eine Dateihierarchie haben. Die Einstellungen in einer Datei können angeben, welche der anderen Dateien geladen werden sollen. Ich habe diesen Mechanismus mit C # Windows-Diensten verwendet, die mehrere optionale Komponenten mit jeweils eigenen Konfigurationseinstellungen haben. Die Dateistrukturmuster waren für alle Dateien gleich, wurden jedoch basierend auf den primären Dateieinstellungen geladen oder nicht.

0
Tim Butterfield

Die Klasse, die Sie beschreiben, klingt wie ein Gott-Objekt-Antimuster , außer mit Daten anstelle von Funktionen. Das ist ein Problem. Sie sollten wahrscheinlich die Konfigurationsdaten lesen und im entsprechenden Objekt speichern lassen, während Sie die Daten nur dann erneut lesen, wenn sie aus irgendeinem Grund benötigt werden.

Außerdem verwenden Sie einen Singleton aus einem nicht geeigneten Grund. Singletons sollten nur verwendet werden, wenn das Vorhandensein mehrerer Objekte einen schlechten Zustand erzeugt. Die Verwendung eines Singleton ist in diesem Fall unangemessen, da mehr als ein Konfigurationsleser nicht sofort einen Fehlerstatus verursachen sollte. Wenn Sie mehr als einen haben, machen Sie es wahrscheinlich falsch, aber es ist nicht unbedingt erforderlich, dass Sie nur einen Konfigurationsleser haben.

Schließlich stellt das Erstellen eines solchen globalen Status eine Verletzung der Kapselung dar, da Sie mehr Klassen zulassen, als Sie benötigen, um direkt auf die Daten zuzugreifen.

0
indyK1ng

Ich denke, wenn es viele Einstellungen gibt, mit "Klumpen" verwandter, ist es sinnvoll, diese in separate Schnittstellen mit entsprechenden Implementierungen aufzuteilen und diese Schnittstellen dann bei Bedarf mit DI zu injizieren. Sie erhalten Testbarkeit, geringere Kopplung und SRP.

Joshua Flanagan von Los Techies hat mich in dieser Angelegenheit inspiriert. er hat einen Artikel geschrieben zu diesem Thema vor einiger Zeit.

0
Grant Palin