Ist ein einzelnes Konfigurationsobjekt eine schlechte Idee?


43

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 zu sehr mit der Tatsache beschäftigt, dass ich so einen globalen Staat habe. Aber jetzt, wo ich mit Unit-Tests anfange, wird es 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 einige 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, aber ändern Sie es von einem Singleton in eine Instanz, die Sie überall dort weitergeben, wo sie benötigt wird. Dann können Sie es einmal in Ihrer Anwendung und einmal in Ihren Tests mit verschiedenen Einstellungen erstellen.

Wie auch immer, Sie haben immer noch ein zweites Problem: Fast jede Klasse kann das config-Objekt verwenden. In einem Test müssen Sie also die Konfiguration für die zu testende Klasse einrichten, aber auch ALLE ihre Abhängigkeiten. Dies kann Ihren Testcode hässlich machen.

Ich komme allmählich zu dem Schluss, dass diese Art von Konfigurationsobjekt eine schlechte Idee ist. Was denkst du? Welche Alternativen gibt es? Und wie können Sie eine Anwendung umgestalten, bei der die Konfiguration überall verwendet wird?


Die Konfiguration erhält ihre Einstellungen durch Lesen einer Datei auf der Festplatte, oder? Warum also nicht einfach eine "test.config" -Datei haben, die es lesen kann?
Anon.

Das löst das erste Problem, aber nicht das zweite.
JW01

@ JW01: Benötigen alle Ihre Tests eine deutlich andere Konfiguration? Sie müssen diese Konfiguration während Ihres Tests irgendwo einrichten , nicht wahr?
Anon.

Wahr. Wenn ich jedoch weiterhin einen einzelnen Pool von Einstellungen verwende (mit einem anderen Pool zum Testen), werden alle meine Tests mit denselben Einstellungen ausgeführt. Und da ich das Verhalten mit verschiedenen Einstellungen testen möchte, ist dies nicht ideal.
JW01

"In einem Test müssen Sie also die Konfiguration für die zu testende Klasse einrichten, aber auch ALLE ihre Abhängigkeiten. Dies kann Ihren Testcode hässlich machen." Hast du ein Beispiel? Ich habe das Gefühl, dass es nicht die Struktur Ihrer gesamten Anwendung ist, die sich mit der Konfigurationsklasse verbindet, sondern die Struktur der Konfigurationsklasse selbst, die die Dinge "hässlich" macht. Sollte das Einrichten der Konfiguration einer Klasse ihre Abhängigkeiten nicht automatisch konfigurieren?
AndrewKS

Antworten:


35

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 einer starken 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 beschädigt haben.

Eine Möglichkeit, damit umzugehen, besteht darin, mehrere Schnittstellen zu erstellen, die Teile des Konfigurationsobjekts anzeigen, 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 und nicht von der gesamten config-Klasse ab. Zum Testen von Einheiten können Sie schließlich eine benutzerdefinierte Klasse erstellen, die die Schnittstelle implementiert.

Wenn Sie diese Ideen weiter untersuchen möchten, empfehle ich, die SOLID- Prinzipien zu lesen , insbesondere das Prinzip der Grenzflächensegregation und das Prinzip der Abhängigkeitsinversion .


7

Ich trenne Gruppen von verwandten Einstellungen mit Schnittstellen.

So 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 Schnittstelle werden die tatsächlichen Abhängigkeiten jedoch viel deutlicher und feinkörniger. Dies ist eine offensichtliche Verbesserung für die Testbarkeit.

Aber ich möchte nicht immer gezwungen werden, die Einstellungen als Abhängigkeit einzuspeisen oder bereitzustellen. Es gibt ein Argument, das ich aus Gründen der Beständigkeit oder der Klarheit vorbringen könnte. Aber im wirklichen Leben scheint es eine unnötige Einschränkung.

Daher verwende ich eine statische Klasse als Fassade, um von überall einen einfachen Einstieg in die Einstellungen zu ermöglichen. Innerhalb dieser statischen Klasse werde ich die Implementierung der Schnittstelle lokalisieren und die Einstellungen abrufen.

Ich weiß, dass der Servicestandort von den meisten Anbietern einen schnellen Daumen runter hat, aber seien wir ehrlich: Eine Abhängigkeit durch einen Konstrukteur 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 an Schnittstellen und Ermöglichung mehrerer Implementierungen, während der Komfort (in sorgfältig gemessenen und entsprechend wenigen Situationen) eines statischen Singleton-Einstiegspunkts erhalten bleibt.

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

Ich finde, diese Mischung ist die beste aller Welten.


3

Ja, wie Sie festgestellt haben, erschwert ein globales Konfigurationsobjekt das Testen von Einheiten. Einen "geheimen" Setter für Unit-Tests zu haben, ist ein schneller Hack, der, obwohl er nicht nett ist, sehr nützlich sein kann: Er ermöglicht es Ihnen, Unit-Tests zu schreiben, damit Sie Ihren Code im Laufe der Zeit auf ein besseres Design umgestalten können.

(Obligatorischer Verweis: Effizientes Arbeiten mit Legacy-Code . Er enthält diesen und viele weitere unschätzbare Tricks zum Testen von Legacy-Code.)

Nach meiner Erfahrung ist es am besten, so wenig wie möglich von global config abhängig zu sein. Was aber nicht unbedingt Null ist - es hängt alles 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, sofern die Organisatoren nur das tun, z. B. nicht viel testbaren Code selbst enthalten. Auf diese Weise können Sie die meiste oder die gesamte testbare wichtige Funktionalität der App testen, ohne das vorhandene physische Konfigurationsschema vollständig zu stören.

Dies nähert sich jedoch bereits einer echten Dependency-Injection-Lösung wie Spring for Java. Wenn Sie auf ein solches Framework migrieren können, ist dies in Ordnung. Im wirklichen Leben und insbesondere im Umgang mit einer Legacy-App ist das langsame und sorgfältige Refactoring in Richtung DI häufig der bestmögliche Kompromiss.


Ihr Ansatz hat mir sehr gut gefallen und ich würde nicht empfehlen, die Idee eines Setters "geheim" zu halten oder als "schnellen Hack" zu betrachten. Wenn Sie die Auffassung vertreten, dass Komponententests ein wichtiger Benutzer / Verbraucher / Bestandteil Ihrer Anwendung sind, dann test_ist es nicht mehr so ​​schlimm , über Attribute und Methoden zu verfügen, oder? Ich bin damit einverstanden, dass die echte Lösung des Problems darin besteht, ein DI-Framework zu verwenden, aber in Beispielen wie dem Ihren, wenn Sie mit Legacy-Code oder anderen einfachen Fällen arbeiten, gilt es nicht, Testcode sauber zu verfremden.
Filip Dupanović

1
@kRON, das sind immens nützliche und praktische Tricks, um ältere Codeeinheiten testbar zu machen, und ich benutze sie auch ausgiebig. Im Idealfall sollte der Produktionscode jedoch keine Funktionen enthalten, die ausschließlich zu Testzwecken eingeführt wurden. Langfristig ist dies der Punkt, auf den ich umgestalte.
Péter Török

2

Nach meiner Erfahrung ist dies in der Java-Welt das, wofür Spring gedacht ist. Es erstellt und verwaltet Objekte (Beans) basierend auf bestimmten Eigenschaften, die zur Laufzeit festgelegt werden, und ist ansonsten für Ihre Anwendung transparent. Sie können mit der test.config-Datei gehen, die Anon. Erwähnungen oder Sie können eine Logik in die Konfigurationsdatei einfügen, mit der Spring Eigenschaften basierend auf einem anderen Schlüssel (z. B. Hostname oder Umgebung) festlegt.

Was Ihr zweites Problem angeht, können Sie das durch eine Rearchitektur umgehen, aber es ist nicht so schwerwiegend, wie es scheint. Dies bedeutet in Ihrem Fall, dass Sie beispielsweise kein globales Konfigurationsobjekt haben, das von verschiedenen anderen Klassen verwendet wird. Sie müssen nur diese anderen Klassen über Spring konfigurieren und haben dann kein Konfigurationsobjekt, da alles, was konfiguriert werden muss, über Spring erfolgt ist, sodass Sie diese Klassen direkt in Ihrem Code verwenden können.


2

Bei dieser Frage geht es wirklich um ein allgemeineres Problem als um die Konfiguration. Sobald ich das Wort "Singleton" gelesen hatte, dachte ich sofort an alle Probleme, die mit diesem Muster verbunden waren, 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 ist es normalerweise . Wenn Sie jemals darüber nachdenken, ein Singleton-Muster für irgendetwas zu verwenden , sollten Sie Folgendes in Betracht ziehen:

  • Müssen Sie jemals eine Unterklasse erstellen?
  • 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 auch nur ein bisschen 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 überhaupt jede instanzlose Klasse) bieten kann.

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


0

Eine Möglichkeit, wie ich dies in C # gehandhabt habe, besteht darin, ein Singleton-Objekt zu haben, das während der Instanzerstellung gesperrt wird und alle Daten gleichzeitig zur Verwendung durch alle Client-Objekte initialisiert. Wenn dieser Singleton Schlüssel / Wert-Paare verarbeiten kann, können Sie jede Art von Daten speichern, einschließlich komplexer Schlüssel, die von vielen verschiedenen Clients und Client-Typen verwendet werden. Wenn Sie es dynamisch halten und bei Bedarf neue Client-Daten laden möchten, können Sie das Vorhandensein eines Client-Hauptschlüssels überprüfen und, falls nicht vorhanden, die Daten für diesen Client laden und an das Hauptwörterbuch anhängen. Es ist auch möglich, dass das primäre Konfigurations-Singleton Sätze von Clientdaten enthält, bei denen mehrere Clients desselben Clienttyps dieselben Daten verwenden, auf die alle über das primäre Konfigurations-Singleton zugegriffen wurde. 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 / Wert-Konfigurationen als auch spezifische Objekt-APIs verwendet, je nachdem, welche benötigt wurden.

Eines 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. Dadurch wird verhindert, dass Lesevorgänge in anderen Threads blockiert werden.

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


0

Die Klasse, die Sie beschreiben, klingt wie ein Gegenmuster eines Gott-Objekts, nur 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 zutreffenden Grund. Singletons sollten nur verwendet werden, wenn das Vorhandensein mehrerer Objekte zu einem schlechten Zustand führt. In diesem Fall ist die Verwendung eines Singleton ungeeignet, 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 mehr Klassen zugelassen werden, als direkt auf die Daten zugreifen müssen.


0

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

Ich wurde in dieser Angelegenheit von Joshua Flanagan von Los Techies inspiriert; er hat vor einiger Zeit einen Artikel dazu geschrieben.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.