Pregunta

Digamos que tiene una clase llamada Cliente, que contiene los siguientes campos:

  • Nombre de usuario
  • Correo electrónico
  • Nombre de pila
  • Apellido

Digamos también que, según su lógica empresarial, todos los objetos Cliente deben tener definidas estas cuatro propiedades.

Ahora, podemos hacer esto con bastante facilidad obligando al constructor a especificar cada una de estas propiedades.Pero es bastante fácil ver cómo esto puede salirse de control cuando se ve obligado a agregar más campos obligatorios al objeto Cliente.

He visto clases que aceptan más de 20 argumentos en su constructor y es complicado usarlos.Pero, alternativamente, si no requiere estos campos, corre el riesgo de tener información indefinida o, peor aún, errores de referencia a objetos si confía en el código de llamada para especificar estas propiedades.

¿Existe alguna alternativa a esto o simplemente tiene que decidir si X cantidad de argumentos del constructor es demasiado para vivir con él?

¿Fue útil?

Solución

Dos enfoques de diseño a considerar

El esencia patrón

El interfaz fluida patrón

Ambos tienen una intención similar, en el sentido de que lentamente construimos un objeto intermedio y luego creamos nuestro objeto de destino en un solo paso.

Un ejemplo de la interfaz fluida en acción sería:

public class CustomerBuilder {
    String surname;
    String firstName;
    String ssn;
    public static CustomerBuilder customer() {
        return new CustomerBuilder();
    }
    public CustomerBuilder withSurname(String surname) {
        this.surname = surname; 
        return this; 
    }
    public CustomerBuilder withFirstName(String firstName) {
        this.firstName = firstName;
        return this; 
    }
    public CustomerBuilder withSsn(String ssn) {
        this.ssn = ssn; 
        return this; 
    }
    // client doesn't get to instantiate Customer directly
    public Customer build() {
        return new Customer(this);            
    }
}

public class Customer {
    private final String firstName;
    private final String surname;
    private final String ssn;

    Customer(CustomerBuilder builder) {
        if (builder.firstName == null) throw new NullPointerException("firstName");
        if (builder.surname == null) throw new NullPointerException("surname");
        if (builder.ssn == null) throw new NullPointerException("ssn");
        this.firstName = builder.firstName;
        this.surname = builder.surname;
        this.ssn = builder.ssn;
    }

    public String getFirstName() { return firstName;  }
    public String getSurname() { return surname; }
    public String getSsn() { return ssn; }    
}
import static com.acme.CustomerBuilder.customer;

public class Client {
    public void doSomething() {
        Customer customer = customer()
            .withSurname("Smith")
            .withFirstName("Fred")
            .withSsn("123XS1")
            .build();
    }
}

Otros consejos

Veo que algunas personas recomiendan siete como límite superior.Aparentemente no es cierto que las personas puedan tener siete cosas en la cabeza a la vez;sólo pueden recordar cuatro (Susan Weinschenk, 100 cosas que todo diseñador necesita saber sobre las personas, 48).Aun así, considero que cuatro es una especie de órbita terrestre alta.Pero eso se debe a que Bob Martin ha alterado mi forma de pensar.

En Código limpio, El tío Bob defiende tres como límite superior general para el número de parámetros.Hace la afirmación radical (40):

El número ideal de argumentos para una función es cero (niládico).Luego viene uno (monádico) seguido de cerca por dos (diádico).Siempre que sea posible, deben evitarse tres argumentos (tríadicos).Más de tres (poliádico) requieren una justificación muy especial y, de todos modos, no deberían usarse.

Dice esto por motivos de legibilidad;pero también por la capacidad de prueba:

Imagine la dificultad de escribir todos los casos de prueba para garantizar que todas las combinaciones de argumentos funcionen correctamente.

Le animo a buscar una copia de su libro y leer su discusión completa sobre los argumentos de funciones (40-43).

Estoy de acuerdo con quienes han mencionado el Principio de Responsabilidad Única.Es difícil para mí creer que una clase que necesita más de dos o tres valores/objetos sin valores predeterminados razonables realmente tenga una sola responsabilidad y no estaría mejor si se extrajera otra clase.

Ahora, si estás inyectando tus dependencias a través del constructor, los argumentos de Bob Martin sobre lo fácil que es invocar el constructor no se aplican tanto (porque normalmente solo hay un punto en tu aplicación donde conectas eso, o incluso tiene un marco que lo hace por usted).Sin embargo, el principio de responsabilidad única sigue siendo relevante:Una vez que una clase tiene cuatro dependencias, considero que huele que está haciendo una gran cantidad de trabajo.

Sin embargo, como ocurre con todo lo relacionado con la informática, sin duda existen casos válidos para tener una gran cantidad de parámetros de constructor.No contorsione su código para evitar el uso de una gran cantidad de parámetros;pero si utiliza una gran cantidad de parámetros, deténgase y piénselo un poco, porque puede significar que su código ya está retorcido.

En tu caso, quédate con el constructor.La información pertenece al Cliente y 4 campos están bien.

En caso de que tenga muchos campos obligatorios y opcionales, el constructor no es la mejor solución.Como dijo @boojiboy, es difícil de leer y también de escribir código de cliente.

@contagious sugirió usar el patrón y los configuradores predeterminados para atributos opcionales.Eso exige que los campos sean mutables, pero ese es un problema menor.

Joshua Block en Effective Java 2 dice que en este caso debería considerar un constructor.Un ejemplo tomado del libro:

 public class NutritionFacts {  
   private final int servingSize;  
   private final int servings;  
   private final int calories;  
   private final int fat;  
   private final int sodium;  
   private final int carbohydrate;  

   public static class Builder {  
     // required parameters  
     private final int servingSize;  
     private final int servings;  

     // optional parameters  
     private int calories         = 0;  
     private int fat              = 0;  
     private int carbohydrate     = 0;  
     private int sodium           = 0;  

     public Builder(int servingSize, int servings) {  
      this.servingSize = servingSize;  
       this.servings = servings;  
    }  

     public Builder calories(int val)  
       { calories = val;       return this; }  
     public Builder fat(int val)  
       { fat = val;            return this; }  
     public Builder carbohydrate(int val)  
       { carbohydrate = val;   return this; }  
     public Builder sodium(int val)  
       { sodium = val;         return this; }  

     public NutritionFacts build() {  
       return new NutritionFacts(this);  
     }  
   }  

   private NutritionFacts(Builder builder) {  
     servingSize       = builder.servingSize;  
     servings          = builder.servings;  
     calories          = builder.calories;  
     fat               = builder.fat;  
     soduim            = builder.sodium;  
     carbohydrate      = builder.carbohydrate;  
   }  
}  

Y luego úsalo así:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
      calories(100).sodium(35).carbohydrate(27).build();

El ejemplo anterior fue tomado de Java 2 efectivo

Y eso no sólo se aplica al constructor.Citando a Kent Beck en Patrones de implementación:

setOuterBounds(x, y, width, height);
setInnerBounds(x + 2, y + 2, width - 4, height - 4);

Hacer explícito el rectángulo como un objeto explica mejor el código:

setOuterBounds(bounds);
setInnerBounds(bounds.expand(-2));

Creo que la respuesta "pura programación orientada a objetos" es que si las operaciones en la clase no son válidas cuando ciertos miembros no están inicializados, entonces el constructor debe configurar estos miembros.Siempre existe el caso en el que se pueden usar valores predeterminados, pero asumiré que no estamos considerando ese caso.Este es un buen enfoque cuando la API está arreglada, porque cambiar el único constructor permitido después de que la API se haga pública será una pesadilla para usted y todos los usuarios de su código.

En C#, lo que entiendo acerca de las pautas de diseño es que esta no es necesariamente la única forma de manejar la situación.Particularmente con los objetos WPF, encontrará que las clases .NET tienden a favorecer a los constructores sin parámetros y generarán excepciones si los datos no se han inicializado a un estado deseable antes de llamar al método.Sin embargo, esto probablemente sea principalmente específico del diseño basado en componentes;No se me ocurre un ejemplo concreto de una clase .NET que se comporte de esta manera.En su caso, definitivamente causaría una mayor carga en las pruebas para garantizar que la clase nunca se guarde en el almacén de datos a menos que se hayan validado las propiedades.Honestamente, debido a esto, preferiría el enfoque de "el constructor establece las propiedades requeridas" si su API está escrita en piedra o no es pública.

Lo único que yo soy Lo cierto es que probablemente existen innumerables metodologías que pueden resolver este problema, y ​​cada una de ellas presenta su propio conjunto de problemas.Lo mejor que puedes hacer es aprender tantos patrones como sea posible y elegir el mejor para el trabajo.(¿No es esa una respuesta tan evasiva?)

Creo que tu pregunta es más sobre el diseño de tus clases que sobre la cantidad de argumentos en el constructor.Si necesitara 20 datos (argumentos) para inicializar exitosamente un objeto, probablemente consideraría dividir la clase.

Steve McConnell escribe en Code Complete que las personas tienen problemas para mantener más de 7 cosas en su cabeza a la vez, por lo que ese sería el número bajo el cual trato de mantenerme.

Si tiene muchos argumentos desagradables, simplemente empaquetelos en estructuras/clases POD, preferiblemente declarados como clases internas de la clase que está construyendo.De esa manera aún puedes requerir los campos mientras haces que el código que llama al constructor sea razonablemente legible.

Creo que todo depende de la situación.Para algo como su ejemplo, una clase de cliente, no me arriesgaría a que esos datos no estén definidos cuando sea necesario.Por otro lado, pasar una estructura aclararía la lista de argumentos, pero aún tendrías muchas cosas que definir en la estructura.

Creo que la forma más sencilla sería encontrar un valor predeterminado aceptable para cada valor.En este caso, parece que sería necesario construir cada campo, por lo que posiblemente sobrecargue la llamada a la función para que, si algo no está definido en la llamada, se establezca como predeterminado.

Luego, cree funciones getter y setter para cada propiedad para que se puedan cambiar los valores predeterminados.

Implementación de Java:

public static void setEmail(String newEmail){
    this.email = newEmail;
}

public static String getEmail(){
    return this.email;
}

Esta también es una buena práctica para mantener seguras las variables globales.

El estilo cuenta mucho y me parece que si hay un constructor con más de 20 argumentos, entonces el diseño debería modificarse.Proporcionar valores predeterminados razonables.

Estoy de acuerdo con el límite de 7 elementos que menciona Boojiboy.Más allá de eso, puede valer la pena buscar tipos anónimos (o especializados), IDictionary o direccionamiento indirecto a través de una clave primaria a otra fuente de datos.

Encapsularía campos similares en un objeto propio con su propia lógica de construcción/validación.

Digamos, por ejemplo, si tienes

  • Teléfono de negocios
  • Dirección de Negocios
  • Teléfono de casa
  • Direccion de casa

Crearía una clase que almacene el teléfono y la dirección junto con una etiqueta que especifique si es un teléfono/dirección "doméstico" o "comercial".Y luego reduzca los 4 campos a simplemente una matriz.

ContactInfo cinfos = new ContactInfo[] {
    new ContactInfo("home", "+123456789", "123 ABC Avenue"),
    new ContactInfo("biz", "+987654321", "789 ZYX Avenue")
};

Customer c = new Customer("john", "doe", cinfos);

Eso debería hacer que parezca menos espagueti.

Seguramente, si tiene muchos campos, debe haber algún patrón que pueda extraer y que constituya una buena unidad de función propia.Y también haga que el código sea más legible.

Y las siguientes también son posibles soluciones:

  • Distribuya la lógica de validación en lugar de almacenarla en una sola clase.Valide cuando el usuario los ingrese y luego valide nuevamente en la capa de la base de datos, etc.
  • Hacer una CustomerFactory clase que me ayudaría a construir Customers
  • La solución de @marcio también es interesante...

Simplemente use argumentos predeterminados.En un lenguaje que admita argumentos de método predeterminados (PHP, por ejemplo), puede hacer esto en la firma del método:

public function doSomethingWith($this = val1, $this = val2, $this = val3)

Hay otras formas de crear valores predeterminados, como en idiomas que admiten la sobrecarga de métodos.

Por supuesto, también puedes establecer valores predeterminados cuando declaras los campos, si lo consideras apropiado.

Realmente todo se reduce a si es apropiado o no establecer estos valores predeterminados, o si sus objetos deben especificarse durante la construcción todo el tiempo.Esa es realmente una decisión que sólo tú puedes tomar.

A menos que sea más de 1 argumento, siempre uso matrices u objetos como parámetros del constructor y confío en la verificación de errores para asegurarme de que los parámetros requeridos estén ahí.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top