Muster für eine Klasse, die nur eines tut

24

Nehmen wir an, ich habe eine Prozedur, die Sachen macht :

void doStuff(initalParams) {
    ...
}

Jetzt entdecke ich, dass "Sachen machen" eine ziemlich komplizierte Operation ist. Die Prozedur wird groß, ich teile sie in mehrere kleinere Prozeduren auf und bald merke ich, dass es nützlich wäre, wenn ich einen Zustand habe , damit ich weniger Parameter zwischen den kleinen Prozeduren übergeben muss. Also zerlege ich es in eine eigene Klasse:

class StuffDoer {
    private someInternalState;

    public Start(initalParams) {
        ...
    }

    // some private helper procedures here
    ...
}

Und dann nenne ich es so:

new StuffDoer().Start(initialParams);

oder so:

new StuffDoer(initialParams).Start();

Und das fühlt sich falsch an. Wenn ich die .NET- oder Java-API verwende, rufe ich nie auf new SomeApiClass().Start(...);, was den Verdacht aufwirft, dass ich es falsch mache. Klar, ich könnte den Konstruktor von StuffDoer privat machen und eine statische Hilfsmethode hinzufügen:

public static DoStuff(initalParams) {
    new StuffDoer().Start(initialParams);
}

Aber dann hätte ich eine Klasse, deren externe Schnittstelle nur aus einer statischen Methode besteht, was sich auch komisch anfühlt.

Daher meine Frage: Gibt es ein gut etabliertes Muster für diese Art von Klassen, dass

  • habe nur einen Einstiegspunkt und
  • keinen "extern erkennbaren" Zustand haben, dh der Instanzzustand wird nur während der Ausführung dieses einen Eintrittspunktes benötigt?
Heinzi
quelle
Es ist bekannt, dass ich Dinge tue, bool arrayContainsSomestring = new List<string>(stringArray).Contains("somestring");bei denen es mir nur darum ging, dass bestimmte Informationen und die LINQ-Erweiterungsmethoden nicht verfügbar sind. Funktioniert einwandfrei und passt in eine if()Situation, in der Sie nicht durch Reifen springen müssen. Natürlich möchten Sie eine müllsammelnde Sprache, wenn Sie Code wie diesen schreiben.
ein Lebenslauf vom
Wenn es sprachunabhängig ist , warum sollten Sie dann auch Java und C # hinzufügen ? :)
Steven Jeuris
Ich bin gespannt, was ist die Funktionalität dieser "One Method"? Es wäre sehr hilfreich zu bestimmen, was der beste Ansatz in dem spezifischen Szenario ist, aber dennoch eine interessante Frage!
Steven Jeuris
@StevenJeuris: Ich bin in verschiedenen Situationen auf dieses Problem gestoßen, aber im aktuellen Fall handelt es sich um eine Methode, mit der Daten in die lokale Datenbank importiert werden (sie werden von einem Webserver geladen, bearbeitet und in der Datenbank gespeichert).
Heinzi
1
Wenn Sie die Methode beim Erstellen der Instanz immer aufrufen, können Sie sie dann zu einer Initialisierungslogik innerhalb des Konstruktors machen.
NoChance

Antworten:

29

Es gibt ein Muster namens Method Object, bei dem Sie eine einzelne große Methode mit vielen temporären Variablen / Argumenten in eine separate Klasse zerlegen. Sie tun dies, anstatt nur Teile der Methode in separate Methoden zu extrahieren, da diese Zugriff auf den lokalen Status (die Parameter und temporären Variablen) benötigen, und Sie können den lokalen Status nicht mithilfe von Instanzvariablen gemeinsam nutzen, da sie für diese Methode lokal wären (und die daraus extrahierten Methoden) und würden vom Rest des Objekts nicht verwendet.

Stattdessen wird die Methode zu einer eigenen Klasse, die Parameter und temporären Variablen werden zu Instanzvariablen dieser neuen Klasse, und dann wird die Methode in kleinere Methoden der neuen Klasse aufgeteilt. Die resultierende Klasse verfügt normalerweise nur über eine einzige öffentliche Instanzmethode, die die von der Klasse gekapselte Aufgabe ausführt.

anon
quelle
1
Das klingt genau so, wie ich es tue. Danke, zumindest habe ich jetzt einen Namen dafür. :-)
Heinzi
2
Sehr relevanter Link! Es riecht für mich nach einem Anti-Muster. Wenn Ihre Methode so lang ist, könnten vielleicht Teile davon in wiederverwendbaren Klassen extrahiert oder allgemein besser überarbeitet werden? Ich habe auch noch nie von diesem Muster gehört, und ich kann auch nicht viele andere Ressourcen finden, weshalb ich glaube, dass es im Allgemeinen nicht so häufig verwendet wird.
Steven Jeuris
1
@StevenJeuris Ich glaube nicht, dass es ein Anti-Pattern ist. Ein Anti-Muster wäre, umgestaltete Methoden mit Chargenparametern zu überfrachten, anstatt diesen Zustand, den diese Methoden gemeinsam haben, in einer Instanzvariablen zu speichern.
Alfredo Osorio
@AlfredoOsorio: Nun, es überfrachtet überarbeitete Methoden mit vielen Parametern. Sie leben im Objekt, es ist also besser, als sie alle weiterzugeben, aber es ist schlimmer als die Umgestaltung auf wiederverwendbare Komponenten, für die jeweils nur eine begrenzte Teilmenge der Parameter erforderlich ist.
Jan Hudec
13

Ich würde sagen, dass die betreffende Klasse (unter Verwendung eines einzelnen Einstiegspunkts) gut gestaltet ist. Es ist einfach zu bedienen und zu erweitern. Ganz SOLID.

Gleiches für no "externally recognizable" state. Es ist eine gute Verkapselung.

Ich sehe kein Problem mit Code wie:

var doer = new StuffDoer(initialParams);
var result = doer.Calculate(extraParams);
jgauffin
quelle
1
Und natürlich, wenn Sie nicht dasselbe doererneut verwenden müssen, reduziert sich dies auf var result = new StuffDoer(initialParams).Calculate(extraParams);.
einen Lebenslauf vom
Calculate sollte in doStuff umbenannt werden!
Shabunc
1
Ich würde hinzufügen, dass, wenn Sie Ihre Klasse nicht dort initialisieren (neu) möchten, wo Sie sie verwenden (wahrscheinlich möchten Sie eine Factory verwenden), Sie die Option haben, das Objekt an einer anderen Stelle in Ihrer Geschäftslogik zu erstellen und die Parameter dann direkt zu übergeben auf die einzige öffentliche Methode, die Sie haben. Die andere Möglichkeit besteht darin, eine Factory zu verwenden und eine make-Methode zu verwenden, die die Parameter abruft und Ihr Objekt mit allen Parametern über den Konstruktor erstellt. Dann rufen Sie Ihre öffentliche Methode ohne Parameter von jedem beliebigen Ort aus auf.
Patkos Csaba
2
Diese Antwort ist zu simpel. Ist eine StringComparerKlasse, die du verwendest, new StringComparer().Compare( "bleh", "blah" )gut gestaltet? Was ist mit der Schaffung eines Staates, wenn das überhaupt nicht nötig ist? Okay, das Verhalten ist gut gekapselt (zumindest für den Benutzer der Klasse, mehr dazu in meiner Antwort ), aber das macht es nicht standardmäßig zu 'gutem Design'. Als Beispiel für Ihr "Macher-Ergebnis". Dies würde nur Sinn machen, wenn Sie jemals doergetrennt von verwenden würden result.
Steven Jeuris
1
Es ist nicht ein Grund zu existieren, es ist one reason to change. Der Unterschied mag subtil erscheinen. Sie müssen verstehen, was das bedeutet. Es wird niemals eine Methodenklasse erstellen
jgauffin
8

Aus der SOLID Prinzipien Perspektive Antwort jgauffin die Sinn macht. Vergessen Sie jedoch nicht die allgemeinen Gestaltungsprinzipien, z . B. das Ausblenden von Informationen .

Ich sehe mehrere Probleme mit dem angegebenen Ansatz:

  • Wie Sie selbst bemerkt haben, erwarten die Leute nicht, das Schlüsselwort 'new' zu verwenden, wenn das erstellte Objekt keinen Status verwaltet . Ihr Design spiegelt seine Absicht wider. Benutzer Ihrer Klasse können sich nicht sicher sein, welchen Status sie verwaltet und ob nachfolgende Aufrufe der Methode zu einem anderen Verhalten führen können.
  • Aus der Perspektive der Person, die die Klasse benutzt, ist der innere Zustand gut verborgen, aber wenn Sie Änderungen an der Klasse vornehmen oder sie einfach verstehen wollen, machen Sie die Dinge komplexer. Ich habe bereits viel über die Probleme geschrieben, die ich mit Aufteilungsmethoden sehe, um sie zu verkleinern , insbesondere, wenn der Status in den Klassenbereich verschoben wird. Sie ändern die Art und Weise, wie Ihre API verwendet werden soll, um kleinere Funktionen zu haben! Das geht meiner Meinung nach definitiv zu weit.

Einige verwandte Referenzen

Möglicherweise liegt ein Hauptargument darin, wie weit das Prinzip der einheitlichen Verantwortung gedehnt werden kann . "Wenn Sie es so extrem nehmen und Klassen bauen, die einen Grund haben zu existieren, haben Sie möglicherweise nur eine Methode pro Klasse. Dies würde eine große Zahl von Klassen verursachen, selbst für die einfachsten Prozesse, was dazu führen würde, dass das System existiert schwer zu verstehen und schwer zu ändern. "

Ein weiterer relevanter Verweis zu diesem Thema: "Unterteilen Sie Ihre Programme in Methoden, die eine identifizierbare Aufgabe ausführen. Halten Sie alle Operationen in einer Methode auf derselben Abstraktionsebene ." - Kent Beck Key ist hier "dieselbe Abstraktionsebene". Das bedeutet nicht "eins", wie es oft interpretiert wird. Diese Abstraktionsebene hängt vollständig von dem Kontext ab, für den Sie entwerfen.

Was ist der richtige Ansatz?

Ohne Ihren konkreten Anwendungsfall zu kennen, ist es schwer zu sagen. Es gibt ein Szenario, in dem ich manchmal (nicht oft) einen ähnlichen Ansatz verwende. Wenn ich einen Datensatz verarbeiten möchte, ohne diese Funktionalität für den gesamten Klassenbereich verfügbar machen zu wollen. Ich habe einen Blog-Beitrag darüber geschrieben, wie Lambdas die Kapselung noch weiter verbessern können . Ich habe auch eine Frage zum Thema hier auf Programmierer gestartet . Das Folgende ist ein aktuelles Beispiel für die Verwendung dieser Technik.

new TupleList<Key, int>
{
    { Key.NumPad1, 1 },
            ...
    { Key.NumPad3, 16 },
    { Key.NumPad4, 17 },
}
    .ForEach( t =>
    {
        var trigger = new IC.Trigger.EventTrigger(
                        new KeyInputCondition( t.Item1, KeyInputCondition.KeyState.Down ) );
        trigger.ConditionsMet += () => AddMarker( t.Item2 );
        _inputController.AddTrigger( trigger );
    } );

Da der örtliche Code im ForEachnirgendwo anders wiederverwendet wird, kann ich ihn einfach genau dort aufbewahren, wo er relevant ist. Code so zu skizzieren, dass aufeinander aufbauender Code stark gruppiert ist, macht ihn meiner Meinung nach besser lesbar.

Mögliche Alternativen

  • In C # könnten Sie stattdessen Erweiterungsmethoden verwenden. Arbeiten Sie also direkt mit dem Argument, das Sie an diese 'one thing'-Methode übergeben.
  • Prüfen Sie, ob diese Funktion tatsächlich zu keiner anderen Klasse gehört.
  • Machen Sie es zu einer statischen Funktion in einer statischen Klasse . Dies ist wahrscheinlich der am besten geeignete Ansatz, der sich auch in den allgemeinen APIs widerspiegelt, auf die Sie verwiesen haben.
Steven Jeuris
quelle
Ich habe einen möglichen Namen für dieses Anti-Muster gefunden, wie in einer anderen Antwort, Poltergeisten, beschrieben .
Steven Jeuris
4

Ich würde sagen, es ist ziemlich gut, aber Ihre API sollte immer noch eine statische Methode für eine statische Klasse sein, da der Benutzer dies erwartet. Die Tatsache, dass die statische Methode verwendet wird new, um Ihr Hilfsobjekt zu erstellen und die Arbeit zu erledigen, ist ein Implementierungsdetail, das vor jedem, der es aufruft, verborgen werden sollte.

Scott Whitlock
quelle
Wow! Sie haben es viel prägnanter geschafft als ich. :) Vielen Dank. (Natürlich gehe ich etwas weiter, wo wir uns nicht einig sind, aber ich stimme der allgemeinen Prämisse zu)
Steven Jeuris
2
new StuffDoer().Start(initialParams);

Es gibt ein Problem mit ahnungslosen Entwicklern, die eine gemeinsam genutzte Instanz verwenden und verwenden könnten

  1. mehrmals (ok, wenn die vorherige (möglicherweise teilweise) Ausführung die spätere Ausführung nicht durcheinander bringt)
  2. von mehreren Threads (nicht OK, Sie haben ausdrücklich gesagt, dass es internen Status hat).

Hierfür ist eine explizite Dokumentation erforderlich, die besagt, dass es nicht threadsicher ist. Wenn es kompakt ist (schnell erstellt, weder externe Ressourcen noch viel Speicher belegt), ist es in Ordnung, es im laufenden Betrieb zu instanziieren.

Das Ausblenden in einer statischen Methode hilft dabei, da die Instanz dann nie wiederverwendet wird.

Wenn es einige teure Initialisierung hat, es könnte (es ist nicht immer) von Vorteil sein, bereiten Sie es einmal initialisiert und es mehrmals unter Verwendung von einer anderen Klasse zu schaffen, Staat nur enthalten würde und es klonbar machen. Durch die Initialisierung würde ein Status erstellt, in dem gespeichert wird doer. start()würde es klonen und an interne Methoden übergeben.

Dies würde auch andere Dinge wie den anhaltenden Zustand der teilweisen Ausführung ermöglichen. (Wenn es lange dauert und externe Faktoren, z. B. Stromausfall, die Ausführung unterbrechen könnten.) Aber diese zusätzlichen ausgefallenen Dinge werden normalerweise nicht benötigt, sind es also nicht wert.

user470365
quelle
Gute Argumente. Ich hoffe, die Bereinigung macht Ihnen nichts aus.
Steven Jeuris
2

Abgesehen von meiner ausführlicheren, persönlicheren Antwort habe ich das Gefühl, dass die folgende Bemerkung eine separate Antwort verdient.

Es gibt Hinweise darauf, dass Sie dem Anti-Muster der Poltergeisten folgen .

Poltergeister sind Klassen mit sehr begrenzten Rollen und effektiven Lebenszyklen. Sie starten oft Prozesse für andere Objekte. Die überarbeitete Lösung beinhaltet eine Neuverteilung der Zuständigkeiten auf längerlebige Objekte, die die Poltergeisten eliminieren.

Symptome und Folgen

  • Redundante Navigationspfade.
  • Transiente Assoziationen.
  • Staatenlose Klassen.
  • Temporäre Objekte und Klassen von kurzer Dauer.
  • Einzeloperationsklassen, die nur existieren, um andere Klassen durch temporäre Assoziationen zu "setzen" oder "aufzurufen".
  • Klassen mit steuerelementähnlichen Operationsnamen wie start_process_alpha.

Refactored Lösung

Ghostbusters lösen Poltergeister, indem sie sie vollständig aus der Klassenhierarchie entfernen. Nach ihrer Entfernung muss jedoch die vom Poltergeist „bereitgestellte“ Funktionalität ersetzt werden. Dies ist mit einer einfachen Anpassung zur Korrektur der Architektur einfach.

Steven Jeuris
quelle