¿Por qué se uniría un tipo con su constructor?

20

Recientemente eliminé una respuesta mía de en Code Review , que comenzó así:

private Person(PersonBuilder builder) {

Detener. Bandera roja. Un PersonBuilder construiría una Persona; sabe sobre una persona. La clase Person no debería saber nada sobre un PersonBuilder, es solo un tipo inmutable. Ha creado un acoplamiento circular aquí, donde A depende de B, que depende de A.

La persona solo debe tomar sus parámetros; un cliente que esté dispuesto a crear una Persona sin construirlo debería poder hacerlo.

Fui abofeteado con un voto negativo y me dijeron que (citando) Bandera roja, ¿por qué? La implementación aquí tiene la misma forma que Joshua Bloch demostró en su libro "Effective Java" (elemento # 2).

Entonces, parece que la forma correcta de implementar un patrón de generador en Java es hacer que el generador sea un tipo anidado (esto no es de lo que se trata esta pregunta), y luego hacer el producto (la clase del objeto que se está construyendo ) depende del constructor , así:

private StreetMap(Builder builder) {
    // Required parameters
    origin      = builder.origin;
    destination = builder.destination;

    // Optional parameters
    waterColor         = builder.waterColor;
    landColor          = builder.landColor;
    highTrafficColor   = builder.highTrafficColor;
    mediumTrafficColor = builder.mediumTrafficColor;
    lowTrafficColor    = builder.lowTrafficColor;
}

https://en.wikipedia.org/wiki/Builder_pattern#Java_example

La misma página de Wikipedia para el mismo patrón de Builder tiene una implementación muy diferente (y mucho más flexible) para C #:

//Represents a product created by the builder
public class Car
{
    public Car()
    {
    }

    public int Wheels { get; set; }

    public string Colour { get; set; }
}

Como puede ver, el producto aquí no sabe nada acerca de una Builderclase, y por lo que le importa, podría ser instanciado por una llamada de constructor directo, una fábrica abstracta, ... o un constructor, hasta donde yo entiendo, el producto de un patrón de creación nunca necesita saber nada sobre lo que lo está creando.

Se me ha servido el contraargumento (que aparentemente se defiende explícitamente en el libro de Bloch) de que un patrón de construcción podría usarse para reelaborar un tipo que tendría un constructor hinchado con docenas de argumentos opcionales. Entonces, en lugar de apegarme a lo que creía saber, investigué un poco en este sitio y descubrí que, como sospechaba, este argumento no es válido .

Entonces, ¿cuál es el trato? ¿Por qué idear soluciones sobredimensionadas para un problema que ni siquiera debería existir en primer lugar? Si sacamos a Joshua Bloch de su pedestal por un minuto, ¿podemos encontrar una sola razón buena y válida para acoplar dos tipos concretos y llamarlo una mejor práctica?

Todo esto apesta a programación de culto de carga para mí.

Mathieu Guindon
fuente
12
Y esta "pregunta" apesta a ser simplemente una queja ...
Philip Kendall
2
@PhilipKendall tal vez un poco. Pero estoy realmente interesado en comprender por qué cada implementación de Java Builder tiene ese acoplamiento estrecho.
Mathieu Guindon
19
@PhilipKendall Me apesta a curiosidad.
Mástil
8
@PhilipKendall Se lee como una diatriba, sí, pero en el fondo hay una pregunta válida sobre el tema. Tal vez la queja podría ser editado?
Andres F.
No estoy impresionado por el argumento de "demasiados argumentos de constructor". Pero estoy aún menos impresionado por estos dos ejemplos de constructores. Tal vez sea porque los ejemplos son demasiado simples para demostrar un comportamiento no trivial, pero el ejemplo de Java se lee como una interfaz fluida enrevesada , y el ejemplo de C # es solo una forma indirecta de implementar propiedades. Ninguno de los ejemplos tiene ninguna forma de validación de parámetros; un constructor al menos validaría el tipo y la cantidad de argumentos proporcionados, lo que significa que el constructor con demasiados argumentos gana.
Robert Harvey

Respuestas:

22

No estoy de acuerdo con su afirmación de que es una bandera roja. También estoy en desacuerdo con su representación del contrapunto, que hay una forma correcta de representar un patrón de generador.

Para mí hay una pregunta: ¿Es el generador una parte necesaria de la API del tipo? Aquí, ¿el PersonBuilder es una parte necesaria de la API de Person?

Es completamente razonable que una clase de Persona con comprobación de validez, inmutable y / o encapsulada estrechamente se cree necesariamente a través de un Generador que proporciona (independientemente de si ese generador está anidado o adyacente). Al hacerlo, la Persona puede mantener todos sus campos privados o paquete privado y final, y también dejar la clase abierta para modificación si es un trabajo en progreso. (Puede terminar cerrado por modificación y abierto por extensión, pero eso no es importante en este momento, y es especialmente controvertido para los objetos de datos inmutables).

Si se da el caso de que una Persona se crea necesariamente a través de un PersonBuilder suministrado como parte de un diseño de API de nivel de paquete único, entonces un acoplamiento circular está bien y una bandera roja no está garantizada aquí. En particular, lo declaró como un hecho incontrovertible y no como una opinión o elección de diseño de API, por lo que puede haber contribuido a una mala respuesta. No te habría rechazado, pero no culpo a nadie más por hacerlo, y dejaré el resto de la discusión sobre "lo que justifica un voto negativo" al centro de ayuda y meta de la Revisión de Código . Los votos negativos suceden; No es una "bofetada".

Por supuesto, si desea dejar muchas formas abiertas para construir un objeto Person, entonces PersonBuilder podría convertirse en un consumidor o utilidadde la clase Persona, de la cual la Persona no debería depender directamente. Esta opción parece más flexible, ¿quién no querría más opciones de creación de objetos? . Si el Constructor está destinado a representar o reemplazar un constructor con muchos campos opcionales o un constructor abierto a modificación, entonces el constructor largo de la clase puede ser un detalle de implementación que mejor se deje oculto. (También tenga en cuenta que un generador oficial y necesario no le impide escribir su propia utilidad de construcción, solo que su utilidad de construcción puede consumir el generador como una API como en mi pregunta original anterior).


pd: Observo que la muestra de revisión de código que enumeró tiene una Persona inmutable, pero el contraejemplo de Wikipedia enumera un Coche mutable con captadores y establecedores. Puede ser más fácil ver la maquinaria necesaria omitida del automóvil si mantiene el lenguaje y los invariantes consistentes entre ellos.

Jeff Bowman apoya a Monica
fuente
Creo que veo su punto sobre tratar al constructor como un detalle de implementación. Simplemente no parece que Java Efectivo transmita este mensaje correctamente (no he leído / no he leído ese libro), dejando un montón de desarrolladores Java junior (?) Suponiendo "así es como se hace" independientemente del sentido común . Estoy de acuerdo en que los ejemplos de Wikipedia son malos para la comparación ... pero bueno, es Wikipedia ... y FWIW En realidad no me importa el voto negativo; la respuesta eliminada está sentada con una puntuación neta positiva de todos modos (y existe la posibilidad de que finalmente obtenga una [insignia: presión de grupo]).
Mathieu Guindon
1
Sin embargo, parece que no puedo librarme de la idea de que un tipo con demasiados argumentos de constructor es un problema de diseño (huele a ruptura de SRP), que un patrón de constructor empuja rápidamente debajo de una alfombra.
Mathieu Guindon
3
@ Mat'sMug Mi publicación anterior da por sentado que la clase necesita tantos argumentos de constructor . También lo trataría como un olor a diseño si estuviéramos hablando de un componente de lógica empresarial arbitrario, pero para un objeto de datos, no es cuestión de que un tipo tenga diez o veinte atributos directos. Por ejemplo , no es una violación de SRP que un objeto represente un único registro fuertemente tipado en un registro .
Jeff Bowman apoya a Monica
1
Lo suficientemente justo. Sin embargo, todavía no entiendo el String(StringBuilder)constructor, que parece obedecer a ese "principio" en el que es normal que un tipo dependa de su constructor. Me quedé estupefacto cuando vi que el constructor se sobrecargaba. No es como si Stringtuviera 42 parámetros de constructor ...
Mathieu Guindon
3
@Luaan Odd. Literalmente nunca lo he visto usado y no sabía que existía; toString()Es universal.
chrylis -on strike-
7

Entiendo lo molesto que puede ser cuando se usa 'apelación a la autoridad' en un debate. Los argumentos deben basarse en su propia OMI y, aunque no está mal señalar el consejo de una persona tan respetada, realmente no puede considerarse un argumento completo en sí mismo, ya que sabemos que el Sol viaja alrededor de la Tierra porque Aristóteles lo dijo. .

Dicho esto, creo que la premisa de su argumento es la misma. Nunca debemos acoplar dos tipos concretos y llamarlo una mejor práctica porque ... ¿Alguien lo dijo?

Para que el argumento de que el acoplamiento sea problemático sea válido, debe haber problemas específicos que cree. Si este enfoque no está sujeto a esos problemas, entonces no puede argumentar lógicamente que esta forma de acoplamiento es problemática. Entonces, si desea expresar su punto aquí, creo que debe mostrar cómo este enfoque está sujeto a esos problemas y / o cómo el desacoplamiento del constructor mejorará un diseño.

Por casualidad, me cuesta ver cómo el enfoque combinado creará problemas. Las clases están completamente acopladas, sin duda, pero el constructor existe solo para crear instancias de la clase. Otra forma de verlo sería como una extensión del constructor.

JimmyJames
fuente
Entonces ... ¿mayor acoplamiento, menor cohesión => final feliz? hmm ... veo el punto sobre el constructor "extendiendo el constructor", pero para mencionar otro ejemplo, no veo qué Stringestá haciendo con una sobrecarga del constructor tomando un StringBuilderparámetro (aunque es discutible si a StringBuilderes un constructor , pero eso es otra historia).
Mathieu Guindon
44
@ Mat'sMug Para ser claros, realmente no tengo un sentimiento fuerte sobre esto de ninguna manera. No tiendo a usar el patrón de construcción porque realmente no creo clases que tengan mucha entrada de estado. En general, descompondría esa clase por razones a las que alude en otro lugar. Todo lo que digo es que si puede mostrar cómo este enfoque conduce a un problema, no importará si Dios bajó y le entregó ese patrón de construcción a Moisés en una tableta de piedra. Tú ganas'. Por otro lado si no puedes ...
JimmyJames
Un uso legítimo del patrón de construcción que tengo en mi propia base de código es para burlarse de la API VBIDE; básicamente, proporciona el contenido de la cadena de un módulo de código, y el generador le proporciona un VBEsimulacro, completo con ventanas, panel de código activo, un proyecto y un componente con un módulo que contiene la cadena que proporcionó. Sin él, no sé cómo podría escribir el 80% de las pruebas en Rubberduck , al menos sin apuñalarme en el ojo cada vez que las miro.
Mathieu Guindon
@ Mat'sMug Puede ser realmente útil para desarmar datos en objetos inmutables. Este tipo de objetos tienden a ser bolsas de propiedades, es decir, no realmente OO. Todo ese espacio de problemas es tal PITA que cualquier cosa que ayude a hacerlo se utilizará si siguen buenas prácticas o no.
JimmyJames
4

Para responder por qué un tipo se combinaría con un generador, es necesario entender por qué alguien usaría un generador en primer lugar. Específicamente: Bloch recomienda usar un generador cuando tenga una gran cantidad de parámetros de constructor. Esa no es la única razón para usar un constructor, pero supongo que es la más común. En resumen, el constructor es un reemplazo para esos parámetros de constructor y, a menudo, el constructor se declara en la misma clase que está construyendo. Por lo tanto, la clase que encierra ya tiene conocimiento del constructor y pasarlo como argumento de constructor no cambia eso. Es por eso que un tipo se combinaría con su constructor.

¿Por qué tener una gran cantidad de parámetros implica que debería usar un constructor? Bueno, tener una gran cantidad de parámetros no es infrecuente en el mundo real, ni viola el principio de responsabilidad única. También apesta cuando tienes diez parámetros de cadena y estás tratando de recordar cuáles son cuáles y si es la quinta o la sexta cadena lo que debería ser nulo. Un constructor facilita la administración de los parámetros y también puede hacer otras cosas interesantes sobre las que debería leer el libro.

¿Cuándo utiliza un generador que no está asociado con su tipo? En general, cuando los componentes de un objeto no están disponibles de inmediato y los objetos que tienen esas piezas no necesitan saber sobre el tipo que está tratando de construir o si está agregando un generador para una clase sobre la que no tiene control .

Viejo gordo ned
fuente
Curiosamente, las únicas veces que necesitaba un generador era cuando tenía un tipo que no tiene una forma definitiva, como este MockVbeBuilder , que crea una simulación de la API de un IDE; una prueba podría necesitar un módulo de código simple, otra prueba podría necesitar dos formularios y un módulo de clase: IMO, para eso es el patrón de construcción de GoF. Dicho esto, podría comenzar a usarlo para otra clase con un constructor loco ... pero el acoplamiento simplemente no se siente del todo bien.
Mathieu Guindon
1
@ Mat's Mug La clase que ha presentado parece más representativa del patrón de diseño del generador (de Design Patterns de Gamma, et al.), No necesariamente una clase de generador simple. Ambos construyen cosas, pero no son lo mismo. Además, nunca "necesita" un constructor, simplemente facilita otras cosas.
Old Fat Ned
1

El producto de un patrón de creación nunca necesita saber nada sobre lo que lo está creando.

La Personclase no sabe qué lo está creando. Solo tiene un constructor con un parámetro, ¿y qué? El nombre del tipo de parámetro termina con ... Generador que realmente no fuerza nada. Es solo un nombre después de todo.

El constructor es un objeto de configuración glorificado 1 . Y un objeto de configuración es realmente solo agrupar propiedades que se pertenecen entre sí. Si solo se pertenecen el uno al otro con el propósito de pasar al constructor de una clase, ¡que así sea! Ambos originy destinationdel StreetMapejemplo son de tipo Point. ¿Sería posible pasar las coordenadas individuales de cada punto al mapa? Claro, pero las propiedades de una Pointpertenecen juntas, además puedes tener todo tipo de métodos que ayudan con su construcción:

1 La diferencia es que el constructor no es solo un objeto de datos simple, sino que permite estas llamadas de setter encadenadas. El Builderse preocupa principalmente por cómo construirse.

Ahora agreguemos un poco de patrón de comando a la mezcla, porque si observa de cerca, el generador es en realidad un comando:

  1. Encapsula una acción: llamar a un método de otra clase
  2. Contiene la información necesaria para realizar esa acción: los valores para los parámetros del método

La parte especial para el constructor es que:

  1. Ese método a llamar es en realidad el constructor de una clase. Mejor llámelo ahora un patrón de creación .
  2. El valor del parámetro es el propio constructor.

Lo que se pasa al Personconstructor puede construirlo, pero no necesariamente tiene que hacerlo. Puede ser un simple objeto de configuración antiguo o un generador.

nulo
fuente
0

No, están completamente equivocados y tienes toda la razón. Ese modelo de constructor es simplemente tonto. No es asunto de la persona de donde provienen los argumentos del constructor. El constructor es solo una conveniencia para los usuarios y nada más.

DeadMG
fuente
44
Gracias ... estoy seguro de que esta respuesta recabaría más votos si se ampliara un poco más, parece una respuesta sin terminar =)
Mathieu Guindon
Sí, yo haría +1, un enfoque alternativo para el constructor que proporciona las mismas garantías y garantías sin el acoplamiento
Matt Freake
3
Para un contrapunto a esto, vea la respuesta aceptada , que menciona casos PersonBuilderen los que de hecho es una parte esencial de la PersonAPI. Tal como está, esta respuesta no discute su caso.
Andres F.