¿Por qué los encadenadores no son convencionales?

46

Tener el encadenamiento implementado en beans es muy útil: no es necesario sobrecargar los constructores, los mega constructores, las fábricas, y le proporciona una mayor legibilidad. No puedo pensar en ninguna desventaja, a menos que quieras que tu objeto sea inmutable , en cuyo caso no tendría ningún configurador de todos modos. Entonces, ¿hay alguna razón por la que esto no sea 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");
    
pregunta Ben 02.02.2016 - 18:06

7 respuestas

48
  

Entonces, ¿hay alguna razón por la que no sea esta una convención OOP?

Mi mejor estimación: porque viola CQS

Tienes un comando (que cambia el estado del objeto) y una consulta (que devuelve una copia del estado, en este caso, el objeto en sí) 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, le gustaría combinar los dos, pero no puede hacer que y estén a salvo de excepciones. (No es un problema en Java, porque el operador de asignación en Java no lanza).

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, los valores de retorno de encadenamiento son un dolor colosal cuando se trata de la herencia. Consulte el "Patrón de plantilla curiosamente recurrente"

Finalmente, existe el problema de que el constructor predeterminado debería dejarte con un objeto que se encuentra en un estado válido. Si debe ejecutar una serie de comandos para restaurar el objeto a un estado válido, algo ha ido muy mal.

    
respondido por el VoiceOfUnreason 02.02.2016 - 18:51
32
  1. Guardar algunas pulsaciones de teclas no es convincente. Puede ser bueno, pero las convenciones OOP se preocupan más por los conceptos y las estructuras, no por las pulsaciones de teclas.

  2. El valor de retorno no tiene sentido.

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

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

    En realidad, tu colocador muta el objeto.

  4. No funciona bien con 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í, los números 1 a 3 son los más importantes. El código bien escrito no se trata de un texto bien arreglado. El código bien escrito trata sobre los conceptos fundamentales, las relaciones y la estructura. El texto es solo un reflejo del significado verdadero del código.

    
respondido por el Paul Draper 02.02.2016 - 21:37
12

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

Parece que te gusta usar Java. Java tiene una especificación de JavaBeans que especifica el tipo de retorno del configurador que se 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 que puede preguntar, ¿por qué no está encadenando parte de la especificación? No sé la respuesta, tal vez este patrón no era conocido / popular en ese momento.

    
respondido por el qbd 02.02.2016 - 18:19
7

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

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

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

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

Configuración de la configuración, por ejemplo, fluent-nhibernate

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, utilizando TestDataBulderClasses (Object Mothers)

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 las clases "normales"; por lo tanto, a menudo se usa el patrón de construcción.

    
respondido por el Ian 03.02.2016 - 11:32
5

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

En lugar de:

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

Uno podría escribir:

(en JS)

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

(en C #)

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

(en Java)

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

setFoo y setBar ya 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 tratar de rellenar todo en una sola línea solo para 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 entender lo que está sucediendo. Reformateo 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 ninguna ventaja real sobre:

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");
    
respondido por el zzzzBov 02.02.2016 - 18:40
5

Esa técnica se usa realmente en el patrón de Generador.

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 configuradores), 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 intentar asumir que el usuario desea ver una solución u otra, a menos que sea fundamental para el diseño del objeto.

    
respondido por el Cort Ammon 03.02.2016 - 04:46
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 poco común en absoluto. En realidad, en mi entorno de trabajo (desarrollador web) es muy común.

Por ejemplo, así es como la doctrina de Symfony: generar: el comando de las entidades genera automáticamente todos setters , por defecto .

jQuery kinda encadena la mayoría de sus métodos de una manera muy similar.

    
respondido por el xDaizu 04.02.2016 - 11:27

Lea otras preguntas en las etiquetas

Comentarios Recientes

Cuando vemos que agregan valor y suministran energía, tendemos a copiarlos. El encadenamiento nos permite secuenciar grandes variaciones en el juego y calcular los resultados del juego orgánicamente a través de contadores naturales. Esto enfatiza la ubicación y los enfrentamientos. Al carecer de una utilidad, puede estar en una derrota situacional cuando intenta ganar después de no tener esa eliminación o esos contadores todavía. mezcla de ambos Aunque algunos ejecutan el golpe de derecha con más frecuencia... Lee mas