¿Por qué el encadenamiento de setters no es convencional?

46

Tener encadenado implementado en beans es muy útil: no hay necesidad de sobrecargar a los constructores, megaconstructores, fábricas y le brinda una mayor legibilidad. No puedo pensar en ningún inconveniente, a menos que desee que su objeto sea inmutable , en cuyo caso no tendría ningún setter de todos modos. Entonces, ¿hay alguna razón por la cual esta no es una convención OOP?

public class DTO {

    private String foo;
    private String bar;

    public String getFoo() {
         return foo;
    }

    public String getBar() {
        return bar;
    }

    public DTO setFoo(String foo) {
        this.foo = foo;
        return this;
    }

    public DTO setBar(String bar) {
        this.bar = bar;
        return this;
    }

}

//...//

DTO dto = new DTO().setFoo("foo").setBar("bar");
Ben
fuente
32
Porque Java puede ser el único lenguaje donde los setters no son una abominación para el hombre ...
Telastyn 02 de
11
Es un mal estilo porque el valor de retorno no es semánticamente significativo. Es engañoso La única ventaja es ahorrar muy pocas pulsaciones de teclas.
Usr
58
Este patrón no es raro en absoluto. Incluso hay un nombre para eso. Se llama interfaz fluida .
Philipp
99
También puede abstraer esta creación en un generador para obtener un resultado más legible. myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();Lo haría para no entrar en conflicto con la idea general de que los colocadores son vacíos.
99
@Philipp, aunque técnicamente tienes razón, no diría que se new Foo().setBar('bar').setBaz('baz')siente muy "fluido". Quiero decir, seguro que podría implementarse exactamente de la misma manera, pero esperaba leer algo más comoFoo().barsThe('bar').withThe('baz').andQuuxes('the quux')
Wayne Werner,

Respuestas:

50

Entonces, ¿hay alguna razón por la cual no se trata de una convención OOP?

Mi mejor suposición: porque viola CQS

Tienes un comando (que cambia el estado del objeto) y una consulta (que devuelve una copia de estado, en este caso, el objeto mismo) mezclado en el mismo método. Eso no es necesariamente un problema, pero viola algunas de las pautas básicas.

Por ejemplo, en C ++, std :: stack :: pop () es un comando que devuelve void, y std :: stack :: top () es una consulta que devuelve una referencia al elemento superior en la pila. Clásicamente, te gustaría combinar los dos, pero no puedes hacer eso y estar a salvo de excepciones. (No es un problema en Java, porque el operador de asignación en Java no arroja).

Si DTO fuera un tipo de valor, podría lograr un final similar con

public DTO setFoo(String foo) {
    return new DTO(foo, this.bar);
}

public DTO setBar(String bar) {
    return new DTO(this.foo, bar);
}

Además, encadenar los valores de retorno es un dolor colosal cuando se trata de herencia. Consulte el "Patrón de plantilla curiosamente recurrente"

Finalmente, está el problema de que el constructor predeterminado debería dejarte con un objeto que esté en un estado válido. Si debe ejecutar un montón de comandos para restaurar el objeto a un estado válido, algo ha salido muy mal.

VoiceOfUnreason
fuente
44
Finalmente, una verdadera catain aquí. La herencia es el verdadero problema. Creo que el encadenamiento podría ser un setter convencional para objetos de representación de datos que se usan en la composición.
Ben
66
"devolver una copia de estado de un objeto", no hace eso.
user253751
2
Yo diría que la razón principal para seguir esas pautas (con algunas excepciones notables como iteradores) es que hace que el código sea mucho más fácil de razonar.
jpmc26
1
@MatthieuM. no. Principalmente hablo Java, y prevalece allí en las API "fluidas" avanzadas. Estoy seguro de que también existe en otros idiomas. Esencialmente, hace que su tipo sea genérico en un tipo que se extiende a sí mismo. Luego agrega un abstract T getSelf()método con retorna el tipo genérico. Ahora, en lugar de regresar thisde sus establecedores, usted return getSelf()y cualquier clase anuladora simplemente hacen que el tipo sea genérico en sí mismo y regresa thisde getSelf. De esta manera, los setters devuelven el tipo real y no el tipo de declaración.
Boris the Spider
1
@MatthieuM. ¿De Verdad? Suena similar a CRTP para C ++ ...
Inútil
33
  1. Guardar unas pocas teclas no es convincente. Puede ser agradable, pero las convenciones de OOP se preocupan más por conceptos y estructuras, no por pulsaciones de teclas.

  2. El valor de retorno no tiene sentido.

  3. Incluso más que no tener sentido, el valor de retorno es engañoso, ya que los usuarios pueden esperar que el valor de retorno tenga significado. Pueden esperar que sea un "setter inmutable"

    public FooHolder {
        public FooHolder withFoo(int foo) {
            /* return a modified COPY of this FooHolder instance */
        }
    }
    

    En realidad tu setter muta el objeto.

  4. No funciona bien con la herencia.

    public FooHolder {
        public FooHolder setFoo(int foo) {
            ...
        }
    }
    
    public BarHolder extends FooHolder {
        public FooHolder setBar(int bar) {
            ...
        }
    } 
    

    puedo escribir

    new BarHolder().setBar(2).setFoo(1)

    pero no

    new BarHolder().setFoo(1).setBar(2)

Para mí, # 1 a # 3 son los más importantes. El código bien escrito no se trata de texto agradablemente organizado. El código bien escrito trata sobre los conceptos fundamentales, las relaciones y la estructura. El texto es solo un reflejo externo del verdadero significado del código.

Paul Draper
fuente
2
Sin embargo, la mayoría de estos argumentos se aplican a cualquier interfaz fluida / encadenable.
Casey
@Casey, sí. Constructores (es decir, con setters) es el caso más frecuente de encadenamiento.
Paul Draper
10
Si está familiarizado con la idea de una interfaz fluida, no se aplica el n. ° 3, ya que es probable que sospeche una interfaz fluida del tipo de retorno. También tengo que estar en desacuerdo con "El código bien escrito no se trata de texto agradablemente organizado". De eso no se trata, pero el texto bien organizado es bastante importante, ya que permite que el lector humano del programa lo entienda con menos esfuerzo.
Michael Shaw
1
Setter encadenamiento no se trata de organizar el texto de forma agradable o de guardar las pulsaciones de teclas. Se trata de reducir el número de identificadores. Con el encadenamiento de setter puede crear y configurar el objeto en una sola expresión, lo que significa que es posible que no tenga que guardarlo en una variable, una variable que tendrá que nombrar , y que permanecerá allí hasta el final del alcance.
Idan Arye
3
Sobre el punto # 4, es algo que se puede resolver en Java de la siguiente manera: public class Foo<T extends Foo> {...}con el setter regresando Foo<T>. También esos métodos 'setter', prefiero llamarlos 'con' métodos. Si la sobrecarga funciona bien, a continuación, sólo Foo<T> with(Bar b) {...}, de lo contrario Foo<T> withBar(Bar b).
YoYo
12

No creo que esta sea una convención OOP, está más relacionada con el diseño del lenguaje y sus convenciones.

Parece que te gusta usar Java. Java tiene una especificación JavaBeans que especifica el tipo de retorno del setter a anular, es decir, está en conflicto con el encadenamiento de setters. Esta especificación es ampliamente aceptada e implementada en una variedad de herramientas.

Por supuesto, puede preguntar, ¿por qué no encadena parte de la especificación? No sé la respuesta, tal vez este patrón simplemente no era conocido / popular en ese momento.

qbd
fuente
Sí, JavaBeans es exactamente lo que tenía en mente.
Ben
No creo que la mayoría de las aplicaciones que usan JavaBeans se preocupen, pero podrían (usando la reflexión para tomar los métodos que se llaman 'set ...', tienen un solo parámetro y son nulos de retorno ). Muchos programas de análisis estático se quejan de no verificar un valor de retorno de un método que devuelve algo y esto podría ser muy molesto.
Las llamadas reflexivas de @MichaelT para obtener métodos en Java no especifican el tipo de retorno. Llamar a un método de esa manera devuelve Object, lo que puede dar como resultado que se devuelva el singleton de tipo Voidsi el método se especifica voidcomo su tipo de retorno. En otras noticias, aparentemente "dejar un comentario pero no votar" es motivo para reprobar una auditoría de "primera publicación", incluso si es un buen comentario. Culpo a Shog por esto.
7

Como han dicho otras personas, esto a menudo se llama una interfaz fluida .

Normalmente los establecedores son llamadas que pasan variables en respuesta al código lógico en una aplicación; Su clase DTO es un ejemplo de esto. El código convencional cuando los setters no devuelven nada es lo mejor para esto. Otras respuestas han explicado de manera.

Sin embargo, hay algunos casos en los que la interfaz fluida puede ser una buena solución, estos tienen en común.

  • Las constantes se pasan principalmente a los setters
  • La lógica del programa no cambia lo que se pasa a los setters.

Configuración de la configuración, por ejemplo , nhibernate fluido

Id(x => x.Id);
Map(x => x.Name)
   .Length(16)
   .Not.Nullable();
HasMany(x => x.Staff)
   .Inverse()
   .Cascade.All();
HasManyToMany(x => x.Products)
   .Cascade.All()
   .Table("StoreProduct");

Configuración de datos de prueba en pruebas unitarias, usando TestDataBulderClasses (Object Mothers) especiales

members = MemberBuilder.CreateList(4)
    .TheFirst(1).With(b => b.WithFirstName("Rob"))
    .TheNext(2).With(b => b.WithFirstName("Poya"))
    .TheNext(1).With(b => b.WithFirstName("Matt"))
    .BuildList(); // Note the "build" method sets everything else to
                  // senible default values so a test only need to define 
                  // what it care about, even if for example a member 
                  // MUST have MembershipId  set

Sin embargo, crear una buena interfaz fluida es muy difícil , por lo que solo vale la pena cuando tienes mucha configuración "estática". Además, la interfaz fluida no debe mezclarse con clases "normales"; por lo tanto, el patrón de construcción se usa a menudo.

Ian
fuente
5

Creo que gran parte de la razón por la que no es una convención encadenar un setter tras otro es porque para esos casos es más típico ver un objeto o parámetros de opciones en un constructor. C # también tiene una sintaxis de inicializador.

En lugar de:

DTO dto = new DTO().setFoo("foo").setBar("bar");

Se podría escribir:

(en JS)

var dto = new DTO({foo: "foo", bar: "bar"});

(Cía#)

DTO dto = new DTO{Foo = "foo", Bar = "bar"};

(en Java)

DTO dto = new DTO("foo", "bar");

setFooy setBarya no son necesarios para la inicialización, y se pueden usar para la mutación más adelante.

Si bien la capacidad de encadenamiento es útil en algunas circunstancias, es importante no intentar rellenar todo en una sola línea solo por reducir los caracteres de nueva línea.

Por ejemplo

dto.setFoo("foo").setBar("fizz").setFizz("bar").setBuzz("buzz");

hace que sea más difícil leer y comprender lo que está sucediendo. Reformateando a:

dto.setFoo("foo")
    .setBar("fizz")
    .setFizz("bar")
    .setBuzz("buzz");

Es mucho más fácil de entender y hace que el "error" en la primera versión sea más obvio. Una vez que haya refactorizado el código a ese formato, no hay una ventaja real sobre:

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");
zzzzBov
fuente
3
1. Realmente no puedo estar de acuerdo con el problema de legibilidad, agregar una instancia antes de cada llamada no lo aclara en absoluto. 2. Los patrones de inicialización que mostraste con js y C # no tienen nada que ver con los constructores: lo que hiciste en js pasa un solo argumento, y lo que hiciste en C # es una sintaxis shugar que llama geters y setters detrás de escena, y java no tiene el shugar geter-setter como C #.
Ben
1
@Benedictus, estaba mostrando varias formas diferentes en que los lenguajes OOP manejan este problema sin encadenarse. El punto no era proporcionar un código idéntico, el punto era mostrar alternativas que hicieran innecesario el encadenamiento.
zzzzBov
2
"agregar instancia antes de cada llamada no lo hace más claro" Nunca dije que agregar la instancia antes de cada llamada aclarara algo, solo dije que era relativamente equivalente.
zzzzBov
1
VB.NET también tiene una palabra clave "Con" utilizable que crea una referencia temporal del compilador de modo que, por ejemplo, [usar /para representar salto de línea] With Foo(1234) / .x = 23 / .y = 47sería equivalente a Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47. Dicha sintaxis no crea ambigüedad, ya que .xpor sí misma no puede tener otro significado que el de unirse a la declaración "Con" inmediatamente circundante [si no hay ninguno o si el objeto no tiene miembro x, entonces no .xtiene sentido]. Oracle no ha incluido algo así en Java, pero tal construcción encajaría sin problemas en el lenguaje.
supercat
5

Esa técnica se usa en realidad en el patrón Builder.

x = ObjectBuilder()
        .foo(5)
        .bar(6);

Sin embargo, en general se evita porque es ambiguo. No es obvio si el valor de retorno es el objeto (por lo que puede llamar a otros establecedores), o si el objeto de retorno es el valor que se acaba de asignar (también un patrón común). En consecuencia, el Principio de Menos Sorpresa sugiere que no debe tratar de asumir que el usuario quiere ver una solución u otra, a menos que sea fundamental para el diseño del objeto.

Cort Ammon
fuente
66
La diferencia es que, cuando se usa el patrón de construcción, generalmente está escribiendo un objeto de solo escritura (el Constructor) que eventualmente construye un objeto de solo lectura (inmutable) (cualquier clase que esté construyendo). En ese sentido, es deseable tener una larga cadena de llamadas a métodos porque puede usarse como una sola expresión.
Darkhogg
"o si el objeto devuelto es el valor que se acaba de asignar" Odio eso, si quiero mantener el valor que acabo de pasar lo pondré primero en una variable. Sin embargo, puede ser útil obtener el valor que se asignó previamente (creo que algunas interfaces de contenedor lo hacen).
JAB
@JAB Estoy de acuerdo, no soy tan fanático de esa notación, pero tiene sus lugares. Lo que me viene a la mente es Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); que creo que también ganó popularidad porque refleja a = b = c = d, aunque no estoy convencido de que la popularidad esté bien fundada. He visto el que mencionas, devolviendo el valor anterior, en algunas bibliotecas de operaciones atómicas. ¿Moraleja de la historia? Es aún más confuso de lo que lo hice sonar =)
Cort Ammon
Creo que los académicos se vuelven un poco pedantes: no hay nada intrínsecamente malo en el idioma. Es tremendamente útil para agregar claridad en ciertos casos. Tome la siguiente clase de formato de texto, por ejemplo, que sangra cada línea de una cadena y dibuja un cuadro de arte ascii alrededor de ella new BetterText(string).indent(4).box().print();. En ese caso, pasé por alto un montón de gobbledygook y tomé una cadena, la sangré, la recuadré y la saqué. Es posible que incluso desee un método de duplicación dentro de la cascada (como, por ejemplo, .copy()) para permitir que todas las acciones siguientes ya no modifiquen el original.
tgm1024
2

Esto es más un comentario que una respuesta, pero no puedo comentar, así que ...

Solo quería mencionar que esta pregunta me sorprendió porque no veo esto como algo poco común . En realidad, en mi entorno de trabajo (desarrollador web) es muy muy común.

Por ejemplo, así es como la doctrina de Symfony: genera: el comando de entidades genera automáticamente todo setters , de forma predeterminada .

jQuery Kinda cadenas de la mayoría de sus métodos de una manera muy similar.

xDaizu
fuente
Js es una abominación
Ben
@Benedictus Yo diría que PHP es la abominación más grande. JavaScript es un lenguaje perfectamente bueno y se ha vuelto bastante agradable con la inclusión de las funciones de ES6 (aunque estoy seguro de que algunas personas todavía prefieren CoffeeScript o variantes; personalmente, no soy fanático de cómo CoffeeScript maneja el alcance variable mientras verifica el alcance externo primero, en lugar de tratar las variables asignadas en el ámbito local como locales, a menos que se indique explícitamente como no local / global de la forma en que lo hace Python).
JAB
@Benedictus con el que estoy de acuerdo JAB. En mis años de universidad solía mirar hacia abajo en JS como una abominación lenguaje de script aspirante a, pero después de trabajar un par de años con él, y el aprendizaje de la DERECHA forma de usarlo, me he dado ... en realidad el amor que . Y creo que con ES6 se convertirá en un lenguaje realmente maduro . Pero, lo que me hace girar la cabeza es ... ¿qué tiene que ver mi respuesta con JS en primer lugar? ^^ U
xDaizu
De hecho, trabajé un par de años con js y puedo decir que aprendí de varias maneras. Sin embargo, veo este lenguaje como nada más que un error. Pero estoy de acuerdo, Php es mucho peor.
Ben
@Benedictus Entiendo tu posición y la respeto. Sin embargo, todavía me gusta. Claro, tiene sus peculiaridades y ventajas (¿qué idioma no tiene?), Pero avanza constantemente en la dirección correcta. Pero ... en realidad no me gusta tanto que necesito defenderlo. No obtengo nada de la gente que lo ama o lo odia ... jajaja Además, este no es lugar para eso, mi respuesta ni siquiera fue sobre JS.
xDaizu