Verbessern des Builder-Musters von Bloch, um es für die Verwendung in Klassen mit hoher Erweiterbarkeit geeigneter zu machen

34

Ich bin stark von Joshua Blochs Effective Java-Buch (2. Auflage) beeinflusst worden, wahrscheinlich mehr als von jedem anderen Programmierbuch, das ich gelesen habe. Insbesondere sein Builder Pattern (Item 2) hat die größte Wirkung gezeigt.

Obwohl Blochs Erbauer mich in den letzten Monaten viel weiter gebracht hat als in den letzten zehn Jahren der Programmierung, stoße ich immer noch auf die gleiche Mauer: Es ist bestenfalls entmutigend und schlimmstenfalls ein Alptraum, den Unterricht mit selbst wiederkehrenden Methodenketten zu verlängern - besonders wenn Generika ins Spiel kommen, und besonders bei selbstreferenziellen Generika (wie Comparable<T extends Comparable<T>>).

Es gibt zwei Hauptbedürfnisse, auf die ich mich in dieser Frage konzentrieren möchte:

  1. Das erste Problem ist, "wie man selbst-wiederkehrende Methodenketten teilt, ohne sie in jeder ... einzelnen ... Klasse neu implementieren zu müssen?" Für diejenigen, die vielleicht neugierig sind, habe ich diesen Teil am Ende dieses Antwortposts angesprochen, aber darauf möchte ich mich hier nicht konzentrieren.

  2. Das zweite Problem, das ich kommentieren möchte, lautet: "Wie kann ich einen Builder in Klassen implementieren, die selbst von vielen anderen Klassen erweitert werden sollen?" Das Erweitern einer Klasse mit einem Builder ist natürlich schwieriger als das Erweitern ohne. Das Erweitern einer Klasse, die über einen Builder verfügt , der ebenfalls implementiert wird Needable, und mit dem daher wichtige Generika verknüpft sind , ist unhandlich.

Das ist also meine Frage: Wie kann ich den Bloch-Builder verbessern (wie ich es nenne), damit ich jeder Klasse einen Builder hinzufügen kann - auch wenn diese Klasse eine "Basisklasse" sein soll viele Male verlängert und unterschritten - ohne mein zukünftiges Selbst oder die Benutzer meiner Bibliothek zu entmutigen , wegen des zusätzlichen Gepäcks, das der Erbauer (und seine potenziellen Generika) ihnen auferlegt?


Nachtrag
Meine Frage konzentriert sich auf Teil 2 oben, aber ich wollte ein wenig auf Problem eins eingehen, einschließlich der Art und Weise, wie ich damit umgegangen bin:

Das erste Problem ist, "wie man selbst-wiederkehrende Methodenketten teilt, ohne sie in jeder ... einzelnen ... Klasse neu implementieren zu müssen?" Dies soll nicht verhindern, dass erweiterte Klassen diese Ketten erneut implementieren müssen, was sie natürlich auch müssen - vielmehr, wie verhindert wird, dass Nicht-Unterklassen , die diese Methodenketten nutzen möchten , erneut implementiert werden müssen -implementieren Sie jede selbst-wiederkehrende Funktion, damit ihre Benutzer sie nutzen können? Zu diesem Zweck habe ich mir ein Design ausgedacht, das von den Benutzern benötigt wird. Ich drucke hier die Schnittstellenskelette aus und lasse es vorerst dabei. Für mich hat es gut funktioniert (dieses Design war jahrelang in der Entwicklung ... das Schwierigste war, kreisförmige Abhängigkeiten zu vermeiden):

public interface Chainable  {  
    Chainable chainID(boolean b_setStatic, Object o_id);  
    Object getChainID();  
    Object getStaticChainID();  
}
public interface Needable<O,R extends Needer> extends Chainable  {
    boolean isAvailableToNeeder();
    Needable<O,R> startConfigReturnNeedable(R n_eeder);
    R getActiveNeeder();
    boolean isNeededUsable();
    R endCfg();
}
public interface Needer  {
    void startConfig(Class<?> cls_needed);
    boolean isConfigActive();
    Class getNeededType();
    void neeadableSetsNeeded(Object o_fullyConfigured);
}
Aliteralmind
quelle

Antworten:

21

Ich habe das geschaffen, was für mich eine große Verbesserung gegenüber Josh Blochs Builder Pattern darstellt. Ganz zu schweigen davon, dass es "besser" ist, nur dass es in einer ganz bestimmten Situation einige Vorteile bietet - das größte ist, dass es den Bauherrn von seiner zu bauenden Klasse entkoppelt.

Ich habe diese Alternative, die ich als Blind Builder Pattern bezeichne, im Folgenden ausführlich dokumentiert.


Entwurfsmuster: Blind Builder

Als Alternative zu Joshua Blochs Builder-Muster (Punkt 2 in Effective Java, 2. Ausgabe) habe ich das sogenannte "Blind Builder-Muster" erstellt, das viele der Vorteile des Bloch Builder teilt und neben einem einzelnen Charakter wird genauso verwendet. Blinde Bauherren haben den Vorteil von

  • den Builder von seiner einschließenden Klasse zu entkoppeln, wodurch eine zirkuläre Abhängigkeit beseitigt wird,
  • stark reduziert die Größe des Quellcodes (was nicht mehr ist ) der einschließenden Klasse, und
  • Ermöglicht das ToBeBuiltErweitern der Klasse, ohne dass der Builder erweitert werden muss .

In dieser Dokumentation werde ich die Klasse, die erstellt wird, als " ToBeBuilt" Klasse bezeichnen.

Eine Klasse, die mit einem Bloch Builder implementiert wurde

Ein Bloch Builder ist ein public static classBestandteil der Klasse, die er erstellt. Ein Beispiel:

öffentliche Klasse UserConfig {
   private final String sName;
   private final int iAge;
   private final String sFavColor;
   public UserConfig (UserConfig.Cfg uc_c) {// CONSTRUCTOR
      //Transfer
         Versuchen {
            sName = uc_c.sName;
         } catch (NullPointerException rx) {
            werfen neue NullPointerException ("uc_c");
         }
         iAge = uc_c.iAge;
         sFavColor = uc_c.sFavColor;
      // ALLE FELDER HIER GÜLTIG MACHEN
   }
   public String toString () {
      return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
   //builder...START
   öffentliche statische Klasse Cfg {
      private String sName;
      private int iAge;
      private String sFavColor;
      public Cfg (String s_name) {
         sName = s_name;
      }
      // selbst zurückkehrende Setter ... START
         öffentliches Cfg-Alter (int i_age) {
            iAge = i_age;
            gib das zurück;
         }
         public Cfg favoriteColor (String s_color) {
            sFavColor = s_color;
            gib das zurück;
         }
      // selbst zurückkehrende Setter ... END
      public UserConfig build () {
         return (new UserConfig (this));
      }
   }
   //Builder ... END
}

Instanziieren einer Klasse mit einem Bloch Builder

UserConfig uc = new UserConfig.Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();

Dieselbe Klasse, implementiert als Blind Builder

Ein Blind Builder besteht aus drei Teilen, die sich jeweils in einer separaten Quellcodedatei befinden:

  1. Die ToBeBuiltKlasse (in diesem Beispiel: UserConfig)
  2. Seine " Fieldable" Schnittstelle
  3. Der Bauarbeiter

1. Die zu bauende Klasse

Die zu erstellende Klasse akzeptiert ihre FieldableSchnittstelle als einzigen Konstruktorparameter. Der Konstruktor setzt alle internen Felder daraus und validiert sie . Am wichtigsten ist, dass diese ToBeBuiltKlasse keine Kenntnisse über ihren Builder hat.

öffentliche Klasse UserConfig {
   private final String sName;
   private final int iAge;
   private final String sFavColor;
    public UserConfig (UserConfig_Fieldable uc_f) {// CONSTRUCTOR
      //Transfer
         Versuchen {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            werfen neue NullPointerException ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // ALLE FELDER HIER GÜLTIG MACHEN
   }
   public String toString () {
      return "name =" + sName + ", age =" + iAge + ", sFavColor =" + sFavColor;
   }
}

Wie von einem Smart - Kommentator bemerkt (die aus unerklärlichen Gründen ihre Antwort gelöscht), wenn die ToBeBuiltKlasse implementiert auch seinen Fieldable, sein ein-und-nur - Konstruktor kann sowohl als seinen primären verwendet werden und Copy - Konstruktor (ein Nachteil ist , dass die Felder sind zwar immer validiert, auch Es ist bekannt, dass die Felder im Original ToBeBuiltgültig sind.

2. Die FieldableSchnittstelle " "

Die feldfähige Schnittstelle ist die "Brücke" zwischen der ToBeBuiltKlasse und ihrem Builder und definiert alle Felder, die zum Erstellen des Objekts erforderlich sind. Diese Schnittstelle wird vom ToBeBuiltKlassenkonstruktor benötigt und vom Builder implementiert. Da diese Schnittstelle von anderen Klassen als dem Builder implementiert werden kann, kann jede Klasse die ToBeBuiltKlasse leicht instanziieren , ohne gezwungen zu sein, ihren Builder zu verwenden. Dies erleichtert auch das Erweitern der ToBeBuiltKlasse, wenn das Erweitern des Builders nicht erwünscht oder erforderlich ist.

Wie in einem der folgenden Abschnitte beschrieben, dokumentiere ich die Funktionen in dieser Benutzeroberfläche überhaupt nicht.

öffentliche Schnittstelle UserConfig_Fieldable {
   String getName ();
   int getAge ();
   String getFavoriteColor ();
}

3. Der Erbauer

Der Builder implementiert die FieldableKlasse. Es findet überhaupt keine Validierung statt, und um diese Tatsache zu betonen, sind alle seine Bereiche öffentlich und veränderlich. Obwohl diese öffentliche Zugänglichkeit keine Voraussetzung ist, bevorzuge und empfehle ich sie, da sie die Tatsache verstärkt, dass die Validierung erst nach dem ToBeBuiltAufruf des Konstruktors des Konstruktors erfolgt. Dies ist wichtig, da ein anderer Thread den Builder möglicherweise weiter manipulieren kann, bevor er an den ToBeBuiltKonstruktor des Threads übergeben wird . Die einzige Möglichkeit, die Gültigkeit der Felder zu gewährleisten - vorausgesetzt, der Builder kann seinen Status nicht irgendwie "sperren" - besteht darin, dass die ToBeBuiltKlasse die endgültige Prüfung durchführt.

Schließlich wird , wie mit der FieldableSchnittstelle, dokumentiere ich nichts von seinen Getter.

public class UserConfig_Cfg implementiert UserConfig_Fieldable {
   public String sName;
   public int iAge;
    public String sFavColor;
    public UserConfig_Cfg (String s_name) {
       sName = s_name;
    }
    // selbst zurückkehrende Setter ... START
       public UserConfig_Cfg age (int i_age) {
          iAge = i_age;
          gib das zurück;
       }
       public UserConfig_Cfg favoriteColor (String s_color) {
          sFavColor = s_color;
          gib das zurück;
       }
    // selbst zurückkehrende Setter ... END
    //getters...START
       public String getName () {
          return sName;
       }
       public int getAge () {
          return iAge;
       }
       public String getFavoriteColor () {
          return sFavColor;
       }
    //getters...END
    public UserConfig build () {
       return (new UserConfig (this));
    }
}

Instanziieren einer Klasse mit einem Blind Builder

UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();

Der einzige Unterschied ist " UserConfig_Cfg" statt " UserConfig.Cfg"

Anmerkungen

Nachteile:

  • Blinde Bauherren können nicht auf private Mitglieder ihrer ToBeBuiltKlasse zugreifen.
  • Sie sind ausführlicher, da jetzt sowohl im Builder als auch in der Schnittstelle Getter erforderlich sind.
  • Alles für eine Klasse ist nicht mehr nur an einem Ort .

Das Kompilieren eines Blind Builder ist einfach:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

Die FieldableSchnittstelle ist völlig optional

Für eine ToBeBuiltKlasse mit wenigen erforderlichen Feldern - wie diese UserConfigBeispielklasse - könnte der Konstruktor einfach sein

public UserConfig (String s_name, int i_age, String s_favColor) {

Und beim Baumeister mit angerufen

public UserConfig build () {
   return (new UserConfig (getName (), getAge (), getFavoriteColor ()));
}

Oder sogar durch Eliminieren der Getter (im Builder) insgesamt:

   return (neue UserConfig (sName, iAge, sFavoriteColor));

Durch die direkte Übergabe von Feldern ist die ToBeBuiltKlasse genauso "blind" (ohne Kenntnis ihres Builders) wie bei der FieldableSchnittstelle. Für ToBeBuiltKlassen, die "um ein Vielfaches erweitert und sub-erweitert" werden sollen (wie im Titel dieses Beitrags angegeben), erfordert jede Änderung in einem beliebigen Feld Änderungen in jeder Unterklasse, in jedem Builder und ToBeBuiltKonstruktor. Wenn die Anzahl der Felder und Unterklassen zunimmt, ist dies unpraktisch zu pflegen.

(In der Tat ist die Verwendung eines Builders mit wenigen erforderlichen Feldern möglicherweise zu viel des Guten . Für Interessenten finden Sie hier eine Auswahl der größeren Fieldable-Schnittstellen in meiner persönlichen Bibliothek.)

Sekundärklassen im Unterpaket

Ich entscheide mich dafür, alle Builder und die FieldableKlassen für alle Blind Builder in einem Unterpaket ihrer ToBeBuiltKlasse zu haben. Das Unterpaket heißt immer " z". Dies verhindert, dass diese sekundären Klassen die JavaDoc-Paketliste überladen. Beispielsweise

  • library.class.my.UserConfig
  • library.class.my.z.UserConfig_Fieldable
  • library.class.my.z.UserConfig_Cfg

Validierungsbeispiel

Wie oben erwähnt, erfolgt die gesamte Validierung im ToBeBuiltKonstruktor des Benutzers. Hier ist noch einmal der Konstruktor mit Beispielvalidierungscode:

public UserConfig (UserConfig_Fieldable uc_f) {
   //Transfer
      Versuchen {
         sName = uc_f.getName ();
      } catch (NullPointerException rx) {
         werfen neue NullPointerException ("uc_f");
      }
      iAge = uc_f.getAge ();
      sFavColor = uc_f.getFavoriteColor ();
   // validieren (sollte die Muster wirklich vorkompilieren ...)
      Versuchen {
         if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
            throw new IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") darf nicht leer sein und darf nur Ziffern und Unterstriche enthalten.");
         }
      } catch (NullPointerException rx) {
         neue NullPointerException ("uc_f.getName ()") auslösen;
      }
      if (iAge <0) {
         throw new IllegalArgumentException ("uc_f.getAge () (" + iAge + ") ist kleiner als Null.");
      }
      Versuchen {
         if (! Pattern.compile ("(?: rot | blau | grün | pink)"). matcher (sFavColor) .matches ()) {
            throw new IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") ist nicht rot, blau, grün oder pink.");
         }
      } catch (NullPointerException rx) {
         werfen neue NullPointerException ("uc_f.getFavoriteColor ()");
      }
}

Builder dokumentieren

Dieser Abschnitt gilt sowohl für Bloch Builder als auch für Blind Builder. Es wird gezeigt, wie ich die Klassen in diesem Entwurf dokumentiere und Setter (im Builder) und ihre Getter (in der ToBeBuiltKlasse) direkt miteinander in Querverweis setzten - mit einem einzigen Mausklick und ohne dass der Benutzer wissen muss, wo diese Funktionen sind tatsächlich vorhanden - und ohne dass der Entwickler irgendetwas redundant dokumentieren muss.

Getter: ToBeBuiltNur in den Klassen

Getter werden nur in der ToBeBuiltKlasse dokumentiert . Die entsprechenden Getter in den Klassen _Fieldableund_Cfg werden ignoriert. Ich dokumentiere sie überhaupt nicht.

/ **
   <P> Das Alter des Benutzers. </ P>
   @return Ein Int, der das Alter des Benutzers angibt.
   @see UserConfig_Cfg # age (int)
   @see getName ()
 ** /
public int getAge () {
   return iAge;
}

Der erste @seeist ein Link zu seinem Setter, der sich in der Builder-Klasse befindet.

Setter: In der Builder-Klasse

Der Setter wird so dokumentiert, als ob er sich in der ToBeBuiltKlasse befindet , und auch als ob er die Validierung durchführt (was wirklich vom ToBeBuiltKonstruktor des Setters durchgeführt wird ). Das Sternchen (" *") ist ein visueller Hinweis darauf, dass sich das Ziel des Links in einer anderen Klasse befindet.

/ **
   <P> Stellen Sie das Alter des Benutzers ein. </ P>
   @param i_age Darf nicht kleiner als Null sein. Holen Sie sich mit {@code UserConfig # getName () getName ()} *.
   @see #favoriteColor (String)
 ** /
public UserConfig_Cfg age (int i_age) {
   iAge = i_age;
   gib das zurück;
}

Weitere Informationen

Alles zusammenfassen: Die vollständige Quelle des Blind Builder-Beispiels mit vollständiger Dokumentation

UserConfig.java

import java.util.regex.Pattern;
/ **
   <P> Informationen zu einem Benutzer - <I> [Builder: UserConfig_Cfg] </ I> </ P>
   <P> Die Validierung aller Felder erfolgt in diesem Klassenkonstruktor. Jede Validierungsanforderung ist jedoch nur in den Setterfunktionen des Builders dokumentiert. </ P>
   <P> {@ code java xbn.z.xmpl.lang.builder.finalv.UserConfig} </ P>
 ** /
öffentliche Klasse UserConfig {
   public static final void main (String [] igno_red) {
      UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("green"). Build ();
      System.out.println (uc);
   }
   private final String sName;
   private final int iAge;
   private final String sFavColor;
   / **
      <P> Erstellen Sie eine neue Instanz. Hiermit werden alle Felder festgelegt und überprüft. </ P>
      @param uc_f Darf nicht {@code null} sein.
    ** /
   public UserConfig (UserConfig_Fieldable uc_f) {
      //Transfer
         Versuchen {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            werfen neue NullPointerException ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      //bestätigen
         Versuchen {
            if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
               throw new IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") darf nicht leer sein und darf nur Ziffern und Unterstriche enthalten.");
            }
         } catch (NullPointerException rx) {
            neue NullPointerException ("uc_f.getName ()") auslösen;
         }
         if (iAge <0) {
            throw new IllegalArgumentException ("uc_f.getAge () (" + iAge + ") ist kleiner als Null.");
         }
         Versuchen {
            if (! Pattern.compile ("(?: rot | blau | grün | pink)"). matcher (sFavColor) .matches ()) {
               throw new IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") ist nicht rot, blau, grün oder pink.");
            }
         } catch (NullPointerException rx) {
            werfen neue NullPointerException ("uc_f.getFavoriteColor ()");
         }
   }
   //getters...START
      / **
         <P> Der Name des Benutzers. </ P>
         @return Eine nicht - {@ code null}, nicht leere Zeichenfolge.
         @see UserConfig_Cfg # UserConfig_Cfg (String)
         @see #getAge ()
         @see #getFavoriteColor ()
       ** /
      public String getName () {
         return sName;
      }
      / **
         <P> Das Alter des Benutzers. </ P>
         @return Eine Zahl größer als oder gleich Null.
         @see UserConfig_Cfg # age (int)
         @see #getName ()
       ** /
      public int getAge () {
         return iAge;
      }
      / **
         <P> Die Lieblingsfarbe des Benutzers. </ P>
         @return Eine nicht - {@ code null}, nicht leere Zeichenfolge.
         @see UserConfig_Cfg # age (int)
         @see #getName ()
       ** /
      public String getFavoriteColor () {
         return sFavColor;
      }
   //getters...END
   public String toString () {
      return "getName () =" + getName () + ", getAge () =" + getAge () + ", getFavoriteColor () =" + getFavoriteColor ();
   }
}

UserConfig_Fieldable.java

/ **
   <P> Erforderlich für den {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable) -Konstruktor}. </ P>
 ** /
öffentliche Schnittstelle UserConfig_Fieldable {
   String getName ();
   int getAge ();
   String getFavoriteColor ();
}

UserConfig_Cfg.java

import java.util.regex.Pattern;
/ **
   <P> Builder für {@link UserConfig}. </ P>
   <P> Die Überprüfung aller Felder erfolgt im <CODE> UserConfig </ CODE> -Konstruktor. Jede Validierungsanforderung ist jedoch nur in diesen Klasseneinstellungsfunktionen dokumentiert. </ P>
 ** /
public class UserConfig_Cfg implementiert UserConfig_Fieldable {
   public String sName;
   public int iAge;
   public String sFavColor;
   / **
      <P> Erstellen Sie eine neue Instanz mit dem Namen des Benutzers. </ P>
      @param s_name Darf nicht {@code null} oder leer sein und darf nur Buchstaben, Ziffern und Unterstriche enthalten. Holen Sie sich mit {@code UserConfig # getName () getName ()} {@code ()} .
    ** /
   public UserConfig_Cfg (String s_name) {
      sName = s_name;
   }
   // selbst zurückkehrende Setter ... START
      / **
         <P> Stellen Sie das Alter des Benutzers ein. </ P>
         @param i_age Darf nicht kleiner als Null sein. Holen Sie sich mit {@code UserConfig # getName () getName ()} {@code ()} .
         @see #favoriteColor (String)
       ** /
      public UserConfig_Cfg age (int i_age) {
         iAge = i_age;
         gib das zurück;
      }
      / **
         <P> Stellen Sie die Lieblingsfarbe des Benutzers ein. </ P>
         @param s_color Muss {@code "red"}, {@code "blue"}, {@code green} oder {@code "hot pink"} sein. Holen Sie sich mit {@code UserConfig # getName () getName ()} {@code ()} *.
         @see #age (int)
       ** /
      public UserConfig_Cfg favoriteColor (String s_color) {
         sFavColor = s_color;
         gib das zurück;
      }
   // selbst zurückkehrende Setter ... END
   //getters...START
      public String getName () {
         return sName;
      }
      public int getAge () {
         return iAge;
      }
      public String getFavoriteColor () {
         return sFavColor;
      }
   //getters...END
   / **
      <P> Erstellen Sie die UserConfig wie konfiguriert. </ P>
      @return <CODE> (new {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (this)) </ CODE>
    ** /
   public UserConfig build () {
      return (new UserConfig (this));
   }
}

Aliteralmind
quelle
1
Auf jeden Fall ist es eine Verbesserung. Der hier implementierte Bloch-Builder koppelt zwei konkrete Klassen, nämlich die zu erstellende und den Builder. Das ist schlechtes Design an sich . Der von Ihnen beschriebene Blind Builder unterbricht diese Kopplung, indem die zu erstellende Klasse ihre Konstruktionsabhängigkeit als Abstraktion definiert , die andere Klassen entkoppelt implementieren können. Sie haben eine wesentliche objektorientierte Gestaltungsrichtlinie angewendet.
Rucamzu
3
Du solltest wirklich irgendwo darüber bloggen, wenn du es noch nicht getan hast, schönes Stück Algorithmus-Design! Ich teile es jetzt nicht :-).
Martijn Verburg
4
Wir danken Ihnen für die freundlichen Worte. Dies ist jetzt der erste Beitrag in meinem neuen Blog: aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind
Wenn sowohl der Builder als auch die erstellten Objekte Fieldable implementieren, ähnelt das Muster demjenigen, das ich als ReadableFoo / MutableFoo / ImmutableFoo bezeichnet habe, obwohl ich nicht die Methode habe, ein veränderliches Objekt zum "Build" -Mitglied des Builders zu machen Rufen Sie es auf asImmutableund fügen Sie es in die ReadableFooBenutzeroberfläche ein. [Unter Verwendung dieser Philosophie würde das Aufrufen buildeines unveränderlichen Objekts einfach einen Verweis auf dasselbe Objekt zurückgeben.]
Supercat
1
@ThomasN Sie müssen *_Fieldableneue Getter erweitern und hinzufügen und die erweitern *_Cfgund neue Setter hinzufügen, aber ich verstehe nicht, warum Sie vorhandene Getter und Setter reproduzieren müssen. Sie werden vererbt und müssen nicht neu erstellt werden, es sei denn, sie benötigen andere Funktionen.
Aliteralmind
13

Ich denke, die Frage hier geht von vornherein von etwas aus, ohne zu beweisen, dass das Builder-Muster von Natur aus gut ist.

Ich denke, das Baumuster ist selten, wenn überhaupt, eine gute Idee.


Builder-Muster Zweck

Der Zweck des Builder-Musters besteht darin, zwei Regeln beizubehalten, die das Konsumieren Ihrer Klasse erleichtern:

  1. Objekte sollten nicht in inkonsistenten / unbrauchbaren / ungültigen Zuständen konstruiert werden können.

    • Dies bezieht sich auf Situationen , in denen beispielsweise ein PersonObjekt , ohne konstruiert sein , es Idin gefüllt, während alle Teile des Codes , dass die Verwendung dieses Objekt kann verlangen , die Idnur mit der richtig Arbeit Person.
  2. Objektkonstruktoren sollten nicht zu viele Parameter benötigen .

Der Zweck des Builder-Musters ist also nicht umstritten. Ich denke, ein Großteil des Wunsches und der Verwendung davon basiert auf Analysen, die im Grunde genommen so weit gegangen sind: Wir wollen diese beiden Regeln, das gibt diese beiden Regeln - obwohl ich denke, es lohnt sich, andere Wege zu untersuchen, um diese beiden Regeln zu erreichen.


Warum sollte man sich andere Ansätze ansehen?

Ich denke, der Grund wird durch die Tatsache dieser Frage selbst gut gezeigt; Es gibt Komplexität und viel Zeremonie in den Strukturen, wenn das Baumuster auf sie angewendet wird. Diese Frage stellt sich die Frage, wie ein Teil dieser Komplexität gelöst werden kann, da dies, wie oft, zu einem Szenario führt, das sich seltsam verhält (vererbt). Diese Komplexität erhöht auch den Wartungsaufwand (das Hinzufügen, Ändern oder Entfernen von Eigenschaften ist weitaus komplexer als sonst).


Andere Ansätze

Welche Ansätze gibt es für Regel Nummer eins oben? Der Schlüssel, auf den sich diese Regel bezieht, besteht darin, dass ein Objekt beim Erstellen alle Informationen enthält, die es zum ordnungsgemäßen Funktionieren benötigt. Nach dem Erstellen können diese Informationen nicht mehr extern geändert werden (es handelt sich also um unveränderliche Informationen).

Eine Möglichkeit, einem Objekt bei der Konstruktion alle erforderlichen Informationen zu geben, besteht darin, dem Konstruktor einfach Parameter hinzuzufügen. Wenn diese Informationen vom Konstruktor angefordert werden, können Sie dieses Objekt ohne diese Informationen nicht erstellen. Daher wird es in einen gültigen Zustand versetzt. Aber was ist, wenn das Objekt viele Informationen benötigt, um gültig zu sein? Oh, verdammt, wenn das der Fall wäre, würde dieser Ansatz gegen Regel Nr. 2 verstoßen .

Ok, was gibt es sonst noch? Nun, Sie können einfach alle Informationen, die für einen konsistenten Zustand Ihres Objekts erforderlich sind, in ein anderes Objekt bündeln, das zur Konstruktionszeit erfasst wird. Ihr Code oben anstelle eines Builder-Musters wäre dann:

//DTO...START
public class Cfg  {
   public String sName    ;
   public int    iAge     ;
   public String sFavColor;
}
//DTO...END

public class UserConfig  {
   private final String sName    ;
   private final int    iAge     ;
   private final String sFavColor;
   public UserConfig(Cfg uc_c)  {
      ...
   }

   public String toString()  {
      return  "name=" + sName + ", age=" + iAge + ", sFavColor=" + sFavColor;
   }
}

Dies unterscheidet sich nicht wesentlich vom Builder-Muster, obwohl es etwas einfacher ist, und vor allem erfüllen wir jetzt Regel 1 und Regel 2 .

Warum also nicht das gewisse Extra dazugeben und es zu einem vollwertigen Builder machen? Es ist einfach unnötig . Ich habe beide Zwecke des Builder-Musters in diesem Ansatz erfüllt, mit etwas etwas Einfacherem, leichter zu pflegendem und wiederverwendbarem . Das letzte Bit ist der Schlüssel. Dieses verwendete Beispiel ist imaginär und eignet sich nicht für semantische Zwecke in der Praxis. Lassen Sie uns also zeigen, wie dieser Ansatz zu einem wiederverwendbaren DTO und nicht zu einer einzelnen Zweckklasse führt .

public class NetworkAddress {
   public String Ip;
   public int Port;
   public NetworkAddress Proxy;
}

public class SocketConnection {
   public SocketConnection(NetworkAddress address) {
      ...
   }
}

public class FtpClient {
   public FtpClient(NetworkAddress address) {
      ...
   }
}

Wenn Sie also zusammenhängende DTOs wie diese erstellen, können beide den Zweck des Builder-Musters einfacher und mit einem breiteren Nutzen erfüllen. Darüber hinaus löst dieser Ansatz die Vererbungskomplexität, zu der das Builder-Muster führt:

public class SslCert {
   public NetworkAddress Authority;
   public byte[] PrivateKey;
   public byte[] PublicKey;
}

public class FtpsClient extends FtpClient {
   public FtpsClient(NetworkAddress address, SslCert cert) {
      super(address);
      ...
   }
}

Möglicherweise ist das DTO nicht immer zusammenhängend, oder um die Gruppierung von Eigenschaften zusammenhängend zu machen, müssen sie auf mehrere DTOs aufgeteilt werden. Dies ist eigentlich kein Problem. Wenn für Ihr Objekt 18 Eigenschaften erforderlich sind und Sie mit diesen Eigenschaften drei zusammenhängende DTOs erstellen können, verfügen Sie über eine einfache Konstruktion, die den Zwecken des Erstellers entspricht. Wenn Sie keine zusammenhängenden Gruppierungen finden können, kann dies ein Zeichen dafür sein, dass Ihre Objekte nicht zusammenhängend sind, wenn sie Eigenschaften aufweisen, die nicht in Beziehung zueinander stehen. Aber selbst dann ist es aufgrund der einfacheren Implementierung und dem Plus immer noch vorzuziehen, ein einziges nicht zusammenhängendes DTO zu erstellen Beheben Sie Ihr Vererbungsproblem.


So verbessern Sie das Builder-Muster

Ok, also, Sie haben ein Problem und suchen nach einem Design-Ansatz, um dieses Problem zu lösen. Mein Vorschlag: Das Erben von Klassen kann einfach eine verschachtelte Klasse haben, die von der Builder-Klasse der Superklasse erbt. Die erbende Klasse hat also im Grunde die gleiche Struktur wie die Superklasse und ein Builder-Muster, das mit den zusätzlichen Funktionen genau gleich funktionieren sollte für die zusätzlichen Eigenschaften der Unterklasse.


Wenn es eine gute Idee ist

Abgesehen davon hat das Baumuster eine Nische . Wir alle wissen es, weil wir alle diesen bestimmten Erbauer an der einen oder anderen Stelle gelernt haben:StringBuilder - Hier ist der Zweck keine einfache Konstruktion, da Strings nicht einfacher zu konstruieren und zu verketten sind usw. Dies ist ein großartiger Builder, weil er einen Leistungsvorteil hat .

Der Leistungsvorteil ist also: Sie haben eine Reihe von Objekten, die unveränderlichen Typs sind, und Sie müssen sie auf ein Objekt eines unveränderlichen Typs reduzieren. Wenn Sie es inkrementell tun, werden hier viele Zwischenobjekte erstellt, so dass es weitaus performanter und idealer ist, alles auf einmal zu tun.

Ich denke, der Schlüssel zu einer guten Idee liegt in der Problemdomäne von StringBuilder: Mehrere Instanzen unveränderlicher Typen müssen in eine einzige Instanz eines unveränderlichen Typs umgewandelt werden .

Jimmy Hoffa
quelle
Ich denke nicht, dass Ihr gegebenes Beispiel irgendeine Regel erfüllt. Nichts hindert mich daran, eine Cfg in einem ungültigen Zustand zu erstellen, und obwohl die Parameter aus dem ctor verschoben wurden, wurden sie nur an einen weniger idiomatischen und ausführlicheren Ort verschoben. fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()Bietet eine prägnante API zum Erstellen von Foos und kann die eigentliche Fehlerprüfung im Builder selbst anbieten. Ohne den Builder muss das Objekt selbst seine Eingaben überprüfen, was bedeutet, dass es uns nicht besser geht als früher.
Phoshi
DTOs können ihre Eigenschaften auf zahlreiche Arten deklarativ mit Annotationen validieren lassen, auf dem Setter, wie auch immer Sie vorgehen möchten - die Validierung ist ein separates Problem, und in seinem Builder-Ansatz zeigt er die im Konstruktor stattfindende Validierung, dass dieselbe Logik perfekt passt in meinem Ansatz. Im Allgemeinen ist es jedoch besser, das DTO zur Validierung zu verwenden, da das DTO, wie ich zeige, zum Erstellen mehrerer Typen verwendet werden kann und eine Validierung sich daher zur Validierung mehrerer Typen eignet. Der Builder überprüft nur den Typ, für den er erstellt wurde.
Jimmy Hoffa
Am flexibelsten wäre es vielleicht, eine statische Validierungsfunktion im Builder zu haben, die einen einzelnen FieldableParameter akzeptiert . Ich würde diese Validierungsfunktion vom ToBeBuiltKonstruktor aus aufrufen , aber sie könnte von jedem Ort aus von jedem aufgerufen werden. Dadurch wird das Potenzial für redundanten Code beseitigt, ohne eine bestimmte Implementierung zu erzwingen. (Und es gibt nichts , was Sie daran zu hindern, in den einzelnen Feldern der Validierung Funktion übergeben, wenn Sie das nicht tun FieldableKonzept - aber jetzt wäre es zumindest drei Orte , in denen die Feldliste hätte beibehalten werden.)
Aliteralmind
+1 Und eine Klasse, deren Konstruktor zu viele Abhängigkeiten aufweist, ist offensichtlich nicht zusammenhängend genug und sollte in kleinere Klassen umgestaltet werden.
Basilevs
@ JimmyHoffa: Ah, ich verstehe, das hast du einfach weggelassen. Ich bin nicht sicher, ob ich den Unterschied zwischen diesem und einem Builder sehe. Ansonsten wird eine Konfigurationsinstanz an den ctor übergeben, anstatt .build für einen Builder aufzurufen, und ein Builder verfügt über einen offensichtlicheren Pfad für die Korrektheitsprüfung für alle die Daten. Jede einzelne Variable könnte innerhalb ihrer gültigen Bereiche liegen, in dieser bestimmten Permutation jedoch ungültig sein. .build kann dies überprüfen, aber die Übergabe des Elements an den ctor erfordert eine Fehlerprüfung im Objekt selbst - icky!
Phoshi