2008-09-02 18 views
105

Digamos que usted tiene una clase llamada de atención al cliente, que contiene los siguientes campos:¿Cuántos argumentos de constructor son demasiados?

  • Nombre de usuario
  • correo electrónico
  • Nombre
  • Apellido

Supongamos también que de acuerdo con su lógica comercial, todos los objetos del Cliente deben tener estas cuatro propiedades definidas.

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

He visto clases que incluyen más de 20 argumentos en su constructor y es simplemente un dolor usarlos. Pero, como alternativa, si no necesita estos campos, corre el riesgo de tener información no definida o, peor aún, errores de referencia de objeto si confía en el código de llamada para especificar estas propiedades.

¿Hay alguna alternativa a esto o simplemente tienes que decidir si la cantidad X de argumentos del constructor es demasiada para que puedas vivir?

Respuesta

96

Dos enfoques de diseño a considerar

El essence patrón

El patrón fluent interface

Ambos son similares en su intención, en el que poco a poco construimos un objeto intermedio, y luego creamos nuestro objeto de destino en un solo paso.

Un ejemplo de la interfaz fluida en la 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(); 
    } 
} 
+2

Sé esto como la "Idiomática de parámetros nombrados": http://www.parashift.com/c++faq-lite/ctors.html#faq-10.18. Relacionado: También existe la "Named Constructor Idiom": http://www.parashift.com/c++faq-lite/ctors.html#faq-10.8 – Frank

+0

¿Puedes separar la persona que llama y los segmentos llamantes del código para hacer es más claro que son entidades separadas? –

+0

La comprobación de valores nulos (apellido == null || firstName == null || ssn == null) debe hacerse EN EL objeto Cliente. Esto se debe a la posibilidad de que estos valores se modifiquen antes de que se cree el objeto Cliente. Además, asegúrese de que los campos de la clase Cliente estén declarados como "finales privados", lo que hace que el Cliente sea inmutable. – drozzy

0

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

3

Si tiene desagradables muchos argumentos, simplemente empaquételos en estructuras/clases POD, preferiblemente declaradas como clases internas de la clase que está construyendo. De esta forma, aún puede requerir los campos mientras hace que el código que llama al constructor sea razonablemente legible.

0

Simplemente use los argumentos predeterminados. En un lenguaje compatible con los argumentos del método por defecto (PHP, por ejemplo), se puede hacer esto en la firma del método:

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

Hay otras maneras de crear valores por defecto, como en lenguajes que soportan la sobrecarga de métodos .

Por supuesto, también puede establecer valores predeterminados cuando declare los campos, si lo considera apropiado.

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

2

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

2

Creo que todo depende de la situación. Para algo como tu ejemplo, una clase de cliente, no arriesgaría la posibilidad de 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ía muchas cosas que definir en la estructura.

2

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

Luego, realice las funciones getter y setter para cada propiedad para que los valores predeterminados puedan cambiarse.

aplicación Java:

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

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

Esto también es una buena práctica para mantener sus variables globales seguro.

3

Steve Mcconnell escribe en Code Complete que las personas tienen problemas para guardar más 7 cosas en su cabeza a la vez, por lo que ese sería el número con el que intento quedarme.

+0

Pero vea Weinschenk, * 100 cosas que todo diseñador necesita saber sobre las personas *, 48. Al parecer, esto ha sido desacreditado: cuatro es un límite superior más preciso. –

0

Acepto el límite de 7 elementos que menciona Boojiboy. Más allá de eso, puede valer la pena mirar tipos anónimos (o especializados), IDictionary o indirección a través de la clave principal a otra fuente de datos.

4

Creo que la respuesta de "OOP puro" es que si las operaciones en la clase son inválidas cuando ciertos miembros no se inicializan, entonces estos miembros deben ser establecidos por el constructor. Siempre existe el caso de que se puedan usar valores predeterminados, pero supongo que no estamos considerando ese caso. Este es un buen enfoque cuando la API es fija, porque cambiar el único constructor permitido después de que la API se haga pública será una pesadilla para usted y para 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 objetos WPF, encontrará que las clases .NET tienden a favorecer constructores sin parámetros y arrojarán excepciones si los datos no se han inicializado a un estado deseable antes de llamar al método. Sin embargo, esto es probablemente principalmente específico del diseño basado en componentes; No puedo encontrar 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 las propiedades hayan sido validadas. Sinceramente, debido a esto prefiero el enfoque de "constructor establece las propiedades requeridas" si su API está en piedra o no es pública.

La única cosa que yo soy cierto es que probablemente hay innumerables metodologías que pueden resolver este problema, y ​​cada una de ellas presenta su propio conjunto de problemas. Lo mejor que puede hacer es aprender tantos patrones como sea posible y elegir el mejor para el trabajo. (¿No es eso una solución imposible?)

5

Creo que su pregunta es más sobre el diseño de sus clases que sobre el número de argumentos en el constructor. Si necesitaba 20 datos (argumentos) para inicializar con éxito un objeto, probablemente consideraría dividir la clase.

+0

A veces simplemente no es posible. Considere un archivo de Excel con 50 columnas que deben procesarse. La idea de que la clase MyExcelFileLine tenga un constructor con 50 argumentos es bastante aterradora. –

13

En su caso, seguir con el constructor. La información pertenece al Cliente y 4 campos están bien.

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

@contagious sugirió usar el patrón predeterminado y los ajustadores para las atribuciones opcionales. Eso exige que los campos sean mutables, pero ese es un problema menor.

Joshua Block en Java 2 efectivo dicen que en este caso debe 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 usarlo como esto:

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

El ejemplo de arriba fue tomada de Effective Java 2

Y eso no sólo se aplica a constructor. Citando Kent Beck en Implementation Patterns:

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

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

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

Por supuesto, si todos los argumentos son obligatorios en el constructor, terminas moviendo un gran constructor de un lugar a otro. – drozzy

1

me gustaría encapsulan los campos similares en un objeto de su cuenta con su propia construcción/validación lógica.

decir, por ejemplo, si tienes

  • BusinessPhone
  • BusinessAddress
  • HomePhone
  • HomeAddress

me gustaría hacer una clase que almacena teléfono y dirección en conjunto con una etiqueta que especifica si es un teléfono/dirección "de casa" o "de negocios". 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 se parezca menos a los espaguetis.

Seguramente si tiene muchos campos, debe haber algún patrón que pueda extraer que podría ser una buena unidad de función propia. Y crea un código más legible también.

Y la siguiente es también posibles soluciones:

  • extiendo la lógica de validación en lugar de almacenarla en una sola clase. Validar la entrada del usuario cuando ellos y luego validar de nuevo en la capa de base de datos, etc ...
  • hacer una clase CustomerFactory que me ayudaría a construyo Customer s
  • @ solución de Marcio También es interesante ...
21

I ver que algunas personas recomiendan siete como límite superior. Aparentemente no es cierto que la gente pueda tener siete cosas en su cabeza a la vez; solo pueden recordar cuatro (Susan Weinschenk, 100 cosas que todo diseñador debe saber sobre las personas, 48).Aun así, considero que cuatro son una especie de órbita terrestre alta. Pero eso es porque mi pensamiento ha sido alterado por Bob Martin.

En código de limpieza, el tío Bob argumenta a favor de tres como un límite superior general para el número de parámetros. Él hace la afirmación radical (40):

El número ideal de argumentos para una función es cero (niladic). Luego viene uno (monádico) seguido de cerca por dos (diádica). Tres argumentos (triádicos) deben evitarse siempre que sea posible. Más de tres (polyadic) requiere una justificación muy especial — y luego no debe usarse de todos modos.

Lo dice por su legibilidad; pero también debido a la capacidad de prueba:

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

Le sugiero que busque una copia de su libro y lea la discusión completa de los argumentos de la función (40-43).

Estoy de acuerdo con aquellos que han mencionado el Principio de Responsabilidad Individual. Es difícil para mí creer que una clase que necesita más de dos o tres valores/objetos sin valores predeterminados razonables realmente tiene solo una responsabilidad, y no estaría mejor con otra clase extraída.

Ahora, si está inyectando sus dependencias a través del constructor, los argumentos de Bob Martin sobre lo fácil que es invocar al constructor no se aplican tanto (porque normalmente solo hay un punto en su aplicación donde lo conecta) , o incluso tiene un marco que lo hace por usted). Sin embargo, el Principio de Responsabilidad Individual sigue siendo relevante: una vez que una clase tiene cuatro dependencias, considero que es un olor que está haciendo una gran cantidad de trabajo.

Sin embargo, al igual que con todas las cosas en ciencias de la computación, existen sin duda casos válidos para tener una gran cantidad de parámetros de constructor. No distorsione su código para evitar el uso de una gran cantidad de parámetros; pero si usa una gran cantidad de parámetros, deténgase y reflexione, porque puede significar que su código ya está contorsionado.

+0

Nunca paso argumentos a los constructores ... Los paso todos en una función init, y el argumento es 1 objeto que contiene todos los argumentos requeridos. Pero luego, hago javascript ... ¿Qué es Java? – andygoestohollywood

+1

Siempre me he preguntado cómo funciona esto con las "clases de datos", que existen solo para contener datos relacionados. Si aplica esto a la pregunta de OP, su clase solo contiene datos para un cliente. ¿Alguna idea sobre cómo se pueden reducir los parámetros en ese caso? – 0cd

+0

@Puneet, También hay una crítica similar en la que un constructor podría tomar solo 3 argumentos, pero todos esos argumentos son grandes clases compuestas. Entonces, en esencia, estás enviando 60 parámetros al constructor, solo que están empaquetados. – LegendLength

Cuestiones relacionadas