Cómo mejorar el Patrón de construcción de Bloch, para hacerlo más apropiado para su uso en clases altamente extensibles

34

El libro Effective Java de Joshua Bloch (segunda edición) me ha influenciado mucho, probablemente más que con cualquier libro de programación que haya leído. En particular, su Patrón de constructor (elemento 2) ha tenido el mayor efecto.

A pesar de que el constructor de Bloch me llevó mucho más lejos en los dos meses que en mis últimos diez años de programación, todavía me encuentro golpeando el mismo muro: extender las clases con cadenas de métodos de retorno automático es, en el mejor de los casos, desalentador y, en el peor, una pesadilla - especialmente cuando los genéricos entran en juego, y especialmente con los genéricos autorreferenciales (como Comparable<T extends Comparable<T>>).

Hay dos necesidades principales que tengo, solo la segunda en la que me gustaría centrarme en esta pregunta:

  1. El primer problema es "¿cómo compartir cadenas de métodos de retorno automático, sin tener que volver a implementarlas en cada ... clase ...?" Para aquellos que puedan ser curiosos, he abordado esta parte al final de esta respuesta, pero no es en lo que quiero centrarme aquí.

  2. El segundo problema, sobre el que solicito comentarios, es "¿cómo puedo implementar un constructor en clases que están destinadas a ser extendidas por muchas otras clases?" Extender una clase con un constructor es naturalmente más difícil que extender una sin él. Ampliar una clase que tiene un generador que también implementa Needable, y por lo tanto tiene genéricos significativos asociados , es difícil de manejar.

Entonces esa es mi pregunta: ¿Cómo puedo mejorar (lo que llamo) el Bloch Builder, para que pueda asociar un generador a cualquier clase, incluso cuando esa clase está destinada a ser una "clase base" que puede ser extendido y sub extendido muchas veces, sin desanimar a mi futuro o usuarios de mi biblioteca , debido al equipaje adicional que el constructor (y sus posibles genéricos) les imponen?


Anexo
Mi pregunta se enfoca en la parte 2 anterior, pero quería desarrollar un poco el problema uno, incluida la forma en que lo he tratado:

El primer problema es "¿cómo compartir cadenas de métodos de retorno automático, sin tener que volver a implementarlas en cada ... clase ...?" Esto no es para evitar que las clases extendidas tengan que volver a implementar estas cadenas, que, por supuesto, deben, más bien, cómo evitar que las no subclases , que desean aprovechar estas cadenas de métodos, tengan que volver a implementar -Implementar todas las funciones de retorno automático para que sus usuarios puedan aprovecharlas. Para esto, se me ocurrió un diseño needer-needable en el que imprimiré los esqueletos de interfaz por aquí, y lo dejaré así por ahora. Me ha funcionado bien (este diseño tardó años en hacerse ... la parte más difícil fue evitar las dependencias circulares):

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);
}
mente mental
fuente

Respuestas:

21

He creado lo que, para mí, es una gran mejora sobre el Patrón de construcción de Josh Bloch. No quiere decir de ninguna manera que sea "mejor", solo que en una situación muy específica , proporciona algunas ventajas, siendo la más importante que desacopla al constructor de su clase por construir.

He documentado a fondo esta alternativa a continuación, a la que llamo el patrón Blind Builder.


Patrón de diseño: constructor ciego

Como alternativa al Patrón de construcción de Joshua Bloch (elemento 2 en Java efectivo, 2a edición), he creado lo que llamo el "Patrón de construcción ciega", que comparte muchos de los beneficios del Constructor de Bloch y, aparte de un solo personaje, se usa exactamente de la misma manera. Los constructores ciegos tienen la ventaja de

  • desacoplar el constructor de su clase envolvente, eliminando una dependencia circular,
  • reduce en gran medida el tamaño del código fuente de (lo que ya no es ) la clase adjunta, y
  • permite que la ToBeBuiltclase se extienda sin tener que extender su generador .

En esta documentación, me referiré a la clase que se está creando como la ToBeBuiltclase " ".

Una clase implementada con un Bloch Builder

Un Bloch Builder está public static classcontenido dentro de la clase que construye. Un ejemplo:

UserConfig de clase pública {
   Cadena privada final sName;
   privado final int iAge;
   privado final String sFavColor;
   public UserConfig (UserConfig.Cfg uc_c) {// CONSTRUCTOR
      //transferir
         tratar {
            sName = uc_c.sName;
         } catch (NullPointerException rx) {
            lanzar nueva NullPointerException ("uc_c");
         }
         iAge = uc_c.iAge;
         sFavColor = uc_c.sFavColor;
      // VALIDE TODOS LOS CAMPOS AQUÍ
   }
   public String toString () {
      return "nombre =" + sName + ", edad =" + iAge + ", sFavColor =" + sFavColor;
   }
   //builder...START
   clase estática pública Cfg {
      Nombre de cadena privado;
      privado int iAge;
      Private String sFavColor;
      Cfg público (String s_name) {
         sName = s_name;
      }
      // setters de retorno automático ... START
         public Cfg age (int i_age) {
            iAge = i_age;
            devuelve esto;
         }
         public Cfg favoriteColor (String s_color) {
            sFavColor = s_color;
            devuelve esto;
         }
      // setters de retorno automático ... END
      public UserConfig build () {
         return (nuevo UserConfig (este));
      }
   }
   //builder ...END
}

Instanciar una clase con un Bloch Builder

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

La misma clase, implementada como Blind Builder

Hay tres partes en un Blind Builder, cada una de las cuales está en un archivo de código fuente separado:

  1. La ToBeBuiltclase (en este ejemplo: UserConfig)
  2. Su Fieldableinterfaz " "
  3. El constructor

1. La clase a construir

La clase to-be-build acepta su Fieldableinterfaz como su único parámetro constructor. El constructor establece todos los campos internos y valida cada uno. Lo más importante, esta ToBeBuiltclase no tiene conocimiento de su constructor.

UserConfig de clase pública {
   Cadena privada final sName;
   privado final int iAge;
   privado final String sFavColor;
    public UserConfig (UserConfig_Fieldable uc_f) {// CONSTRUCTOR
      //transferir
         tratar {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            lanzar nueva NullPointerException ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      // VALIDE TODOS LOS CAMPOS AQUÍ
   }
   public String toString () {
      return "nombre =" + sName + ", edad =" + iAge + ", sFavColor =" + sFavColor;
   }
}

Como señaló un comentarista inteligente (que eliminó inexplicablemente su respuesta), si la ToBeBuiltclase también implementa su Fieldable, su único constructor puede usarse como su constructor principal y de copia (una desventaja es que los campos siempre se validan, aunque se sabe que los campos en el original ToBeBuiltson válidos).

2. La Fieldableinterfaz " "

La interfaz desplegable es el "puente" entre la ToBeBuiltclase y su generador, que define todos los campos necesarios para construir el objeto. El ToBeBuiltconstructor de clases requiere esta interfaz y el constructor la implementa. Dado que esta interfaz puede ser implementada por otras clases que no sean el generador, cualquier clase puede crear una instancia de la ToBeBuiltclase fácilmente , sin verse obligada a usar su generador. Esto también hace que sea más fácil extender la ToBeBuiltclase, cuando extender su generador no es deseable o necesario.

Como se describe en la sección a continuación, no documenté las funciones de esta interfaz en absoluto.

interfaz pública UserConfig_Fieldable {
   Cadena getName ();
   int getAge ();
   Cadena getFavoriteColor ();
}

3. El constructor

El constructor implementa la Fieldableclase. No valida en absoluto, y para enfatizar este hecho, todos sus campos son públicos y mutables. Si bien esta accesibilidad pública no es un requisito, lo prefiero y lo recomiendo, porque refuerza el hecho de que la validación no ocurre hasta que ToBeBuiltse llama al constructor. Esto es importante, porque es posible que otro subproceso manipule aún más el constructor, antes de pasarlo al ToBeBuiltconstructor. La única forma de garantizar que los campos sean válidos, suponiendo que el constructor no pueda "bloquear" de alguna manera su estado, es que la ToBeBuiltclase haga la verificación final.

Finalmente, como con la Fieldableinterfaz, no documenté ninguno de sus captadores.

La clase pública UserConfig_Cfg implementa UserConfig_Fieldable {
   Nombre de cadena pública;
   public int iAge;
    public String sFavColor;
    public UserConfig_Cfg (String s_name) {
       sName = s_name;
    }
    // setters de retorno automático ... START
       public UserConfig_Cfg age (int i_age) {
          iAge = i_age;
          devuelve esto;
       }
       public UserConfig_Cfg favoriteColor (String s_color) {
          sFavColor = s_color;
          devuelve esto;
       }
    // setters de retorno automático ... END
    //getters...START
       public String getName () {
          devolver sName;
       }
       public int getAge () {
          volver iAge;
       }
       public String getFavoriteColor () {
          devolver sFavColor;
       }
    //getters ...END
    public UserConfig build () {
       return (nuevo UserConfig (este));
    }
}

Crear instancias de una clase con un Blind Builder

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

La única diferencia es " UserConfig_Cfg" en lugar de " UserConfig.Cfg"

Notas

Desventajas

  • Blind Builders no puede acceder a miembros privados de su ToBeBuiltclase,
  • Son más detallados, ya que ahora se requieren getters tanto en el constructor como en la interfaz.
  • Todo para una sola clase ya no está en un solo lugar .

Compilar un Blind Builder es sencillo:

  1. ToBeBuilt_Fieldable
  2. ToBeBuilt
  3. ToBeBuilt_Cfg

La Fieldableinterfaz es completamente opcional.

Para una ToBeBuiltclase con pocos campos obligatorios, como esta UserConfigclase de ejemplo, el constructor podría simplemente ser

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

Y llamó al constructor con

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

O incluso eliminando los captadores (en el generador) por completo:

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

Al pasar los campos directamente, la ToBeBuiltclase es tan "ciega" (sin darse cuenta de su constructor) como lo es con la Fieldableinterfaz. Sin embargo, para las ToBeBuiltclases que están destinadas a ser "extendidas y sub-extendidas muchas veces" (que está en el título de esta publicación), cualquier cambio en cualquier campo requiere cambios en cada subclase, en cada constructor y ToBeBuiltconstructor. A medida que aumenta el número de campos y subclases, resulta poco práctico mantenerlo.

(De hecho, con pocos campos necesarios, usar un constructor puede ser excesivo. Para aquellos interesados, aquí hay una muestra de algunas de las interfaces Fieldable más grandes en mi biblioteca personal).

Clases secundarias en subpaquete

Elijo tener todos los constructores y las Fieldableclases, para todos los Constructores ciegos, en un subpaquete de su ToBeBuiltclase. El subpaquete siempre se llama " z". Esto evita que estas clases secundarias llenen la lista de paquetes JavaDoc. Por ejemplo

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

Ejemplo de validación

Como se mencionó anteriormente, toda la validación ocurre en el ToBeBuiltconstructor de 's. Aquí está el constructor nuevamente con un código de validación de ejemplo:

Public UserConfig (UserConfig_Fieldable uc_f) {
   //transferir
      tratar {
         sName = uc_f.getName ();
      } catch (NullPointerException rx) {
         lanzar nueva NullPointerException ("uc_f");
      }
      iAge = uc_f.getAge ();
      sFavColor = uc_f.getFavoriteColor ();
   // validar (realmente debería precompilar los patrones ...)
      tratar {
         if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
            lanzar una nueva IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") puede no estar vacío y debe contener solo letras, dígitos y guiones bajos.");
         }
      } catch (NullPointerException rx) {
         lanzar una nueva NullPointerException ("uc_f.getName ()");
      }
      if (iAge <0) {
         lanzar una nueva IllegalArgumentException ("uc_f.getAge () (" + iAge + ") es menor que cero");
      }
      tratar {
         if (! Pattern.compile ("(?: rojo | azul | verde | rosa fuerte)"). matcher (sFavColor) .matches ()) {
            lanzar una nueva IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") no es rojo, azul, verde o rosa fuerte");
         }
      } catch (NullPointerException rx) {
         lanzar una nueva NullPointerException ("uc_f.getFavoriteColor ()");
      }
}

Documentación de constructores

Esta sección es aplicable tanto a Bloch Builders como a Blind Builders. Demuestra cómo documento las clases en este diseño, haciendo que los colocadores (en el generador) y sus captadores (en la ToBeBuiltclase) se crucen directamente entre sí, con un solo clic del mouse y sin que el usuario necesite saber dónde esas funciones realmente residen, y sin que el desarrollador tenga que documentar nada de forma redundante.

Getters: solo en las ToBeBuiltclases

Los captadores se documentan solo en la ToBeBuiltclase. Los captadores equivalentes en las clases _Fieldabley_Cfg se ignoran. No los documenté en absoluto.

/ **
   <P> La edad del usuario. </P>
   @return Un int que representa la edad del usuario.
   @ver UserConfig_Cfg # age (int)
   @ver getName ()
 ** /
public int getAge () {
   volver iAge;
}

El primero @seees un enlace a su setter, que está en la clase de generador.

Setters: en la clase de constructor

El colocador se documenta como si se encuentra en la ToBeBuiltclase , y también como si lo hace la validación (que realmente se lleva a cabo por el ToBeBuiltconstructor 's). El asterisco (" *") es una pista visual que indica que el objetivo del enlace está en otra clase.

/ **
   <P> Establecer la edad del usuario. </P>
   @param i_age No puede ser menor que cero. Obtenga con {@code UserConfig # getName () getName ()} *.
   @ver #favoriteColor (Cadena)
 ** /
public UserConfig_Cfg age (int i_age) {
   iAge = i_age;
   devuelve esto;
}

Más información

Poniendo todo junto: la fuente completa del ejemplo de Blind Builder, con documentación completa

UserConfig.java

import java.util.regex.Pattern;
/ **
   <P> Información sobre un usuario - <I> [constructor: UserConfig_Cfg] </I> </P>
   <P> La validación de todos los campos ocurre en este constructor de clases. Sin embargo, cada requisito de validación es documento solo en las funciones de establecimiento del constructor. </P>
   <P> {@ código de Java xbn.z.xmpl.lang.builder.finalv.UserConfig} </ P>
 ** /
UserConfig de clase pública {
   public static final void main (String [] igno_red) {
      UserConfig uc = new UserConfig_Cfg ("Kermit"). Age (50) .favoriteColor ("verde"). Build ();
      System.out.println (uc);
   }
   Cadena privada final sName;
   privado final int iAge;
   privado final String sFavColor;
   / **
      <P> Crear una nueva instancia. Esto establece y valida todos los campos. </P>
      @param uc_f Puede no ser {@code null}.
    ** /
   Public UserConfig (UserConfig_Fieldable uc_f) {
      //transferir
         tratar {
            sName = uc_f.getName ();
         } catch (NullPointerException rx) {
            lanzar nueva NullPointerException ("uc_f");
         }
         iAge = uc_f.getAge ();
         sFavColor = uc_f.getFavoriteColor ();
      //validar
         tratar {
            if (! Pattern.compile ("\\ w +"). matcher (sName) .matches ()) {
               lanzar una nueva IllegalArgumentException ("uc_f.getName () (\" "+ sName +" \ ") puede no estar vacío y debe contener solo letras, dígitos y guiones bajos.");
            }
         } catch (NullPointerException rx) {
            lanzar una nueva NullPointerException ("uc_f.getName ()");
         }
         if (iAge <0) {
            lanzar una nueva IllegalArgumentException ("uc_f.getAge () (" + iAge + ") es menor que cero");
         }
         tratar {
            if (! Pattern.compile ("(?: rojo | azul | verde | rosa fuerte)"). matcher (sFavColor) .matches ()) {
               lanzar una nueva IllegalArgumentException ("uc_f.getFavoriteColor () (\" "+ uc_f.getFavoriteColor () +" \ ") no es rojo, azul, verde o rosa fuerte");
            }
         } catch (NullPointerException rx) {
            lanzar una nueva NullPointerException ("uc_f.getFavoriteColor ()");
         }
   }
   //getters...START
      / **
         <P> El nombre del usuario. </P>
         @return Una cadena no vacía {@ code null}.
         @see UserConfig_Cfg # UserConfig_Cfg (String)
         @ver #getAge ()
         @ver #getFavoriteColor ()
       ** /
      public String getName () {
         devolver sName;
      }
      / **
         <P> La edad del usuario. </P>
         @return Un número mayor que o igual a cero.
         @ver UserConfig_Cfg # age (int)
         @ver #getName ()
       ** /
      public int getAge () {
         volver iAge;
      }
      / **
         <P> El color favorito del usuario. </P>
         @return Una cadena no vacía {@ code null}.
         @ver UserConfig_Cfg # age (int)
         @ver #getName ()
       ** /
      public String getFavoriteColor () {
         devolver sFavColor;
      }
   //getters ...END
   public String toString () {
      return "getName () =" + getName () + ", getAge () =" + getAge () + ", getFavoriteColor () =" + getFavoriteColor ();
   }
}

UserConfig_Fieldable.java

/ **
   <P> Requerido por el {@link UserConfig} {@code UserConfig # UserConfig (UserConfig_Fieldable) constructor}. </P>
 ** /
interfaz pública UserConfig_Fieldable {
   Cadena getName ();
   int getAge ();
   Cadena getFavoriteColor ();
}

UserConfig_Cfg.java

import java.util.regex.Pattern;
/ **
   <P> Generador para {@link UserConfig}. </P>
   <P> La validación de todos los campos se produce en el constructor <CODE> UserConfig </CODE>. Sin embargo, cada requisito de validación es documento solo en las funciones de establecimiento de esta clase. </P>
 ** /
La clase pública UserConfig_Cfg implementa UserConfig_Fieldable {
   Nombre de cadena pública;
   public int iAge;
   public String sFavColor;
   / **
      <P> Cree una nueva instancia con el nombre del usuario. </P>
      @param s_name Puede no estar {@code null} o estar vacío, y debe contener solo letras, dígitos y guiones bajos. Obtenga con {@code UserConfig # getName () getName ()} {@ code ()} .
    ** /
   public UserConfig_Cfg (String s_name) {
      sName = s_name;
   }
   // setters de retorno automático ... START
      / **
         <P> Establecer la edad del usuario. </ P>
         @param i_age No puede ser menor que cero. Obtenga con {@code UserConfig # getName () getName ()} {@ code ()} .
         @see #favoriteColor (String)
       ** /
      public UserConfig_Cfg age (int i_age) {
         iAge = i_age;
         devuelve esto;
      }
      / **
         <P> Establezca el color favorito del usuario. </P>
         @param s_color Debe ser {@code "rojo"}, {@code "azul"}, {@code green} o {@code "hot pink"}. Obtenga con {@code UserConfig # getName () getName ()} {@ code ()} *.
         @ver #age (int)
       ** /
      public UserConfig_Cfg favoriteColor (String s_color) {
         sFavColor = s_color;
         devuelve esto;
      }
   // setters de retorno automático ... END
   //getters...START
      public String getName () {
         devolver sName;
      }
      public int getAge () {
         volver iAge;
      }
      public String getFavoriteColor () {
         devolver sFavColor;
      }
   //getters ...END
   / **
      <P> Cree el UserConfig, como está configurado. </P>
      @return <CODE> (nuevo {@link UserConfig # UserConfig (UserConfig_Fieldable) UserConfig} (esto)) </CODE>
    ** /
   public UserConfig build () {
      return (nuevo UserConfig (este));
   }
}

mente mental
fuente
1
En definitiva, se trata de una mejora. Constructor de la Bloch, tal como se aplica aquí, parejas dos concretos clases, siendo estos el a-ser-construida uno y su constructor. Este es un mal diseño per se . El Generador de Ciegos se describe que rompe el acoplamiento haciendo que el a-ser incorporado clase definen su dependencia de la construcción como una abstracción , que otras clases pueden implementar de una manera disociada. Has aplicado en gran medida lo que es una directriz esencial de diseño orientado a objetos.
rucamzu
3
Usted realmente debe blog acerca de esto en alguna parte si no lo ha hecho, buena pieza de diseño de algoritmos! Estoy fuera compartirlo ahora :-).
Martijn Verburg
44
Gracias por las amables palabras. Este es ahora el primer post en mi nuevo blog: aliteralmind.wordpress.com/2014/02/14/blind_builder
aliteralmind
Si el constructor y los objetos construidos implementan Fieldable, el patrón comienza a parecerse a uno que he denominado ReadableFoo / MutableFoo / ImmutableFoo, aunque en lugar de tener el método para hacer que una cosa mutable sea el miembro "build" del constructor, yo llámelo asImmutablee inclúyalo en la ReadableFoointerfaz [usando esa filosofía, llamar builda un objeto inmutable simplemente devolvería una referencia al mismo objeto].
supercat
1
@ThomasN Necesitas extenderlo *_Fieldabley agregarle nuevos captadores, y extenderlo *_Cfg, y agregarle nuevos establecedores, pero no veo por qué necesitarías reproducir captadores y establecedores existentes. Se heredan y, a menos que necesiten una funcionalidad diferente, no hay necesidad de recrearlos.
aliteralmind
13

Creo que la pregunta aquí supone algo desde el principio sin intentar probarlo, que el patrón de construcción es inherentemente bueno.

tl; dr Creo que el patrón de construcción rara vez es una buena idea.


Propósito del patrón de constructor

El propósito del patrón de construcción es mantener dos reglas que facilitarán el consumo de su clase:

  1. Los objetos no deben poder construirse en estados inconsistentes / inutilizables / inválidos.

    • Esto se refiere a escenarios en los que, por ejemplo, un Personobjeto puede construirse sin haberlo Idrellenado, mientras que todas las piezas de código que usan ese objeto pueden requerir el Idjusto para funcionar correctamente con el Person.
  2. Los constructores de objetos no deberían requerir demasiados parámetros .

Por lo tanto, el propósito del patrón de construcción no es controvertido. Creo que gran parte de su deseo y uso se basa en un análisis que básicamente ha llegado tan lejos: queremos estas dos reglas, esto nos da estas dos reglas, aunque creo que vale la pena investigar otras formas de lograr esas dos reglas.


¿Por qué molestarse en mirar otros enfoques?

Creo que la razón está bien demostrada por el hecho de esta pregunta en sí; Hay complejidad y mucha ceremonia añadida a las estructuras al aplicarles el patrón de construcción. Esta pregunta es cómo resolver parte de esa complejidad porque, como lo hace a menudo, crea un escenario que se comporta de manera extraña (heredando). Esta complejidad también aumenta la sobrecarga de mantenimiento (agregar, cambiar o eliminar propiedades es mucho más complejo que lo contrario).


Otros enfoques

Entonces, para la regla número uno anterior, ¿qué enfoques hay? La clave a la que se refiere esta regla es que durante la construcción, un objeto tiene toda la información que necesita para funcionar correctamente, y después de la construcción, esa información no se puede cambiar externamente (por lo tanto, es información inmutable).

Una forma de dar toda la información necesaria a un objeto en la construcción es simplemente agregar parámetros al constructor. Si el constructor exige esa información, no podrá construir este objeto sin toda esa información, por lo tanto, se convertirá en un estado válido. Pero, ¿qué pasa si el objeto requiere mucha información para ser válido? Oh dang, si ese es el caso, este enfoque rompería la regla # 2 anterior .

Ok, ¿qué más hay? Bueno, simplemente podría tomar toda la información necesaria para que su objeto esté en un estado consistente y agruparlo en otro objeto que se toma en el momento de la construcción. Su código anterior en lugar de tener un patrón de generador sería:

//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;
   }
}

Esto no es muy diferente del patrón de construcción, aunque es un poco más simple y lo más importante es que estamos cumpliendo la regla # 1 y la regla # 2 ahora .

Entonces, ¿por qué no ir un poco más y convertirlo en un generador completo? Es simplemente innecesario . Cumplí ambos propósitos del patrón de construcción en este enfoque, con algo un poco más simple, más fácil de mantener y reutilizable . Ese último bit es clave, este ejemplo que se usa es imaginario y no se presta para un propósito semántico del mundo real, así que vamos a mostrar cómo este enfoque da como resultado un DTO reutilizable en lugar de una clase de propósito único .

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) {
      ...
   }
}

Entonces, cuando construye DTO cohesivos como este, ambos pueden satisfacer el propósito del patrón de construcción, de manera más simple y con un valor / utilidad más amplio. Además, este enfoque resuelve la complejidad de la herencia que produce el patrón de construcción:

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);
      ...
   }
}

Es posible que el DTO no siempre sea coherente, o para que los grupos de propiedades sean coherentes, deben dividirse en varios DTO; esto no es realmente un problema. Si su objeto requiere 18 propiedades y puede hacer 3 DTO cohesivos con esas propiedades, tiene una construcción simple que cumple con los propósitos de los constructores, y algo más. Si no puede encontrar agrupaciones cohesivas, esto puede ser una señal de que sus objetos no son cohesivos si tienen propiedades que no están relacionadas, pero aun así, es preferible hacer un solo DTO no cohesivo debido a la implementación más simple más resolviendo su problema de herencia.


Cómo mejorar el patrón de construcción

Ok, entonces, dejando a un lado todo el rimble duelo, tienes un problema y estás buscando un enfoque de diseño para resolverlo. Mi sugerencia: las clases heredadas simplemente pueden tener una clase anidada que hereda de la clase de generador de la superclase, por lo que la clase de herencia tiene básicamente la misma estructura que la superclase y tiene un patrón de generador que debería funcionar exactamente igual con las funciones adicionales para las propiedades adicionales de la subclase ..


Cuando es una buena idea

Despotricando a un lado, el patrón constructor tiene un nicho . Todos lo sabemos porque todos aprendimos este constructor particular en un momento u otro: StringBuilder- aquí el propósito no es simple construcción, porque las cadenas no podrían ser más fáciles de construir y concatenar, etc. Este es un gran constructor porque tiene un beneficio de rendimiento .

El beneficio de rendimiento es, por lo tanto: tiene un montón de objetos, son de un tipo inmutable, necesita colapsarlos a un objeto de un tipo inmutable. Si lo hace de forma incremental, aquí creará muchos objetos intermedios, por lo que hacerlo todo a la vez es mucho más eficaz e ideal.

Así que creo que la clave de cuándo es una buena idea está en el dominio del problema de StringBuilder: Necesidad de convertir varias instancias de tipos inmutables en una sola instancia de un tipo inmutable .

Jimmy Hoffa
fuente
No creo que su ejemplo dado satisfaga ninguna de las reglas. No hay nada que me impida crear un Cfg en un estado no válido, y aunque los parámetros se han movido fuera del ctor, simplemente se han movido a un lugar menos idiomático y más detallado. fooBuilder.withBar(2).withBang("Hello").withBaz(someComplexObject).build()ofrece una API sucinta para construir foos y puede ofrecer una comprobación de errores real en el propio constructor. Sin el constructor, el propio objeto tiene que verificar sus entradas, lo que significa que no estamos mejor de lo que solíamos estar.
Phoshi
Los DTO pueden validar sus propiedades de numerosas maneras de forma declarativa con anotaciones, en el setter, como quiera que lo haga: la validación es un problema separado y en su enfoque de constructor muestra que la validación ocurre en el constructor, esa misma lógica encajaría perfectamente bien En mi enfoque. Sin embargo, generalmente sería mejor usar el DTO para que valide porque, como lo muestro, el DTO se puede usar para construir múltiples tipos y, por lo tanto, tener la validación en él se prestaría para validar múltiples tipos. El constructor solo valida para el tipo particular para el que está hecho.
Jimmy Hoffa
Quizás la forma más flexible sería tener una función de validación estática en el generador, que acepta un solo Fieldableparámetro. Me gustaría llamar a esta función de validación del ToBeBuiltconstructor, pero podría ser llamado por cualquier cosa, desde cualquier lugar. Esto elimina la posibilidad de código redundante, sin forzar una implementación específica. (Y no hay nada que le impida pasar campos individuales a la función de validación, si no le gusta el Fieldableconcepto, pero ahora habría al menos tres lugares en los que debería mantenerse la lista de campos)
aliteralmind
+1 Y una clase que tiene demasiadas dependencias en su constructor obviamente no es lo suficientemente coherente y debería refactorizarse en clases más pequeñas.
Basilevs
@JimmyHoffa: Ah, ya veo, acabas de omitir eso. No estoy seguro de ver la diferencia entre esto y un generador, entonces, aparte de esto, pasa una instancia de configuración al ctor en lugar de llamar a .build en algún generador, y que un generador tiene una ruta más obvia para verificar la corrección en todos los datos. Cada variable individual podría estar dentro de sus rangos válidos, pero no válidos en esa permutación particular. .build puede comprobar esto, pero pasar el elemento al ctor requiere una comprobación de errores dentro del objeto en sí mismo: ¡asqueroso!
Phoshi