Zähmung der 'Utility Functions'-Klassen

15

In unserer Java-Codebasis sehe ich immer wieder folgendes Muster:

/**
 This is a stateless utility class
 that groups useful foo-related operations, often with side effects.
*/
public class FooUtil {
  public int foo(...) {...}
  public void bar(...) {...}
}

/**
 This class does applied foo-related things.
*/
class FooSomething {
  int DoBusinessWithFoo(FooUtil fooUtil, ...) {
    if (fooUtil.foo(...)) fooUtil.bar(...);
  }
}

Was mich stört ist, dass ich von FooUtilüberall her eine Instanz bestehen muss , um zu testen.

  • Ich kann die FooUtilMethoden nicht statisch machen, weil ich sie nicht zum Testen der beiden FooUtilKlassen und ihrer Clientklassen verspotten kann .
  • Ich kann mit a keine Instanz von FooUtilam Ort des Verbrauchs erstellen new, da ich sie zum Testen nicht verspotten kann.

Ich nehme an, meine beste Wette ist die Injektion (und das tue ich), aber es fügt seine eigenen Probleme hinzu. Wenn Sie mehrere util-Instanzen übergeben, wird auch die Größe der Methodenparameterlisten erhöht.

Gibt es eine Möglichkeit, besser damit umzugehen, die ich nicht sehe?

Update: Da die Utility-Klassen zustandslos sind, könnte ich wahrscheinlich ein statisches Singleton- INSTANCEMember oder eine statische getInstance()Methode hinzufügen , während die Möglichkeit erhalten bleibt, das zugrunde liegende statische Feld der Klasse zum Testen zu aktualisieren. Scheint auch nicht super sauber zu sein.

9000
quelle
4
Und Ihre Annahme, dass alle Abhängigkeiten verspottet werden müssen, um Unit-Tests effektiv durchführen zu können, ist nicht korrekt (obwohl ich immer noch nach der Frage suche, die das bespricht).
Telastyn,
4
Warum gehören diese Methoden nicht zur Klasse, deren Daten sie bearbeiten?
Jules
3
Habe einen separaten Junit-Satz, der die statischen fns bei FooUtility aufruft. Dann können Sie es verwenden, ohne sich zu lustig zu machen. Wenn es etwas ist, das Sie verspotten müssen, dann vermute ich, dass es nicht nur ein Dienstprogramm ist, sondern eine IO- oder Geschäftslogik ausführt und tatsächlich eine Klassenmitgliedsreferenz sein und über eine Konstruktor-, Init- oder Setter-Methode initialisiert werden sollte.
tgkprog
2
+1 für Jules Kommentar. Util-Klassen sind ein Codegeruch. Abhängig von der Semantik sollte es eine bessere Lösung geben. Vielleicht sollten die FooUtilMethoden eingefügt werden FooSomething, vielleicht gibt es Eigenschaften FooSomething, die geändert / erweitert werden sollten, damit die FooUtilMethoden nicht mehr benötigt werden. Bitte geben Sie uns ein konkreteres Beispiel dafür, was FooSomethinguns bei der Beantwortung dieser Fragen helfen kann.
Valentinstag
13
@valenterry: Der einzige Grund, warum util-Klassen ein Code-Geruch sind, ist, dass Java keine gute Möglichkeit bietet, eigenständige Funktionen zu verwenden. Es gibt sehr gute Gründe, Funktionen zu haben, die nicht für Klassen spezifisch sind.
Robert Harvey

Antworten:

16

Lassen Sie mich zunächst sagen, dass es unterschiedliche Programmieransätze für unterschiedliche Probleme gibt. Für meine Arbeit bevorzuge ich ein starkes OO-Design für Organisation und Wartung, und meine Antwort spiegelt dies wider. Ein funktionaler Ansatz hätte eine ganz andere Antwort.

Ich finde, dass die meisten Utility-Klassen erstellt werden, weil das Objekt, auf dem sich die Methoden befinden sollten, nicht vorhanden ist. Dies kommt fast immer aus einem von zwei Fällen: Entweder gibt jemand Sammlungen weiter oder jemand gibt Bohnen weiter.

Wenn Sie eine Sammlung haben, die Sie weitergeben, muss es Code geben, der Teil dieser Sammlung sein soll. Dies wird dann im gesamten System verteilt und schließlich in Dienstprogrammklassen zusammengefasst.

Mein Vorschlag ist, Ihre Sammlungen (oder Beans) mit anderen eng verknüpften Daten in neue Klassen zu verpacken und den "Utility" -Code in tatsächliche Objekte einzufügen.

Sie könnten die Auflistung erweitern und die Methoden dort hinzufügen, verlieren aber auf diese Weise die Kontrolle über den Status Ihres Objekts. Ich schlage vor, Sie kapseln ihn vollständig und machen nur die erforderlichen Geschäftslogikmethoden verfügbar (denken Sie: "Fragen Sie Ihre Objekte nicht nach ihren Daten." Geben Sie Ihren Objekten Befehle und lassen Sie sie auf ihre privaten Daten einwirken ", ein wichtiger OO-Mandant, der, wenn Sie darüber nachdenken, den hier vorgeschlagenen Ansatz erfordert.

Dieses "Umschließen" ist nützlich für allgemeine Bibliotheksobjekte, die Daten enthalten, denen Sie jedoch keinen Code hinzufügen können. Das Einfügen von Code in die von ihm bearbeiteten Daten ist ein weiteres wichtiges Konzept im OO-Entwurf.

Es gibt viele Codierungsstile, bei denen dieses Umbruchkonzept eine schlechte Idee wäre - wer möchte eine ganze Klasse schreiben, wenn er nur ein einmaliges Skript oder einen Test implementiert? Ich bin jedoch der Meinung, dass die Kapselung von Basistypen / Sammlungen, denen Sie keinen Code hinzufügen können, für OO obligatorisch ist.

Bill K
quelle
Diese Antwort ist so fantastisch. Ich hatte dieses Problem (offensichtlich), las diese Antwort mit einiger Zweifel, ging zurück und schaute meinen Code an und fragte "welches Objekt fehlt, das diese Methoden haben sollte". Siehe da, elegante Lösung gefunden und Objektmodell-Lücke gefüllt.
GreenAsJade
@Deduplicator In 40 Jahren der Programmierung habe ich selten (nie?) Den Fall gesehen, in dem ich durch zu viele Indirektionsebenen blockiert wurde (abgesehen von dem gelegentlichen Kampf mit meiner IDE, um zu einer Implementierung anstatt zu einer Schnittstelle zu springen), aber ich Wir haben oft (sehr oft) den Fall gesehen, in dem Leute schrecklichen Code geschrieben haben, anstatt eine eindeutig benötigte Klasse zu erstellen. Obwohl ich glaube, dass zu viel Indirektion einige Leute gebissen haben könnte, würde ich Fehler auf der Seite der richtigen OO-Prinzipien machen, wie zum Beispiel, dass jede Klasse eine Sache gut macht, die richtige Eindämmung, etc.
Bill K
@ BillK Im Allgemeinen eine gute Idee. Aber ohne eine Notluke kann die Kapselung wirklich stören. Und es gibt eine gewisse Schönheit bei freien Algorithmen, die Sie jedem gewünschten Typ zuordnen können, obwohl OO zugegebenermaßen in strengen OO-Sprachen alles andere als peinlich ist.
Deduplizierer
@Deduplicator Beim Schreiben von "Utility" -Funktionen, die außerhalb der spezifischen Geschäftsanforderungen geschrieben werden müssen, sind Sie richtig. Funktionsreiche Dienstprogrammklassen (z. B. Sammlungen) sind in OO einfach umständlich, da sie nicht mit Daten verknüpft sind. Dies sind die "Escape Hatch", die Leute verwenden, die mit OO nicht vertraut sind (Tool-Anbieter müssen diese auch für sehr allgemeine Funktionen verwenden). Mein Vorschlag oben war, diese Hacks in Klassen einzuschließen, wenn Sie an Geschäftslogik arbeiten, damit Sie Ihren Code wieder mit Ihren Daten verknüpfen können.
Bill K
1

Sie können ein Provider-Muster verwenden und FooSomethingein FooUtilProviderObjekt in Ihr einfügen (möglicherweise in einem Konstruktor; nur einmal).

FooUtilProviderwäre nur eine Methode haben: FooUtils get();. Die Klasse würde dann die von ihr bereitgestellte FooUtilsInstanz verwenden.

Dies ist nur ein Schritt von der Verwendung eines Abhängigkeitsinjektions-Frameworks entfernt, wenn Sie DI-Cointainer nur noch zweimal verbinden müssen: für Produktionscode und für Ihre Testsuite. Sie binden die FooUtilsSchnittstelle einfach an RealFooUtilsoder MockedFooUtils, und der Rest geschieht automatisch. Jedes Objekt, das davon abhängt, FooUtilsProvidererhält die richtige Version von FooUtils.

Konrad Morawski
quelle