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:
- La
ToBeBuiltclase (en este ejemplo: UserConfig)
- Su
Fieldableinterfaz " "
- 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:
ToBeBuilt_Fieldable
ToBeBuilt
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));
}
}