Was sind die gültigen Verwendungen von statischen Klassen?

8

Ich habe festgestellt, dass fast jedes Mal, wenn ich Programmierer sehe, die statische Klassen in objektorientierten Sprachen wie C # verwenden, sie es falsch machen. Die Hauptprobleme sind offensichtlich der globale Zustand und die Schwierigkeit, Implementierungen zur Laufzeit oder mit Mocks / Stubs während der Tests auszutauschen.

Aus Neugier habe ich mir einige meiner Projekte angesehen und diejenigen ausgewählt, die gut getestet sind, und als ich mich bemühte, tatsächlich über Architektur und Design nachzudenken. Die zwei Verwendungen von statischen Klassen, die ich gefunden habe, sind:

  • Die Utility-Klasse - etwas, das ich lieber vermeiden würde, wenn ich diesen Code heute schreiben würde.

  • Der Anwendungscache: Die Verwendung einer statischen Klasse ist eindeutig falsch. Was ist, wenn ich es durch MemoryCacheoder Redis ersetzen möchte ?

In .NET Framework sehe ich auch kein Beispiel für eine gültige Verwendung statischer Klassen. Zum Beispiel:

  • FileDie statische Klasse macht es wirklich schmerzhaft, zu Alternativen zu wechseln. Was ist beispielsweise, wenn Sie zu Isolated Storage wechseln oder Dateien direkt im Speicher speichern müssen oder einen Anbieter benötigen, der NTFS-Transaktionen unterstützt oder zumindest Pfade mit mehr als 259 Zeichen verarbeiten kann ? Nachdem eine gemeinsame Schnittstelle und mehrere Implementierungen erscheint so einfach wie mit Filedirekt, mit dem Vorteil nicht die meisten der Code - Basis , wenn Anforderungen ändern neu schreiben zu müssen.

  • ConsoleDie statische Klasse macht das Testen zu kompliziert. Wie soll ich innerhalb eines Komponententests sicherstellen, dass eine Methode korrekte Daten an die Konsole ausgibt? Sollte ich die Methode ändern, um die Ausgabe an eine beschreibbare Datei zu senden, die zur StreamLaufzeit eingefügt wird? Es scheint, dass eine nicht statische Konsolenklasse wieder so einfach wie eine statische wäre, nur ohne alle Nachteile einer statischen Klasse.

  • MathDie statische Klasse ist auch kein guter Kandidat. Was ist, wenn ich auf Arithmetik mit beliebiger Genauigkeit umschalten muss? Oder zu einer neueren, schnelleren Implementierung? Oder zu einem Stub, der während eines Unit-Tests feststellt, dass eine bestimmte Methode einer MathKlasse tatsächlich während eines Tests aufgerufen wurde?

Auf Programmer.SE habe ich gelesen:

Was sind neben den Erweiterungsmethoden die gültigen Verwendungszwecke statischer Klassen, dh Fälle, in denen Abhängigkeitsinjektion oder Singletons entweder unmöglich sind oder zu einem Design mit geringerer Qualität und einer schwierigeren Erweiterbarkeit und Wartung führen?

Arseni Mourzenko
quelle
in Groovy ? "Wenn die von Ihnen getestete Groovy-Klasse eine statische Methode für eine andere Groovy-Klasse aufruft, können Sie die ExpandoMetaClass verwenden, mit der Sie Methoden, Konstruktoren, Eigenschaften und statische Methoden dynamisch hinzufügen können ..."
gnat
Dies beantwortet Ihre Frage nicht (weshalb es sich um einen Kommentar handelt) und es wird wahrscheinlich eine unpopuläre Meinung sein, aber ich denke, Sie suchen nach einem Grad an Modularität, den Java / C # einfach nicht bieten kann (ohne durchzuspringen) mehr Reifen als ein Zirkustier). Sie haben bereits festgestellt, dass eine statische Methode eine fest codierte Abhängigkeit ist, und im Allgemeinen ist jede Klasse eine fest codierte Abhängigkeit.
Doval
1
Sie könnten also alles hinter eine Schnittstelle stellen und sie als Module behandeln, aber dann benötigen Sie Adapter, um von einer Schnittstelle in eine andere zu konvertieren, wenn Sie Fremdcode einbringen oder wenn Sie verschiedene Funktionen in einer Schnittstelle zusammenfügen oder eine Teilmenge auswählen möchten der Funktionen einer Schnittstelle. Und Sie verlieren die Fähigkeit, nützliche binäre Methoden zu haben (zumindest ohne zu schummeln instanceof), weil es in zwei Fällen IFookeine Garantie gibt, dass sie von derselben Klasse unterstützt werden.
Doval
2
@Magus Das oder Sie Ihre Arme werfen, sagen wir YAGNI, und akzeptieren , dass , wenn Sie jemals eine andere Art von String benötigen, Sie erhalten nur Ihre IDE jede Instanz zu ändern , com.lang.lib.Stringum com.someones.elses.Stringin der gesamten Codebasis.
Doval

Antworten:

20

was, wenn; was, wenn; was, wenn?

YAGNI

Ernsthaft. Wenn jemand verschiedene Implementierungen von Fileoder Mathoder verwenden möchte, Consolekann er den Schmerz durchmachen, dies weg zu abstrahieren. Hast du Java gesehen / benutzt?!? Die Java-Standardbibliotheken sind ein gutes Beispiel dafür, was passiert, wenn Sie Dinge zum Zwecke der Abstraktion abstrahieren. Muss ich wirklich drei Schritte ausführen, um mein Kalenderobjekt zu erstellen ?

Alles in allem werde ich nicht hier sitzen und statische Klassen stark verteidigen. Der statische Zustand ist absolut böse (auch wenn ich ihn gelegentlich immer noch verwende, um Dinge zu bündeln / zwischenzuspeichern). Statische Funktionen können aufgrund von Problemen mit Parallelität und Testbarkeit böse sein.

Aber reine statische Funktionen sind nicht so schrecklich. Es gibt elementare Dinge, deren Abstraktion keinen Sinn macht, da es keine vernünftige Alternative für die Operation gibt. Es gibt Implementierungen einer Strategie, die statisch sein können, da sie bereits über den Delegaten / Funktor abstrahiert wurden. Und manchmal haben diese Funktionen keine Instanzklasse, der sie zugeordnet werden können. Daher sind statische Klassen in Ordnung, um sie zu organisieren.

Erweiterungsmethoden sind auch eine praktische Anwendung, auch wenn sie keine philosophisch statischen Klassen sind.

Telastyn
quelle
1
Ein Teil der Frage scheint die Beschwerde gewesen zu sein, dass es zwar Leute gibt, die sagen, dass es gültige Verwendungen für statische Klassen gibt, diese Aussage jedoch nicht mit Beispielen untermauert wird. Diese Antwort könnte verbessert werden, wenn dies der Fall wäre.
Magus
@Magus - Die 3 Beispiele in der Frage sind vollkommen in Ordnung, obwohl das OP anders argumentiert.
Telastyn
Ich denke nicht, dass es eine schlechte Antwort ist, nur dass es Raum für Verbesserungen gibt. Wenn die Beispiele im OP die einzigen sind, ist das in Ordnung. Es wäre sinnvoll, dies in der Antwort zu sagen, um zukünftige Suchen durchführen zu können. In jedem Fall hat es meine Gegenstimme.
Magus
1
Ich wünschte, Frameworks würden den statischen Status des Threads etwas besser unterstützen, als ihn als nachträglichen Gedanken zu betrachten. So etwas Consolesollte eigentlich nicht Teil des systemweiten Zustands sein, aber auch nicht Teil eines Objekts, das ständig herumgereicht wird. Stattdessen scheint es sinnvoller zu sein zu sagen, dass jeder Thread eine "aktuelle Konsole" -Eigenschaft hat, die einfach gespeichert und wiederhergestellt werden kann, wenn eine Routine erforderlich ist, die schreibt, Consoleum stattdessen etwas anderes zu verwenden.
Supercat
1
@BenjaminHodgson - Design ist selten quantitativ. Auf jeden Fall könnten Erweiterungsmethoden anders implementiert werden (denken Sie an die Manipulation von JavaScript-Prototypen), und Benutzer wären nicht klüger. Dass es sich um statische Funktionen handelt, ist ein Implementierungsdetail und kein wesentlicher Bestandteil ihrer Natur.
Telastyn
18

Mit einem Wort Einfachheit.

Wenn Sie zu viel entkoppeln, erhalten Sie die Hallo-Welt aus der Hölle.

void main(String[] args) {
    TextOutputFactory outputFactory = new TextOutputFactory();
    OutputStream stream = outputFactory.CreateStdOutputStream();

    Encoding encoding = new EncodingFactory.CreateUtf8Encoding();
    stream.Encoding = encoding;

    SystemConstant constant = new SystemConstant();
    stream.LineEnding = constant.PlatformLineEnding;

    stream.FlushOnEachLine = true;

    String greeting = new FixedSizeInMemoryString(); // We may have to switch to a file based string later!"
    greeting.SetContent("Hello world");
    greeting.Initialize();

    stream.SendContent(greeting);
    stream.EndCurrentLine();

    ProcessCompletion completion = new ProcessCompletion();
    completion.Status = constant.SuccessStatus;
    completion.ExitProgram();
}

Aber hey, zumindest gibt es keine XML-Konfiguration, was eine nette Geste ist.

Die statische Klasse ermöglicht die Optimierung für den schnellen und allgemeinen Fall. Der meiste Punkt, den Sie erwähnt haben, kann sehr einfach gelöst werden, indem Sie Ihre eigene Abstraktion darüber einführen oder Dinge wie Console.Out verwenden.

Laurent Bourgault-Roy
quelle
5
Dies erinnert mich an die GNU Hello World, die verschiedene Lokalisierungsprobleme berücksichtigt.
Telastyn
1
-1 Für mangel XML - Konfiguration. +2 Um es auf den Punkt zu bringen.
s1lv3r
1
Hello World sollte wirklich verallgemeinert werden, um supraglobale Szenarien zu ermöglichen. Ich meine, ich könnte doch eine andere Welt begrüßen. Schließlich. Wir müssten zuerst eine gemeinsame Form der Kommunikation bestimmen ... Primzahlen in Binärform, die vom AM-Trägerwellensignal gesendet werden ... aber welche Frequenz? Ich weiß, die grundlegende Art der Siliziumatome!
10

Der Fall für statische Methoden: Dies:

var c = Math.Max(a, Int32.Parse(b));

ist viel einfacher und klarer und lesbarer als dies:

IMathLibrary math = new DefaultMathLibrary();
IIntegerParser integerParser = new DefaultIntegerParser();
var c = math.Max(a, integerParser.Parse(b));

Fügen Sie nun die Abhängigkeitsinjektion hinzu, um die expliziten Instanziierungen auszublenden, und sehen Sie, wie der MaxEinzeiler in zehn oder Hunderte von Zeilen wächst - alles, um das unwahrscheinliche Szenario zu unterstützen, dass Sie Ihre eigene Implementierung erfinden, die erheblich schneller ist als die im Framework und die Sie benötigen transparent zwischen den alternativen MaxImplementierungen wechseln zu können.

API-Design ist ein Kompromiss zwischen Einfachheit und Flexibilität. Sie können jederzeit zusätzliche Indirektionsebenen ( ad infinitum ) einführen , müssen jedoch die Bequemlichkeit des allgemeinen Falls gegen den Vorteil zusätzlicher Ebenen abwägen.

Beispielsweise können Sie jederzeit einen eigenen Instanz-Wrapper um die Dateisystem-API schreiben, wenn Sie in der Lage sein müssen, transparent zwischen Speicheranbietern zu wechseln. Ohne die einfacheren statischen Methoden wäre jeder gezwungen, jedes Mal eine Instanz zu erstellen, wenn er eine einfache Aufgabe wie das Speichern einer Datei ausführen müsste. Es wäre wirklich ein schlechter Design-Kompromiss, für einen äußerst seltenen Randfall zu optimieren und alles für den allgemeinen Fall komplizierter zu machen. Das Gleiche gilt für Console- Sie können ganz einfach Ihren eigenen Wrapper erstellen, wenn Sie die Konsole abstrahieren oder verspotten müssen, aber häufig verwenden Sie die Konsole für schnelle Lösungen oder Debugging-Zwecke, sodass Sie nur die einfachste Möglichkeit zur Ausgabe von Text wünschen.

Während "eine gemeinsame Schnittstelle mit mehreren Implementierungen" cool ist, gibt es nicht unbedingt eine "richtige" Abstraktion, die alle gewünschten Speicheranbieter vereint. Möglicherweise möchten Sie zwischen Dateien und isoliertem Speicher wechseln, aber eine andere Person möchte möglicherweise zwischen Dateien, Datenbank und Cookies wechseln, um sie zu speichern. Die gemeinsame Oberfläche würde anders aussehen, und es ist unmöglich, alle möglichen Abstraktionen vorherzusagen, die Benutzer wünschen könnten. Es ist viel flexibler, statische Methoden als "Grundelemente" bereitzustellen und dann Benutzer ihre eigenen Abstraktionen und Wrapper erstellen zu lassen.

Ihr MathBeispiel ist noch zweifelhafter. Wenn Sie zu Mathematik mit beliebiger Genauigkeit wechseln möchten, benötigen Sie vermutlich neue numerische Typen. Dies wäre eine grundlegende Änderung der Du ganzen Programms, und ändern zu müssen , Math.Max()um ArbitraryPrecisionMath.Max()ist sicherlich die am wenigsten von euch Sorgen.

Das heißt, ich stimme zu, dass Stateful statische Klassen sind in den meisten Fällen das Böse.

JacquesB
quelle
0

Im Allgemeinen sind die einzigen Orte, an denen ich statische Klassen verwenden würde, diejenigen, an denen die Klasse reine Funktionen hostet, die keine Systemgrenzen überschreiten. Mir ist klar, dass Letzteres überflüssig ist, da alles, was eine Grenze überschreitet, wahrscheinlich absichtlich nicht rein ist. Ich komme zu diesem Schluss sowohl aus einem Misstrauen gegenüber dem globalen Staat als auch aus einer leicht zu testenden Perspektive. Selbst wenn die begründete Erwartung besteht, dass es eine einzige Implementierung für eine Klasse gibt, die eine Systemgrenze überschreitet (Netzwerk, Dateisystem, E / A-Gerät), verwende ich eher Instanzen als statische Klassen, da ein Mock bereitgestellt werden kann Die gefälschte Implementierung in Komponententests ist entscheidend für die Entwicklung schneller Komponententests ohne Kopplung an tatsächliche Geräte. In Fällen, in denen die Methode eine reine Funktion ohne Kopplung an ein externes System / Gerät ist, kann eine statische Klasse meiner Meinung nach angemessen sein, aber nur, wenn sie die Testbarkeit nicht beeinträchtigt. Ich finde die Anwendungsfälle außerhalb bestehender Framework-Klassen relativ selten. Darüber hinaus kann es Fälle geben, in denen Sie keine andere Wahl haben - beispielsweise Erweiterungsmethoden in C #.

Tvanfosson
quelle