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
ToBeBuilt
clase se extienda sin tener que extender su generador .
En esta documentación, me referiré a la clase que se está creando como la ToBeBuilt
clase " ".
Una clase implementada con un Bloch Builder
Un Bloch Builder está public static class
contenido 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:
- La
ToBeBuilt
clase (en este ejemplo: UserConfig
)
- Su
Fieldable
interfaz " "
- El constructor
1. La clase a construir
La clase to-be-build acepta su Fieldable
interfaz como su único parámetro constructor. El constructor establece todos los campos internos y valida cada uno. Lo más importante, esta ToBeBuilt
clase 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 ToBeBuilt
clase 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 ToBeBuilt
son válidos).
2. La Fieldable
interfaz " "
La interfaz desplegable es el "puente" entre la ToBeBuilt
clase y su generador, que define todos los campos necesarios para construir el objeto. El ToBeBuilt
constructor 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 ToBeBuilt
clase fácilmente , sin verse obligada a usar su generador. Esto también hace que sea más fácil extender la ToBeBuilt
clase, 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 Fieldable
clase. 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 ToBeBuilt
se llama al constructor. Esto es importante, porque es posible que otro subproceso manipule aún más el constructor, antes de pasarlo al ToBeBuilt
constructor. 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 ToBeBuilt
clase haga la verificación final.
Finalmente, como con la Fieldable
interfaz, 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
ToBeBuilt
clase,
- 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:
ToBeBuilt_Fieldable
ToBeBuilt
ToBeBuilt_Cfg
La Fieldable
interfaz es completamente opcional.
Para una ToBeBuilt
clase con pocos campos obligatorios, como esta UserConfig
clase 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 ToBeBuilt
clase es tan "ciega" (sin darse cuenta de su constructor) como lo es con la Fieldable
interfaz. Sin embargo, para las ToBeBuilt
clases 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 ToBeBuilt
constructor. 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 Fieldable
clases, para todos los Constructores ciegos, en un subpaquete de su ToBeBuilt
clase. 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 ToBeBuilt
constructor 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 ToBeBuilt
clase) 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 ToBeBuilt
clases
Los captadores se documentan solo en la ToBeBuilt
clase. Los captadores equivalentes en las clases _Fieldable
y
_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 @see
es 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 ToBeBuilt
clase , y también como si lo hace la validación (que realmente se lleva a cabo por el ToBeBuilt
constructor '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));
}
}
asImmutable
e inclúyalo en laReadableFoo
interfaz [usando esa filosofía, llamarbuild
a un objeto inmutable simplemente devolvería una referencia al mismo objeto].*_Fieldable
y 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.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:
Los objetos no deben poder construirse en estados inconsistentes / inutilizables / inválidos.
Person
objeto puede construirse sin haberloId
rellenado, mientras que todas las piezas de código que usan ese objeto pueden requerir elId
justo para funcionar correctamente con elPerson
.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:
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 .
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:
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 .fuente
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.Fieldable
parámetro. Me gustaría llamar a esta función de validación delToBeBuilt
constructor, 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 elFieldable
concepto, pero ahora habría al menos tres lugares en los que debería mantenerse la lista de campos)