java - ¿Es este Monster Builder un buen patrón Builder/Factory para abstraer constructores largos mezclados con setters?
design-patterns dsl (4)
Esta es una pregunta de interfaz humana sobre la combinación del patrón del generador de pasos con los patrones del generador enhanced o wizard en un DSL . Utiliza una interfaz fluida, aunque utiliza el método de encadenamiento, no en cascada. Es decir, los métodos devuelven diferentes tipos.
Me enfrento a una clase de monstruos que tiene dos constructores que toman una mezcla de ints, Strings y una serie de Strings. Cada constructor tiene 10 parámetros de longitud. También cuenta con unos 40 configuradores opcionales; algunos de los cuales entran en conflicto entre sí si se usan juntos. Su código de construcción es algo así:
Person person = Person("Homer","Jay", "Simpson","Homie", null, "black", "brown",
new Date(1), 3, "Homer Thompson", "Pie Man", "Max Power", "El Homo",
"Thad Supersperm", "Bald Mommy", "Rock Strongo", "Lance Uppercut", "Mr. Plow");
person.setClothing("Pants!!");
person.setFavoriteBeer("Duff");
person.setJobTitle("Safety Inspector");
Esto eventualmente falla porque resulta que haber establecido la cerveza favorita y el título del trabajo es incompatible. Suspiro.
Rediseñar la clase de monstruos no es una opción. Es ampliamente utilizado. Funciona. Simplemente no quiero ver cómo se construye directamente. Quiero escribir algo limpio que lo alimente. Algo que seguirá sus reglas sin que los desarrolladores las memoricen.
Contrariamente a los maravillosos patrones de construcción que he estado estudiando, esto no viene en sabores o categorías. Exige algunos campos todo el tiempo y otros campos cuando es necesario y otros solo en función de lo que se haya establecido anteriormente. Los constructores no son telescópicos. Proporcionan dos formas alternativas de llevar a la clase al mismo estado. Son largas y feas. Lo que quieren alimentarles varía independientemente.
Un constructor fluido definitivamente haría que los constructores largos sean más fáciles de ver. Sin embargo, el número masivo de configuradores opcionales desordenan los requeridos. Y hay un requisito que un constructor de fluidez en cascada no satisface: compilar el cumplimiento de tiempo.
Los constructores obligan al desarrollador a agregar explícitamente los campos obligatorios, incluso si los anula. Esto se pierde cuando se utiliza un constructor de fluidez en cascada. De la misma manera se pierde con los setters. Quiero una forma de evitar que el desarrollador construya hasta que se haya agregado cada campo requerido.
A diferencia de muchos patrones de constructor, lo que busco no es inmutabilidad. Me voy de la clase como la encontré. Quiero saber que el objeto construido está en buen estado con solo mirar el código que lo construye. Sin necesidad de referirse a la documentación. Esto significa que debe llevar al programador a los pasos condicionales requeridos.
Person makeHomer(PersonBuilder personBuilder){ //Injection avoids hardcoding implementation
return personBuilder
// -- These have good default values, may be skipped, and don''t conflict -- //
.doOptional()
.addClothing("Pants!!") //Could also call addTattoo() and 36 others
// -- All fields that always must be set. @NotNull might be handy. -- //
.doRequired() //Forced to call the following in order
.addFirstName("Homer")
.addMiddleName("Jay")
.addLastName("Simpson")
.addNickName("Homie")
.addMaidenName(null) //Forced to explicitly set null, a good thing
.addEyeColor("black")
.addHairColor("brown")
.addDateOfBirth(new Date(1))
.addAliases(
"Homer Thompson",
"Pie Man",
"Max Power",
"El Homo",
"Thad Supersperm",
"Bald Mommy",
"Rock Strongo",
"Lance Uppercut",
"Mr. Plow")
// -- Controls alternatives for setters and the choice of constructors -- //
.doAlternatives() //Either x or y. a, b, or c. etc.
.addBeersToday(3) //Now can''t call addHowDrunk("Hammered");
.addFavoriteBeer("Duff")//Now can’t call addJobTitle("Safety Inspector");
.doBuild() //Not available until now
;
}
La persona puede construirse después de addBeersToday (), ya que en ese momento se conoce toda la información del constructor, pero no se devuelve hasta doBuild ().
public Person(String firstName, String middleName, String lastName,
String nickName, String maidenName, String eyeColor,
String hairColor, Date dateOfBirth, int beersToday,
String[] aliases);
public Person(String firstName, String middleName, String lastName,
String nickName, String maidenName, String eyeColor,
String hairColor, Date dateOfBirth, String howDrunk,
String[] aliases);
Estos parámetros establecen campos que nunca deben dejarse con valores predeterminados. beersToday and howDrunk establece el mismo campo de diferentes maneras. favoriteBeer y jobTitle son campos diferentes pero causan conflictos con la forma en que se usa la clase, por lo que solo se debe establecer uno. Se manejan con setters no constructores.
El método doBuild()
devuelve un objeto Person
. Es el único que lo hace y Person
es el único tipo que devolverá. Cuando lo hace, la Person
está completamente inicializada.
En cada paso de la interfaz, el tipo devuelto no es siempre el mismo. Cambiar el tipo es cómo se guía al desarrollador a través de los pasos. Sólo ofrece métodos válidos. El método doBuild()
no está disponible hasta que se hayan completado todos los pasos necesarios.
Los prefijos do / add son un truco para facilitar la escritura porque el cambio de tipo de retorno no coincide con la asignación y hace que las recomendaciones de inteligencia se conviertan alfabéticamente en eclipse. He confirmado que intellij no tiene este problema. Gracias NimChimpsky.
Esta pregunta es sobre la interfaz, así que aceptaré respuestas que no proporcionen una implementación. Pero si usted sabe de uno, por favor comparta.
Si sugieres un patrón alternativo, muestra la interfaz que se está utilizando. Usa todas las entradas del ejemplo.
Si sugiere utilizar la interfaz que se presenta aquí, o una pequeña variación, defiéndala de críticas como this .
Lo que realmente quiero saber es si la mayoría de la gente preferiría usar esta interfaz para construir o alguna otra. Esta es una pregunta de interfaz humana. ¿Esto viola PoLA ? No te preocupes por lo difícil que sería implementar.
Sin embargo, si tienes curiosidad por las implementaciones:
Un intento fallido (no tenía suficientes estados ni entendía válido contra no predeterminado)
Una implementación de constructor de pasos (no lo suficientemente flexible para múltiples constructores o alternativas)
enhanced (trazador de líneas aún pero tiene estados flexibles)
wizard (se ocupa de forking pero no recuerda la ruta para elegir un constructor)
Requisito:
- La clase de monstruo (persona) ya está cerrada a modificación y extensión; sin contacto
Metas:
- Oculta los constructores largos ya que la clase monstruo tiene 10 parámetros requeridos
- Determine a qué constructor llamar basándose en las alternativas utilizadas
- No permitir setters conflictivos
- Hacer cumplir las reglas en tiempo de compilación
Intención:
- Señala claramente cuando los valores por defecto no son aceptables
Así que he tenido varios años para pensar en esto, y creo que ahora conozco la manera correcta de hacerlo.
Primero: implemente cada configuración obligatoria como una interfaz de método único, que devuelve la siguiente interfaz de método único. Esto obliga al cliente a completar todos los parámetros requeridos, y tiene el beneficio adicional de que todos deben completarse en el mismo orden en todo el código, lo que facilita la detección de errores.
En segundo lugar: implementar todos los parámetros opcionales independientes en una interfaz, que es el tipo de retorno del parámetro final.
En tercer lugar: para cualquier subgrupo de parámetros opcionales complicados, cree más interfaces que obliguen a una serie de rutas a bajar.
interface FirstName {
public LastName setFirstName(String name)
}
interface LastName {
public OptionalInterface setLastName(String name)
}
interface OptionalParams {
public OptionalParams setOptionalParam(String numberOfBananas)
public OptionalParams setOptionalParam(int numberOfApples)
public AlcoholLevel setAlcoholLevel() // go down to a new interface
public MaritalStatus setMaritalStatus()
public Person build()
}
interface AlcoholLevel {
//mutually exclusive options hoist you back to options
public OptionalParams setNumberOfBeers(int bottles)
public OptionalParams setBottlesOfWine(int wine)
public OptionalParams setShots(int shots)
public OptionalParams isHammered()
public OptionalParams isTeeTotal()
public OptionalParams isTipsy()
}
interface MaritalStatus {
public OptionalParams isUnmarried()
//force you into a required params loop
public HusbandFirstName hasHusband()
public WifeFirstName hasWife()
}
Al tener una serie de interfaces de un método, puede forzar en gran medida un buen comportamiento en el cliente. Este patrón funciona bien, por ejemplo, formando solicitudes HTTP bien formadas en redes donde se requieren ciertas autenticaciones. Una superposición de interfaces en la parte superior de una biblioteca httml estándar dirige a los clientes en la dirección correcta.
Alguna lógica condicional es básicamente demasiado difícil de valer. Cosas como insistir en que la suma del parámetro 1 y el parámetro 2 son menores que el parámetro 3 se manejan mejor lanzando una excepción de tiempo de ejecución en el método de compilación.
En lugar de su patrón de constructor, sugiero crear un nuevo constructor que introduzca diferentes objetos de parámetros, que agrupen diferentes parámetros. Luego puede delegar en el constructor original desde dentro de ese nuevo constructor. Marque también el constructor original como obsoleto y apunte al nuevo.
La refactorización del constructor con objetos de parámetros también se puede realizar con el soporte de IDE y, por lo tanto, no es mucho trabajo. De esta manera usted también podría refactorizar el código existente. Aún puede crear constructores para los objetos de parámetros y la clase en cuestión si aún son necesarios.
El problema en el que debe enfocarse es que los diferentes parámetros dependen unos de otros. Y tales dependencias deben reflejarse en objetos propios.
El problema con el constructor encadenado es que necesita demasiadas clases y que no puede cambiar el orden en que las va a usar, aunque ese orden aún sea correcto.
Por lo tanto, el generador interno estático combinado con una función de fábrica puede hacer algo de lo que desea. (1) Puede imponer dependencias del tipo si se establece A, también B. (2) Puede devolver clases diferentes. (3) Se puede hacer comprobación lógica en las entradas.
Sin embargo, seguirá fallando si los programadores ingresan los campos incorrectos.
Una posible ventaja es el patrón de "múltiples constructores". Si el cliente sabe de antemano el propósito de por qué está construyendo un elemento en particular, entonces puede obtener un constructor diferente. Puedes hacer un constructor para cada combinación.
Dependiendo del tipo de dependencias lógicas en su clase, puede combinar estos múltiples constructores con un generador general. Puede tener, digamos, un constructor general, y cuando llama a setOption (A) en el constructor general, devuelve una clase diferente de constructor, a la que solo puede encadenar los métodos que continúan siendo relevantes. Así obtendrá fluidez, pero Puedes excluir algunos caminos. Al hacer esto, debe tener cuidado de anular los campos que se establecieron, pero se han vuelto irrelevantes; no puede hacer que las constructoras sean subclases entre sí.
Esto puede forzar al cliente a elegir en tiempo de compilación cómo construir el objeto, ¿es eso lo que está buscando?
ACTUALIZACIÓN - Intenté responder a los comentarios:
Entonces, lo primero es lo primero: una función de fábrica es el elemento uno en Joshua Blocks Effective Java, solo significa que para un objeto usted hace que el constructor sea privado, y en su lugar hace una función de fábrica estática. Esto es más flexible que un constructor, porque puede devolver diferentes tipos de objetos. Cuando combina una función de fábrica con varios constructores, puede obtener una combinación muy poderosa. Aquí está la descripción del patrón: http://en.wikipedia.org/wiki/Factory_method_pattern http://en.wikipedia.org/wiki/Abstract_factory_pattern
Entonces imagine que desea crear objetos que describan a una persona y su trabajo, pero cuando especifica su trabajo, desea tener una lista de subopciones específicas.
public class outer{
Map <Map<Class<?>, Object> jobsMap - holds a class object of the new builder class, and a prototypical isntance which can be cloned.
outer(){
jobsMap.add(Mechanic.class, new Mechanic());
//add other classes to the map
}
public class GeneralBuilder{
String name;
int age;
//constructor enforces mandatory parameters.
GeneralBuilder(String name, int age, //other mandatory paramerters){
//set params
}
public T setJob(Class<T extends AbstractJob> job){
AbstractJob result = super.jobsMap.getValue(job).clone();
//set General Builder parameters name, age, etc
return (T) result;
}
}
public MechanicBuilder extends AbstractJobs{
//specific properties
MechanicBuilder(GeneralBuilder){
// set age, name, other General Builder properties
}
//setters for specific properties return this
public Person build(){
//check mechanic mandatory parameters filled, else throw exception.
return Person.factory(this);
}
}
public abstract class AbstractJob{
String name;
String age;
//setters;
}
public class Person {
//a big class with lots of options
//private constructor
public static Person Factory(Mechanic mechanic){
//set relevant person options
}
}
Así que esto ahora es fluido. Creo una instancia de external y lleno el mapa con todos los tipos de trabajos específicos. Entonces puedo crear a partir de esto tantos constructores nuevos como quiera como instancias de la clase interna. Establecí los parámetros para el constructor general que llama a .setJobs (Mechanic.class) y devuelve una instancia de mecánica que tiene un montón de propiedades específicas, a las que ahora puedo llamar con fluidez usando .setOptionA (), etc. Finalmente, llamo a compilar, y esto llama al método de fábrica estático en la clase Persona y se pasa a sí mismo. Recibes una clase de persona.
Es mucha implementación, ya que tiene que crear una clase de constructor específica para cada "tipo" de objeto que podría estar representado por la clase Persona, pero hace que una API sea muy fácil de usar para el cliente. En la práctica, aunque estas clases tienen muchas opciones, en la práctica puede haber solo un puñado de patrones que las personas intentan crear, y el resto solo aparece por accidente.
un constructor interno estático, hecho famoso por josh bloch en java efectivo.
Los parámetros requeridos son argumentos del constructor, los opcionales son métodos.
Un ejemplo. La invocación donde solo se requiere el nombre de usuario:
RegisterUserDto myDto = RegisterUserDto.Builder(myUsername).password(mypassword).email(myemail).Build();
Y el código subyacente (omitiendo las vars de instancia obvias):
private RegisterUserDTO(final Builder builder) {
super();
this.username = builder.username;
this.firstName = builder.firstName;
this.surname = builder.surname;
this.password = builder.password;
this.confirmPassword = builder.confirmPassword;
}
public static class Builder {
private final String username;
private String firstName;
private String surname;
private String password;
private String confirmPassword;
public Builder(final String username) {
super();
this.username = username;
}
public Builder firstname(final String firstName) {
this.firstName = firstName;
return this;
}
public Builder surname(final String surname) {
this.surname = surname;
return this;
}
public Builder password(final String password) {
this.password = password;
return this;
}
public Builder confirmPassword(final String confirmPassword) {
this.confirmPassword = confirmPassword;
return this;
}
public RegisterUserDTO build() {
return new RegisterUserDTO(this);
}
}