it-swarm.com.de

Sind Klassen mit nur einer (öffentlichen) Methode ein Problem?

Ich arbeite derzeit an einem Softwareprojekt, das die Komprimierung und Indizierung von Videoüberwachungsmaterial durchführt. Bei der Komprimierung werden Hintergrund- und Vordergrundobjekte aufgeteilt, der Hintergrund als statisches Bild und der Vordergrund als Sprite gespeichert.

Vor kurzem habe ich begonnen, einige der Klassen zu überprüfen, die ich für das Projekt entworfen habe.

Mir ist aufgefallen, dass es viele Klassen gibt, die nur eine einzige öffentliche Methode haben. Einige dieser Klassen sind:

  • VideoCompressor (mit einer compress -Methode, die ein Eingabevideo vom Typ RawVideo aufnimmt und ein Ausgabevideo vom Typ CompressedVideo zurückgibt).
  • VideoSplitter (mit einer split -Methode, die ein Eingabevideo vom Typ RawVideo aufnimmt und einen Vektor von 2 Ausgabevideos vom Typ RawVideo zurückgibt).
  • VideoIndexer (mit einer index -Methode, die ein Eingabevideo vom Typ RawVideo aufnimmt und einen Videoindex vom Typ VideoIndex zurückgibt).

Ich finde mich dabei, jede Klasse zu instanziieren, nur um Aufrufe wie VideoCompressor.compress(...), VideoSplitter.split(...), VideoIndexer.index(...) zu tätigen.

An der Oberfläche denke ich, dass die Klassennamen ihre beabsichtigte Funktion ausreichend beschreiben, und sie sind tatsächlich Substantive. Entsprechend sind ihre Methoden auch Verben.

Ist das eigentlich ein Problem?

55
yjwong

Nein, das ist kein Problem, ganz im Gegenteil. Es ist ein Zeichen der Modularität und der klaren Verantwortung der Klasse. Die schlanke Oberfläche ist aus Sicht eines Benutzers dieser Klasse leicht zu erfassen und fördert eine lose Kopplung. Dies hat viele Vorteile, aber fast keine Nachteile. Ich wünschte, mehr Komponenten würden so entworfen!

91
Doc Brown

Es ist nicht mehr objektorientiert. Da diese Klassen nichts darstellen, sind sie nur Gefäße für die Funktionen.

Das heißt nicht, dass es falsch ist. Wenn die Funktionalität ausreichend komplex oder generisch ist (d. H. Die Argumente sind Schnittstellen, keine konkreten endgültigen Typen), ist es sinnvoll, diese Funktionalität in ein separates Modul zu setzen.

Von dort hängt es von Ihrer Sprache ab. Wenn die Sprache freie Funktionen hat, sollten sie Module sein, die Funktionen exportieren. Warum so tun, als wäre es eine Klasse, wenn es nicht so ist? Wenn die Sprache keine freien Funktionen wie z. Java, dann erstellen Sie Klassen mit einer einzigen öffentlichen Methode. Nun, das zeigt nur die Grenzen des objektorientierten Designs. Manchmal passt funktional einfach besser zusammen.

Es gibt einen Fall, in dem Sie möglicherweise eine Klasse mit einer einzelnen öffentlichen Methode benötigen, da sie eine Schnittstelle mit einer einzelnen öffentlichen Methode implementieren muss. Sei es für Beobachtermuster oder Abhängigkeitsinjektion oder was auch immer. Auch hier kommt es auf die Sprache an. In Sprachen mit erstklassigen Funktoren (C++ (std::function oder Vorlagenparameter), C # (Delegat), Python, Perl, Ruby (proc), LISP, Haskell, ...) Diese Muster verwenden Funktionstypen und don brauche keinen Unterricht. Java hat (noch nicht in Version 8) Funktionstypen, daher verwenden Sie einzelne Methodenschnittstellen und entsprechende einzelne Methodenklassen.

Natürlich befürworte ich nicht das Schreiben einer einzigen großen Funktion. Es sollte private Unterroutinen haben, diese können jedoch für die Implementierungsdatei (statischer oder anonymer Namespace auf Dateiebene in C++) oder für eine private Hilfsklasse privat sein, die nur innerhalb der öffentlichen Funktion instanziiert wird ( Zum Speichern von Daten oder nicht) ? ).

28
Jan Hudec

Es kann Gründe geben, eine bestimmte Methode in eine dedizierte Klasse zu extrahieren. Einer dieser Gründe besteht darin, die Abhängigkeitsinjektion zuzulassen.

Stellen Sie sich vor, Sie haben eine Klasse namens VideoExporter, die möglicherweise ein Video komprimieren kann. Ein sauberer Weg wäre eine Schnittstelle:

interface IVideoCompressor
{
    Stream compress(Video video);
}

was so umgesetzt würde:

class MpegVideoCompressor : IVideoCompressor
{
    // ...
}

class FlashVideoCompressor : IVideoCompressor
{
    // ...
}

und so verwendet:

class VideoExporter
{
    // ...
    void export(Destination destination, IVideoCompressor compressor)
    {
        // ...
        destination = compressor(this.source);
        // ...
    }
    // ...
}

Eine schlechte Alternative wäre ein VideoExporter, das viele öffentliche Methoden hat und die ganze Arbeit erledigt, einschließlich der Komprimierung. Es würde schnell zu einem Alptraum für die Wartung werden, was es schwierig macht, Unterstützung für andere Videoformate hinzuzufügen.

14

Dies ist ein Zeichen dafür, dass Sie Funktionen als Argumente an andere Funktionen übergeben möchten. Ich vermute, Ihre Sprache (Java?) Unterstützt sie nicht. Wenn dies der Fall ist, ist es weniger ein Fehler in Ihrem Design als vielmehr ein Mangel in der Sprache Ihrer Wahl. Dies ist eines der größten Probleme mit Sprachen, die darauf bestehen, dass alles eine Klasse sein muss.

Wenn Sie diese Faux-Funktionen nicht wirklich weitergeben, möchten Sie nur eine freie/statische Funktion.

8
Doval

Ich weiß, dass ich zu spät zur Party komme, aber wie es scheint, hat jeder versäumt, darauf hinzuweisen:

Dies ist ein bekanntes Entwurfsmuster mit dem Namen: Strategiemuster .

Das Strategiemuster wird verwendet, wenn es mehrere mögliche Strategien zur Lösung eines Unterproblems gibt. In der Regel definieren Sie eine Schnittstelle, die einen Vertrag für alle Implementierungen erzwingt, und verwenden dann eine Form von Dependency Injection , um die konkrete Strategie für Sie bereitzustellen.

In diesem Fall könnten Sie beispielsweise interface VideoCompressor Haben und dann mehrere alternative Implementierungen haben, zum Beispiel class H264Compressor implements VideoCompressor Und class XVidCompressor implements VideoCompressor. Aus OP ist nicht ersichtlich, dass es sich um eine Schnittstelle handelt, auch wenn dies nicht der Fall ist. Es kann einfach sein, dass der ursprüngliche Autor die Tür offen gelassen hat, um bei Bedarf ein Strategiemuster zu implementieren. Was an und für sich auch gutes Design ist.

Das Problem, dass OP ständig die Klassen instanziiert, um eine Methode aufzurufen, ist ein Problem, bei dem sie die Abhängigkeitsinjektion und das Strategiemuster nicht korrekt verwendet. Anstatt es dort zu instanziieren, wo Sie es benötigen, sollte die enthaltende Klasse ein Mitglied mit dem Strategieobjekt haben. Und dieses Mitglied sollte beispielsweise im Konstruktor injiziert werden.

In vielen Fällen führt das Strategiemuster zu Schnittstellenklassen (wie Sie zeigen) mit nur einer einzigen doStuff(...) -Methode.

6
Emily L.
public interface IVideoProcessor
{
   void Split();

   void Compress();

   void Index();
}

Was Sie haben, ist modular und das ist gut so, aber wenn Sie diese Verantwortlichkeiten in IVideoProcessor gruppieren würden, wäre dies aus DDD-Sicht wahrscheinlich sinnvoller.

Wenn das Teilen, Komprimieren und Indizieren jedoch in keiner Weise miteinander zusammenhängt, würde ich sie als separate Komponenten beibehalten.

2
CodeART

Es ist ein Problem - Sie arbeiten eher unter dem funktionalen Aspekt des Designs als unter den Daten. Was Sie tatsächlich haben, sind 3 eigenständige Funktionen, die OO-ified wurden.

Sie haben beispielsweise eine VideoCompressor-Klasse. Warum arbeiten Sie mit einer Klasse, die zum Komprimieren von Videos entwickelt wurde? Warum gibt es keine Videoklasse mit Methoden zum Komprimieren der (Video-) Daten, die jedes Objekt dieses Typs enthält?

Beim Entwerfen von OO -Systemen ist es am besten, Klassen zu erstellen, die Objekte darstellen, anstatt Klassen, die Aktivitäten darstellen, die Sie anwenden können. Früher wurden Klassen als Typen bezeichnet - OO war eine Möglichkeit, eine Sprache mit Unterstützung für neue Datentypen zu erweitern. Wenn Sie an OO) denken, erhalten Sie eine bessere Möglichkeit, Ihre Klassen zu entwerfen.

BEARBEITEN:

lassen Sie mich versuchen, mich ein wenig besser zu erklären. Stellen Sie sich eine String-Klasse mit einer Concat-Methode vor. Sie können so etwas implementieren, bei dem jedes aus der Klasse instanziierte Objekt die Zeichenfolgendaten enthält, also können Sie sagen

string mystring("Hello"); 
mystring.concat("World");

aber das OP möchte, dass es so funktioniert:

string mystring();
string result = mystring.concat("Hello", "World");

jetzt gibt es Orte, an denen eine Klasse verwendet werden kann, um eine Sammlung verwandter Funktionen zu speichern. Dies ist jedoch nicht OO. Dies ist eine praktische Möglichkeit, die OO - Funktionen einer Sprache zur Verwaltung Ihrer Codebasis zu verwenden besser, aber es ist keineswegs irgendeine Art von "OO-Design". Das Objekt ist in solchen Fällen völlig künstlich und wird einfach so verwendet, weil die Sprache nichts Besseres bietet, um diese Art von Problem zu bewältigen, z. B. in Sprachen wie C # Sie würden eine statische Klasse verwenden, um diese Funktionalität bereitzustellen - sie verwendet den Klassenmechanismus wieder, aber Sie müssen kein Objekt mehr instanziieren, nur um die Methoden darauf aufzurufen. Sie erhalten Methoden wie string.IsNullOrEmpty(mystring), die ich Denken ist schlecht im Vergleich zu mystring.isNullOrEmpty().

Wenn also jemand fragt, wie ich meine Klassen entwerfe, empfehle ich, an die Daten zu denken, die die Klasse enthält, und nicht an die Funktionen, die sie enthält. Wenn Sie sich für "eine Klasse ist eine Reihe von Methoden" entscheiden, schreiben Sie am Ende Code im "besseren C" -Stil. (was nicht unbedingt eine schlechte Sache ist, wenn Sie C-Code verbessern), aber es wird Ihnen nicht das beste OO entworfene Programm) geben.

1
gbjbaanb

Das ISP (Prinzip der Schnittstellentrennung) besagt, dass kein Client gezwungen werden sollte, sich auf Methoden zu verlassen, die er nicht verwendet. Die Vorteile sind vielfältig und klar. Ihr Ansatz respektiert den ISP voll und ganz, und das ist gut so.

Ein anderer Ansatz, der auch den ISP berücksichtigt, besteht beispielsweise darin, für jede Methode (oder einen Satz von Methoden mit hoher Kohäsion) eine Schnittstelle zu erstellen und dann eine einzelne Klasse alle diese Schnittstellen implementieren zu lassen. Ob dies eine bessere Lösung ist oder nicht, hängt vom Szenario ab. Dies hat den Vorteil, dass Sie bei Verwendung der Abhängigkeitsinjektion einen Client mit verschiedenen Mitarbeitern haben können (einen pro Schnittstelle), aber am Ende verweisen alle Mitarbeiter auf dieselbe Objektinstanz.

Du hast übrigens gesagt

Ich finde mich dabei, jede Klasse zu instanziieren

Diese Klassen scheinen Dienste zu sein (und somit staatenlos). Hast du darüber nachgedacht, sie zu Singletons zu machen?

0
diegomtassis

Ja, es gibt ein Problem. Aber nicht schwer. Ich meine, Sie können Ihren Code so strukturieren und nichts Schlimmes wird passieren, es ist wartbar. Diese Art der Strukturierung weist jedoch einige Schwächen auf. Wenn sich beispielsweise die Darstellung Ihres Videos (in Ihrem Fall in der RawVideo-Klasse gruppiert) ändert, müssen Sie alle Ihre Operationsklassen aktualisieren. Oder denken Sie daran, dass Sie möglicherweise mehrere Darstellungen von Videos haben, die sich in der Laufzeit unterscheiden. Dann müssen Sie die Darstellung einer bestimmten "Operations" -Klasse zuordnen. Auch subjektiv ist es ärgerlich, für jede Operation, die Sie mit etw ausführen möchten, eine Abhängigkeit zu ziehen. und um die Liste der Abhängigkeiten zu aktualisieren, die jedes Mal weitergegeben werden, wenn Sie entscheiden, dass eine Operation nicht mehr benötigt wird oder Sie eine neue Operation benötigen.

Auch das ist eigentlich eine Verletzung von SRP. Einige Leute behandeln SRP nur als Leitfaden für die Aufteilung von Verantwortlichkeiten (und gehen zu weit, indem sie jede Operation mit einer eigenen Verantwortung behandeln), aber sie vergessen, dass SRP auch ein Leitfaden für die Gruppierung von Verantwortlichkeiten ist. Und gemäß den SRP-Verantwortlichkeiten sollten Änderungen, die sich aus demselben Grund ändern, zusammengefasst werden, damit Änderungen bei möglichst wenigen Klassen/Modulen lokalisiert werden. Bei großen Klassen ist es kein Problem, mehrere Algorithmen in derselben Klasse zu haben, solange diese Algorithmen miteinander verbunden sind (d. H. Wissen teilen, das außerhalb dieser Klasse nicht bekannt sein sollte). Das Problem sind große Klassen mit Algorithmen, die in keiner Weise miteinander verbunden sind und sich aus verschiedenen Gründen ändern/variieren.

0
ddiukariev