Wie kann man eine Methode besser aufrufen, die nur für eine Klasse verfügbar ist, die eine Schnittstelle implementiert, nicht für die andere?

11

Grundsätzlich muss ich unter bestimmten Bedingungen verschiedene Aktionen ausführen. Der vorhandene Code ist so geschrieben

Basisschnittstelle

// DoSomething.java
interface DoSomething {

   void letDoIt(String info);
}

Implementierung der ersten Arbeiterklasse

class DoItThisWay implements DoSomething {
  ...
}

Implementierung der zweiten Arbeiterklasse

class DoItThatWay implements DoSomething {
   ...
}

Die Hauptklasse

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          worker = new DoItThisWay();
        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }

Dieser Code funktioniert einwandfrei und ist leicht zu verstehen.

Jetzt muss ich aufgrund einer neuen Anforderung eine neue Information weitergeben, die nur für sinnvoll ist DoItThisWay.

Meine Frage ist: Ist der folgende Codierungsstil gut, um mit dieser Anforderung umzugehen.

Verwenden Sie eine neue Klassenvariable und Methode

// Use new class variable and method

class DoItThisWay implements DoSomething {
  private int quality;
  DoSomething() {
    quality = 0;
  }

  public void setQuality(int quality) {
    this.quality = quality;
  };

 public void letDoIt(String info) {
   if (quality > 50) { // make use of the new information
     ...
   } else {
     ...
   }
 } ;

}

Wenn ich das so mache, muss ich die entsprechende Änderung am Anrufer vornehmen:

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          int quality = obtainQualityInfo();
          DoItThisWay tmp = new DoItThisWay();
          tmp.setQuality(quality)
          worker = tmp;

        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }

Ist es ein guter Codierungsstil? Oder kann ich es einfach besetzen?

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          int quality = obtainQualityInfo();
          worker = new DoItThisWay();
          ((DoItThisWay) worker).setQuality(quality)
        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }
Anthony Kong
quelle
19
Warum nicht einfach qualityin den Konstruktor übergehen DoItThisWay?
David Arno
Wahrscheinlich muss ich meine Frage überarbeiten ... weil aus Performancegründen die Konstruktion von DoItThisWayund DoItThatWayeinmalig im Konstruktor von erfolgt Main. Mainist eine langlebige Klasse und doingItwird immer wieder aufgerufen.
Anthony Kong
Ja, in diesem Fall ist es eine gute Idee, Ihre Frage zu aktualisieren.
David Arno
Wollen Sie damit sagen, dass die setQualityMethode während der Lebensdauer des DoItThisWayObjekts mehrmals aufgerufen wird?
jpmc26
1
Es gibt keinen relevanten Unterschied zwischen den beiden von Ihnen vorgestellten Versionen. Aus logischer Sicht machen sie dasselbe.
Sebastian Redl

Antworten:

6

Ich gehe davon aus, dass qualitydies neben jedem letDoIt()Anruf auf einem gesetzt werden muss DoItThisWay().

Die Frage , die ich hier sehe entsteht , ist dies: Sie sind die Einführung zeitliche Kopplung (dh was happends wenn Sie anrufen vergessen setQuality()vor dem Aufruf letDoIt()auf ein DoItThisWay?). Und die Implementierungen für DoItThisWayund DoItThatWaylaufen auseinander (man muss setQuality()angerufen haben, der andere nicht).

Während dies im Moment möglicherweise keine Probleme verursacht, wird es Sie möglicherweise irgendwann verfolgen. Es kann sich lohnen, einen weiteren Blick darauf zu werfen letDoIt()und zu prüfen, ob die Qualitätsinformationen Teil der Informationen sein müssen, die infoSie durchlaufen. das kommt aber auf die details an.

CharonX
quelle
15

Ist es ein guter Codierungsstil?

Aus meiner Sicht ist keine Ihrer Versionen. Zuerst muss man anrufen, setQualitybevor letDoItman angerufen werden kann, ist eine zeitliche Kopplung . Sie stecken beim Betrachten DoItThisWayals Derivat von fest DoSomething, aber es ist nicht (zumindest nicht funktional), es ist eher so etwas wie ein

interface DoSomethingWithQuality {
   void letDoIt(String info, int quality);
}

Das würde Maineher so etwas machen

class Main {
    // omitting the creation
    private DoSomething doSomething;
    private DoSomethingWithQuality doSomethingWithQuality;

    public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
            int quality = obtainQualityInfo();
            doSomethingWithQuality.letDoIt(info, quality);
        } else {
            doSomething.letDoIt(info);
        }
    }
}

Andererseits könnten Sie die Parameter direkt an die Klassen übergeben (sofern dies möglich ist) und die Entscheidung, welche verwendet werden soll, an eine Factory delegieren (dies würde die Instanzen wieder austauschbar machen und beide können abgeleitet werden DoSomething). Das würde so Mainaussehen

class Main {
    private DoSomethingFactory doSomethingFactory;

     public doingIt(String info) {
         int quality = obtainQualityInfo();
         DoSomething doSomethingWorker = doSomethingFactory.Create(info, quality);
         doSomethingWorker.letDoIt();
     }
}

Mir ist bewusst, dass Sie das geschrieben haben

weil aus Performancegründen die Erstellung von DoItThisWay und DoItThatWay einmal im Konstruktor von Main erfolgt

Sie können aber auch die Teile zwischenspeichern, die in der Fabrik teuer sind, und sie an den Konstruktor übergeben.

Paul Kertscher
quelle
Argh, spionierst du mich aus, während ich Antworten schreibe? Wir machen solche ähnlichen Punkte (aber Ihre ist viel umfassender und beredter);)
CharonX
1
@DocBrown Ja, es ist umstritten, aber da die DoItThisWayQualität eingestellt werden muss, bevor letDoItes aufgerufen wird, verhält es sich anders. Ich würde erwarten, dass a DoSomethingfür alle Derivate auf die gleiche Weise funktioniert (ohne zuvor die Qualität einzustellen). Macht das Sinn? Dies ist natürlich etwas unscharf, da setQualityder Kunde , wenn wir die Methode von einer Fabrik aufrufen, nicht weiß, ob die Qualität eingestellt werden muss oder nicht.
Paul Kertscher
2
@DocBrown Ich nehme an, der Vertrag letDoIt()lautet (ohne zusätzliche Informationen) "Ich kann alles korrekt ausführen (mache, was ich tue), wenn du es mir gibst String info". Das vorausgehende Anfordern setQuality()eines Anrufs stärkt die Vorbedingung für einen Anruf letDoIt()und wäre somit eine Verletzung des LSP.
CharonX
2
@CharonX: Wenn es zum Beispiel einen Vertrag geben würde, der impliziert, dass " letDoIt" keine Ausnahmen auslösen soll, und das Vergessen, "setQuality" aufzurufen letDoIt, eine Ausnahme auslösen würde, dann würde ich zustimmen, dass eine solche Implementierung den LSP verletzen würde. Aber diese Annahmen erscheinen mir sehr künstlich.
Doc Brown
1
Das ist eine gute Antwort. Das einzige, was ich hinzufügen möchte, ist ein Kommentar darüber, wie katastrophal schlecht die zeitliche Kopplung für eine Codebasis ist. Es ist ein versteckter Vertrag zwischen scheinbar entkoppelten Komponenten. Zumindest ist es bei normaler Kopplung eindeutig und leicht zu identifizieren. Dabei wird im Wesentlichen eine Grube gegraben und mit Blättern bedeckt.
JimmyJames
10

Nehmen wir an, dass qualitykeine Übergabe an den Konstruktor möglich ist und der Aufruf an setQualityerforderlich ist.

Derzeit mag ein Code-Snippet

    int quality = obtainQualityInfo();
    worker = new DoItThisWay();
    ((DoItThisWay) worker).setQuality(quality);

ist viel zu klein, um zu viele Gedanken in sie zu investieren. IMHO sieht es ein bisschen hässlich aus, ist aber nicht wirklich schwer zu verstehen.

Probleme treten auf, wenn ein solches Code-Snippet wächst und Sie aufgrund von Umgestaltungen mit so etwas enden

    int quality = obtainQualityInfo();
    worker = CreateAWorkerForThisInfo();
    ((DoItThisWay) worker).setQuality(quality);

Jetzt sehen Sie nicht sofort, ob dieser Code noch korrekt ist, und der Compiler sagt es Ihnen nicht. Das Einführen einer temporären Variablen des richtigen Typs und das Vermeiden der Umwandlung ist daher ohne großen zusätzlichen Aufwand ein wenig typsicherer.

Allerdings würde ich eigentlich tmpeinen besseren Namen geben:

      int quality = obtainQualityInfo();
      DoItThisWay workerThisWay = new DoItThisWay();
      workerThisWay.setQuality(quality)
      worker = workerThisWay;

Eine solche Benennung hilft, falschen Code falsch aussehen zu lassen.

Doc Brown
quelle
tmp könnte noch besser als "workerThatUsesQuality" bezeichnet werden
user949300
1
@ user949300: IMHO, das ist keine gute Idee: Angenommen, DoItThisWayes werden spezifischere Parameter abgerufen - nach Ihren Vorstellungen muss der Name jedes Mal geändert werden. Es ist besser, Namen für Variablen zu verwenden, die den Objekttyp verdeutlichen, nicht die verfügbaren Methodennamen.
Doc Brown
Zur Verdeutlichung wäre es der Name der Variablen, nicht der Name der Klasse. Ich stimme zu, dass es eine schlechte Idee ist, alle Fähigkeiten in den Klassennamen aufzunehmen. Also schlage ich vor, dass workerThisWayso etwas wird usingQuality. In diesem kleinen Code-Snippet ist es sicherer, genauer zu sein. (Und wenn es einen besseren Ausdruck in ihrer Domain gibt als "usingQuality", verwenden Sie ihn.
user949300
2

Eine allgemeine Schnittstelle, bei der die Initialisierung basierend auf Laufzeitdaten variiert, eignet sich normalerweise für das Factory-Muster.

DoThingsFactory factory = new DoThingsFactory(thingThatProvidesQuality);
DoSomething doSomething = factory.getDoer(info);

doSomething.letDoIt();

Dann kann sich DoThingsFactory darum kümmern, die Qualitätsinformationen innerhalb der getDoer(info)Methode abzurufen und festzulegen, bevor das konkrete DoThingsWithQuality-Objekt an die DoSomething-Schnittstelle zurückgegeben wird.

Greg Burghardt
quelle