Wie hält man die Anzahl der Argumente niedrig und dennoch die Abhängigkeiten von Drittanbietern getrennt?

13

Ich benutze eine Drittanbieter-Bibliothek. Sie übergeben mir ein POJO , das nach unseren Absichten und Zwecken wahrscheinlich so implementiert wird:

public class OurData {
  private String foo;
  private String bar;
  private String baz;
  private String quux;
  // A lot more than this

  // IMPORTANT: NOTE THAT THIS IS A PACKAGE PRIVATE CONSTRUCTOR
  OurData(/* I don't know what they do */) {
    // some stuff
  }

  public String getFoo() {
    return foo;
  }

  // etc.
}

Aus vielen Gründen, einschließlich, aber nicht beschränkt auf das Einkapseln ihrer API und das Ermöglichen von Komponententests, möchte ich ihre Daten verpacken. Aber ich möchte nicht, dass meine Kernklassen von ihren Daten abhängig sind (auch hier aus Testgründen)! Im Moment habe ich so etwas:

public class DataTypeOne implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
  }
}

public class DataTypeTwo implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz, String quux) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
    this.quux = quux;
  }
}

Und dann das:

public class ThirdPartyAdapter {
  public static makeMyData(OurData data) {
    if(data.getQuux() == null) {
      return new DataTypeOne(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
      );
    } else {
      return new DataTypeTwo(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
        data.getQuux();
      );
  }
}

Diese Adapterklasse ist mit den wenigen anderen Klassen gekoppelt, die über die Drittanbieter-API Bescheid wissen MÜSSEN, wodurch ihre Verbreitung im Rest meines Systems eingeschränkt wird. Aber ... diese Lösung ist GROSS! In Clean Code, Seite 40:

Mehr als drei Argumente (polyadisch) bedürfen einer besonderen Begründung - und sollten dann sowieso nicht verwendet werden.

Dinge, über die ich nachgedacht habe:

  • Erstellen eines Factory-Objekts anstelle einer statischen Hilfsmethode
    • Löst nicht das Problem der Bajillion Argumente
  • Erstellen einer Unterklasse von DataTypeOne und DataTypeTwo mit einem abhängigen Konstruktor
    • Hat noch einen polyadengeschützten Konstruktor
  • Erstellen Sie vollständig separate Implementierungen, die derselben Schnittstelle entsprechen
  • Mehrere der oben genannten Ideen gleichzeitig

Wie soll mit dieser Situation umgegangen werden?


Beachten Sie, dass dies keine Antikorruptionssituation ist . Es ist nichts falsch mit ihrer API. Die Probleme sind:

  • Ich möchte nicht, dass MEINE Datenstrukturen vorhanden sind import com.third.party.library.SomeDataStructure;
  • Ich kann ihre Datenstrukturen in meinen Testfällen nicht aufbauen
  • Meine derzeitige Lösung führt zu sehr sehr hohen Argumentationszahlen. Ich möchte die Anzahl der Argumente niedrig halten, OHNE ihre Datenstrukturen weiterzugeben.
  • Diese Frage lautet " Was ist eine Antikorruptionsschicht?". Meine Frage lautet: " Wie kann ich ein Muster oder ein beliebiges Muster verwenden, um dieses Szenario zu lösen?"

Ich frage auch nicht nach Code (ansonsten wäre diese Frage SO), sondern nur nach einer ausreichenden Antwort, damit ich den Code effektiv schreiben kann (was diese Frage nicht bietet).

durron597
quelle
Wenn es mehrere solche POJOs von Drittanbietern gibt, kann es sich lohnen, benutzerdefinierten Testcode zu schreiben, der eine Map mit einigen Konventionen (z. B. die Schlüssel int_bar) als Testeingabe verwendet. Oder verwenden Sie JSON oder XML mit einem benutzerdefinierten Zwischencode. In der Tat eine Art DSL zum Testen von Drittanbietern.
user949300
Das vollständige Zitat aus Clean Code:The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.
Lilienthal
11
Das blinde Festhalten an einem Muster oder einer Programmierrichtlinie ist ein eigenes Anti-Muster .
Lilienthal
2
"Einkapseln der API und Ermöglichen von Komponententests" Klingt so, als wäre dies ein Fall von Überprüfung und / oder testbedingtem Designschaden für mich (oder als Hinweis darauf, dass Sie dies zunächst anders gestalten könnten). Fragen Sie sich Folgendes: Ist Ihr Code dadurch wirklich leichter zu verstehen, zu ändern und wiederzuverwenden? Ich würde mein Geld auf "Nein" setzen. Wie realistisch ist es, dass Sie diese Bibliothek jemals austauschen werden? Wahrscheinlich nicht sehr. Wenn Sie es austauschen, ist es dann wirklich einfacher, ein völlig anderes Objekt an Ort und Stelle abzulegen? Ich würde wieder auf "Nein" wetten.
jpmc26
1
@JamesAnderson Ich habe gerade das vollständige Zitat reproduziert, weil ich es interessant fand, aber mir war aus dem Snippet nicht klar, ob es sich auf Funktionen im Allgemeinen oder auf Konstruktoren im Besonderen bezieht. Ich wollte der Behauptung nicht zustimmen, und wie jpmc26 sagte, sollte mein nächster Kommentar Ihnen einen Hinweis darauf geben, dass ich das nicht tue. Ich bin mir nicht sicher, warum Sie das Bedürfnis verspüren, Akademiker anzugreifen, aber die Verwendung von Polysilben lässt niemanden zu einem akademischen Elitisten werden, der auf seinem Elfenbeinturm über den Wolken thront.
Lilienthal

Antworten:

10

Die Strategie, die ich bei mehreren Initialisierungsparametern angewendet habe, besteht darin, einen Typ zu erstellen, der nur die Parameter für die Initialisierung enthält

public class DataTypeTwoParameters {
    public String foo;  // use getters/setters instead if it's appropriate
    public int bar;
    public double baz;
    public String quuz;
}

Dann nimmt der Konstruktor für DataTypeTwo ein DataTypeTwoParameters-Objekt und DataTypeTwo wird erstellt über:

DataTypeTwoParameters p = new DataTypeTwoParameters();
p.foo = "Hello";
p.bar = 4;
p.baz = 3;
p.quuz = "World";

DataTypeTwo dtt = new DataTypeTwo(p);

Dies gibt eine Menge Gelegenheit, um deutlich zu machen, welche Parameter in DataTypeTwo enthalten sind und was sie bedeuten. Sie können auch sinnvolle Standardwerte im DataTypeTwoParameters-Konstruktor angeben, sodass nur Werte festgelegt werden können, die in einer beliebigen Reihenfolge festgelegt werden müssen, die dem Benutzer der API gefällt.

Erik
quelle
Interessanter Ansatz. Wo würden Sie eine relevante setzen Integer.parseInt? In einem Setter oder außerhalb der Parameterklasse?
Durron597
5
Außerhalb der Parameterklasse. Die Parameters-Klasse sollte ein "dummes" Objekt sein und sollte nicht versuchen, etwas anderes zu tun, als die erforderlichen Eingaben und ihre Typen auszudrücken. Das Parsen sollte an anderer Stelle erfolgen, z p.bar = Integer.parseInt("4").
Erik
7
das klingt wie ein Parameter-Objekt- Muster
gnat
9
... oder Anti-Pattern.
Telastyn
1
... oder Sie könnten einfach umbenennen DataTypeTwoParametersin DataTypeTwo.
user253751
14

Sie haben hier zwei separate Probleme: das Umbrechen einer API und die geringe Anzahl von Argumenten.

Beim Packen einer API besteht die Idee darin, die Schnittstelle wie von Grund auf neu zu gestalten und dabei nur die Anforderungen zu kennen. Sie sagen, dass an der API nichts falsch ist, und dann im selben Atemzug eine Reihe von Dingen auflisten, die an der API falsch sind: Testbarkeit, Konstruierbarkeit, zu viele Parameter in einem Objekt usw. Schreiben Sie die API, die Sie sich gewünscht haben. Wenn dies mehrere Objekte anstelle des einen erfordert, tun Sie dies. Wenn es erforderlich ist, eine Ebene höher zu den Objekten zu springen, die das POJO erstellen , tun Sie dies.

Sobald Sie Ihre gewünschte API haben, ist die Parameteranzahl möglicherweise kein Problem mehr. Wenn ja, gibt es eine Reihe von gängigen Mustern zu berücksichtigen:

  • Ein Parameterobjekt, wie in Eriks Antwort .
  • Das Builder-Muster , in dem Sie ein separates Builder-Objekt erstellen, dann eine Reihe von Setzern aufrufen, um die Parameter individuell festzulegen, und dann Ihr Endobjekt erstellen.
  • Das Prototypmuster , in dem Sie Unterklassen Ihres gewünschten Objekts mit den bereits intern festgelegten Feldern klonen.
  • Eine Fabrik, mit der Sie bereits vertraut sind.
  • Eine Kombination der oben genannten.

Beachten Sie, dass diese Erstellungsmuster häufig einen polyadischen Konstruktor aufrufen, den Sie als in Ordnung betrachten sollten, wenn er gekapselt ist. Das Problem mit polyadischen Konstruktoren ist, dass sie nicht nur einmal aufgerufen werden. Sie müssen sie jedes Mal aufrufen, wenn Sie ein Objekt erstellen müssen.

Beachten Sie, dass es in der Regel viel einfacher und leichter zu handhaben ist, die zugrunde liegende API zu erreichen, indem Sie einen Verweis auf das OurDataObjekt speichern und die Methodenaufrufe weiterleiten, anstatt zu versuchen, die internen Funktionen erneut zu implementieren. Beispielsweise:

public class DataTypeTwo implements DataInterface {
  private OurData data;

  public DataTypeOne(OurData data) {
    this.data = data;
  }

   public String getFoo() {
    return data.getFoo();
  }

  public int getBar() {
    return Integer.parseInt(data.getBar());
  }
  ...
}
Karl Bielefeldt
quelle
Erste Hälfte dieser Antwort: großartig, sehr hilfreich, +1. Zweite Hälfte dieser Antwort: "Durchreichen der zugrunde liegenden API durch Speichern eines Verweises auf das OurDataObjekt" - dies ist das, was ich zumindest in der Basisklasse vermeiden möchte, um sicherzustellen, dass keine Abhängigkeit besteht.
Durron597
1
Das ist der Grund, warum Sie dies nur in einer Ihrer Implementierungen von tun DataInterface. Sie erstellen eine weitere Implementierung für Ihre Scheinobjekte.
Karl Bielefeldt
@durron597: ja, aber du weißt schon, wie du dieses Problem lösen kannst, wenn es dich wirklich stört.
Doc Brown
1

Ich glaube, Sie interpretieren die Empfehlung von Onkel Bob zu streng. Für normale Klassen, mit Logik und Methoden und Konstruktoren und dergleichen, fühlt sich ein polyadischer Konstruktor tatsächlich sehr nach Code-Geruch an. Aber für etwas, das ausschließlich ein Datencontainer ist, der Felder verfügbar macht und von einem Factory-Objekt generiert wird, denke ich nicht, dass es so schlimm ist.

Sie können das in einem Kommentar vorgeschlagene Parameter-Objekt-Muster verwenden, um diese Konstruktor-Parameter für Sie zu verpacken, wobei Ihr lokaler Datentyp-Wrapper bereits im Wesentlichen ein Parameter-Objekt ist. Alles, was Ihr Parameter-Objekt tun wird, ist, die Parameter zu packen (wie werden Sie sie erstellen? Mit einem polyadischen Konstruktor?) Und sie eine Sekunde später in ein fast identisches Objekt zu entpacken.

Wenn Sie keine Setter für Ihre Felder anzeigen und sie aufrufen möchten, ist es meiner Meinung nach in Ordnung, sich an einen polyadischen Konstruktor in einer gut definierten und gekapselten Factory zu halten.

Avner Shahar-Kashtan
quelle
Das Problem ist, dass sich die Anzahl der Felder in meiner Datenstruktur mehrmals geändert hat und sich wahrscheinlich wieder ändern wird. Das heißt, ich muss den Konstruktor in all meinen Testfällen überarbeiten. Das Parametermuster mit vernünftigen Standardeinstellungen scheint ein besserer Weg zu sein. Eine veränderbare Version zu haben, die in der unveränderlichen Form gespeichert wird, könnte mein Leben in vielerlei Hinsicht erleichtern.
Durron597