Constructor con toneladas de parámetros vs patrón de constructor


21

Es bien sabido que si su clase tiene un constructor con muchos parámetros, digamos más de 4, entonces probablemente sea un olor a código . Debe reconsiderar si la clase satisface SRP .

Pero, ¿qué pasa si construimos y obtenemos que depende de 10 o más parámetros, y finalmente terminamos con la configuración de todos esos parámetros a través del patrón Builder? Imagina que construyes un Personobjeto tipo con su información personal, información de trabajo, información de amigos, información de intereses, información de educación, etc. Esto ya es bueno, pero de alguna manera estableces los mismos más de 4 parámetros, ¿verdad? ¿Por qué estos dos casos no se consideran iguales?

Respuestas:


25

El patrón de construcción no resuelve el "problema" de muchos argumentos. Pero, ¿por qué son problemáticos muchos argumentos?

  • Indican que su clase podría estar haciendo demasiado . Sin embargo, hay muchos tipos que legítimamente contienen muchos miembros que no se pueden agrupar con sensatez.
  • Probar y comprender una función con muchas entradas se vuelve exponencialmente más complicado, ¡literalmente!
  • Cuando el idioma no ofrece parámetros con nombre, una llamada de función no se documenta automáticamente . Leer una llamada de función con muchos argumentos es bastante difícil porque no tiene idea de lo que se supone que debe hacer el séptimo parámetro. Ni siquiera se daría cuenta si el quinto y sexto argumento se intercambiaron accidentalmente, especialmente si está en un lenguaje de tipo dinámico o todo sucede como una cadena, o cuando el último parámetro es truepor alguna razón.

Falsificación de parámetros con nombre

El patrón de construcción aborda solo uno de estos problemas, a saber, las preocupaciones de mantenimiento de las llamadas a funciones con muchos argumentos . Entonces una llamada a función como

MyClass o = new MyClass(a, b, c, d, e, f, g);

podría convertirse

MyClass o = MyClass.builder()
  .a(a).b(b).c(c).d(d).e(e).f(f).g(g)
  .build();

Pattern El patrón Builder originalmente se pensó como un enfoque agnóstico de representación para ensamblar objetos compuestos, lo cual es una aspiración mucho mayor que los argumentos nombrados para los parámetros. En particular, el patrón de construcción no requiere una interfaz fluida.

Esto ofrece un poco de seguridad adicional, ya que explotará si invoca un método de creación que no existe, pero de lo contrario no le ofrece nada que un comentario en la llamada del constructor no tendría. Además, la creación manual de un generador requiere código, y más código siempre puede contener más errores.

En los lenguajes donde es fácil definir un nuevo tipo de valor, descubrí que es mucho mejor usar microtipado / tipos pequeños para simular argumentos con nombre. Se llama así porque los tipos son realmente pequeños, pero terminas escribiendo mucho más ;-)

MyClass o = new MyClass(
  new MyClass.A(a), new MyClass.B(b), new MyClass.C(c),
  new MyClass.D(d), new MyClass.E(e), new MyClass.F(f),
  new MyClass.G(g));

Obviamente, los nombres de los tipos A, B, C, ... deben ser nombres de auto-documentado que ilustran el significado del parámetro, a menudo el mismo nombre que le daría la variable de parámetros. En comparación con el idioma del generador de argumentos nombrados, la implementación requerida es mucho más simple y, por lo tanto, es menos probable que contenga errores. Por ejemplo (con sintaxis Java-ish):

class MyClass {
  ...
  public static class A {
    public final int value;
    public A(int a) { value = a; }
  }
  ...
}

El compilador le ayuda a garantizar que se proporcionaron todos los argumentos; con un generador, tendría que verificar manualmente los argumentos faltantes o codificar una máquina de estado en el sistema de tipo de idioma del host; ambos probablemente contengan errores.

Existe otro enfoque común para simular argumentos con nombre: un único objeto de parámetro abstracto que utiliza una sintaxis de clase en línea para inicializar todos los campos. En Java:

MyClass o = new MyClass(new MyClass.Arguments(){{ argA = a; argB = b; argC = c; ... }});

class MyClass {
  ...
  public static abstract class Arguments {
    public int argA;
    public String ArgB;
    ...
  }
}

Sin embargo, es posible olvidar los campos, y esta es una solución bastante específica del lenguaje (he visto usos en JavaScript, C # y C).

Afortunadamente, el constructor aún puede validar todos los argumentos, lo que no es el caso cuando sus objetos se crean en un estado parcialmente construido, y requiere que el usuario proporcione más argumentos a través de establecedores o un init()método, que requieren el menor esfuerzo de codificación, pero Es más difícil escribir programas correctos .

Entonces, si bien existen muchos enfoques para abordar los "muchos parámetros sin nombre que dificultan el mantenimiento del código", quedan otros problemas.

Acercarse al problema raíz

Por ejemplo, el problema de la capacidad de prueba. Cuando escribo pruebas unitarias, necesito la capacidad de inyectar datos de prueba y proporcionar implementaciones de prueba para burlar las dependencias y operaciones que tienen efectos secundarios externos. No puedo hacer eso cuando instancias cualquier clase dentro de tu constructor. A menos que la responsabilidad de su clase sea la creación de otros objetos, no debe crear instancias de clases no triviales. Esto va de la mano con el problema de la responsabilidad individual. Cuanto más enfocada sea la responsabilidad de una clase, más fácil será probarla (y, a menudo, más fácil de usar).

El enfoque más fácil y, a menudo, el mejor es que el constructor tome las dependencias completamente construidas como parámetro , aunque esto incumbe la responsabilidad de administrar las dependencias al llamante, lo cual tampoco es ideal, a menos que las dependencias sean entidades independientes en su modelo de dominio.

A veces se utilizan fábricas (abstractas) o marcos de inyección de dependencia completa , aunque pueden ser excesivos en la mayoría de los casos de uso. En particular, estos solo reducen el número de argumentos si muchos de estos argumentos son objetos cuasi globales o valores de configuración que no cambian entre la instanciación de objetos. Por ejemplo, si los parámetros ay dfueran globales-ish, obtendríamos

Dependencies deps = new Dependencies(a, d);
...
MyClass o = deps.newMyClass(b, c, e, f, g);

class MyClass {
  MyClass(Dependencies deps, B b, C c, E e, F f, G g) {
    this.depA = deps.newDepA(b, c);
    this.depB = deps.newDepB(e, f);
    this.g = g;
  }
  ...
}

class Dependencies {
  private A a;
  private D d;
  public Dependencies(A a, D d) { this.a = a; this.d = d; }
  public DepA newDepA(B b, C c) { return new DepA(a, b, c); }
  public DepB newDepB(E e, F f) { return new DepB(d, e, f); }
  public MyClass newMyClass(B b, C c, E e, F f, G g) {
    return new MyClass(deps, b, c, e, f, g);
  }
}

Dependiendo de la aplicación, esto podría cambiar las reglas del juego donde los métodos de fábrica terminan casi sin argumentos porque el administrador de dependencias puede proporcionar todo, o puede ser una gran cantidad de código que complica la instanciación sin ningún beneficio aparente. Dichas fábricas son mucho más útiles para asignar interfaces a tipos concretos que para administrar parámetros. Sin embargo, este enfoque intenta abordar el problema raíz de demasiados parámetros en lugar de solo ocultarlo con una interfaz bastante fluida.


Realmente argumentaría en contra de la parte autodocumentada. Si tiene un IDE decente + comentarios decentes, la mayoría de las veces el constructor y la definición de parámetros están a una tecla de distancia.
JavierIEH 01 de

En Android Studio 3.0, el nombre del parámetro en el constructor se muestra adyacente al valor que se pasa en una llamada de constructor. por ejemplo: nuevo A (operando1: 34, operando2: 56); operand1 y operand2 son los nombres de los parámetros en el constructor. El IDE les muestra que hacen que el código sea más legible. Por lo tanto, no es necesario ir a la definición para averiguar cuál es el parámetro.
granate

9

El patrón de construcción no resuelve nada por usted y no corrige fallas de diseño.

Si tiene una clase que necesita 10 parámetros para ser construida, hacer que un constructor la construya no mejorará repentinamente su diseño. Debe optar por refactorizar la clase en cuestión.

Por otro lado, si tiene una clase, quizás un DTO simple, donde ciertos atributos de la clase son opcionales, el patrón de construcción podría facilitar la construcción de dicho objeto.


1

Cada parte, por ejemplo, información personal, información de trabajo (cada "período de trabajo"), cada amigo, etc., deben convertirse en sus propios objetos.

Considere cómo implementaría una funcionalidad de actualización. El usuario quiere agregar un nuevo historial de trabajo. El usuario no quiere cambiar ninguna otra parte de la información, ni el historial de trabajo anterior, solo manténgalo igual.

Cuando hay mucha información, es inevitable construir la información pieza por pieza. Puede agrupar esas piezas lógicamente, según la intuición humana (lo que hace que sea más fácil de usar para un programador), o según el patrón de uso (qué piezas de información generalmente se actualizan al mismo tiempo).

El patrón del generador es solo una forma de capturar una lista (ordenada o no) de argumentos escritos, que luego se pueden pasar al constructor real (o el constructor puede leer los argumentos capturados del generador).

La pauta de 4 es solo una regla general. Hay situaciones que requieren más de 4 argumentos y que no hay una forma lógica o sensata de agruparlos. En esos casos, puede tener sentido crear uno structque se pueda rellenar directamente o con definidores de propiedades. Observe que el structy el constructor en este caso tienen un propósito muy similar.

Sin embargo, en su ejemplo, ya ha descrito cuál sería una forma lógica de agruparlos. Si se enfrenta a una situación en la que esto no es cierto, tal vez pueda ilustrarlo con un ejemplo diferente.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.