¿Hay alguna razón para hacer todo el trabajo de un objeto en un constructor?

47

Permítanme comenzar con esto diciendo que este no es mi código ni el de mis compañeros de trabajo. Hace años, cuando nuestra empresa era más pequeña, teníamos algunos proyectos que necesitábamos que no teníamos capacidad, por lo que fueron subcontratados. Ahora, no tengo nada en contra de la subcontratación o los contratistas en general, pero la base de código que produjeron es una masa de WTF. Dicho esto, funciona (en su mayoría), así que supongo que está en el 10% superior de los proyectos subcontratados que he visto.

A medida que nuestra compañía ha crecido, hemos tratado de tomar más de nuestro desarrollo en casa. Este proyecto en particular aterrizó en mi regazo, así que lo he estado revisando, limpiando, agregando pruebas, etc.

Hay un patrón que veo que se repite mucho y parece tan terrible que me pregunté si tal vez hay una razón y no la veo. El patrón es un objeto sin métodos o miembros públicos, solo un constructor público que hace todo el trabajo del objeto.

Por ejemplo, (el código está en Java, si eso importa, pero espero que sea una pregunta más general):

public class Foo {
  private int bar;
  private String baz;

  public Foo(File f) {
    execute(f);
  }

  private void execute(File f) {
     // FTP the file to some hardcoded location, 
     // or parse the file and commit to the database, or whatever
  }
}

Si se está preguntando, este tipo de código a menudo se llama de la siguiente manera:

for(File f : someListOfFiles) {
   new Foo(f);
}

Hace mucho tiempo, me enseñaron que los objetos instanciados en un bucle generalmente son una mala idea y que los constructores deberían hacer un mínimo de trabajo. Mirando este código parece que sería mejor eliminar el constructor y hacer de execute un método estático público.

Le pregunté al contratista por qué se hizo de esta manera, y la respuesta que obtuve fue "Podemos cambiarlo si lo desea". Lo que no fue muy útil.

De todos modos, ¿hay alguna razón para hacer algo como esto, en cualquier lenguaje de programación, o es solo otra presentación al Daily WTF?

    
pregunta Kane 02.08.2012 - 15:36

12 respuestas

58

Bien, bajando la lista:

Hace mucho tiempo me enseñaron que los objetos instanciados en un bucle generalmente son una mala idea

No en ningún idioma que haya usado.

En C es una buena idea declarar tus variables por adelantado, pero eso es diferente de lo que dijiste. Podría ser ligeramente más rápido si declara los objetos por encima del bucle y los reutiliza, pero hay muchos idiomas en los que este aumento de velocidad no tendrá sentido (y probablemente algunos compiladores que hagan la optimización por usted) :)).

En general, si necesita un objeto dentro de un bucle, cree uno.

los constructores deben hacer un mínimo de trabajo

Los constructores deben crear una instancia de los campos de un objeto y hacer cualquier otra inicialización necesaria para que el objeto esté listo para usar. Esto generalmente significa que los constructores son pequeños, pero hay escenarios en los que esto sería una cantidad sustancial de trabajo.

¿Hay alguna vez una razón para hacer algo como esto, en cualquier lenguaje de programación, o es solo otra presentación al Daily WTF?

Es una presentación al Daily WTF. Por supuesto, hay cosas peores que puedes hacer con el código. El problema es que el autor tiene una gran incomprensión de qué son las clases y cómo usarlas. Específicamente, esto es lo que veo que está mal con este código:

  • Uso indebido de clases: la clase actúa básicamente como una función. Como mencionó, debería ser reemplazado por una función estática, o la función debería implementarse en la clase que la está llamando. Depende de lo que hace y dónde se usa.
  • Gastos generales de rendimiento: según el idioma, crear un objeto puede ser más lento que llamar a una función.
  • Confusión general: es generalmente confuso para el programador cómo usar este código. Sin verlo en uso, nadie sabría cómo pretendía el autor utilizar el código en cuestión.
respondido por el riwalk 02.08.2012 - 15:49
25

Para mí, es un poco sorprendente cuando un objeto hace mucho trabajo en el constructor, por lo que solo lo recomendaría (principio de menos sorpresa).

Un intento de un buen ejemplo :

No estoy seguro de si este uso es un antipatrón, pero en C # he visto un código que estaba en la línea de (ejemplo un poco inventado):

using (ImpersonationContext administrativeContext = new ImpersonationContext(ADMIN_USER))
{
    // Perform actions here under the administrator's security context
}
// We're back to "normal" privileges here

En este caso, la clase ImpersonationContext realiza todo su trabajo en el constructor y en el método Dispose. Aprovecha la declaración C # using , que es bastante bien entendida por los desarrolladores de C #, y por lo tanto garantiza que la configuración que se produce en el constructor se revierte una vez que estamos fuera de la declaración using , incluso si es una excepción. ocurre. Este es probablemente el mejor uso que he visto para esto (es decir, "trabajo" en el constructor), aunque todavía no estoy seguro de que lo considere "bueno". En este caso, es bastante explicativo, funcional y sucinto.

Un ejemplo de un patrón bueno y similar sería el patrón de comando . Definitivamente no ejecuta la lógica en el constructor allí, pero es similar en el sentido de que toda la clase es equivalente a una invocación de método (incluidos los parámetros necesarios para invocar el método). De esta manera, la información de invocación del método puede ser serializada o pasada para un uso posterior, lo que es sumamente útil. Quizás sus contratistas intentaron algo similar, aunque lo dudo mucho.

Comentarios sobre tu ejemplo:

Yo diría que es un anti-patrón muy definido. No agrega nada, va en contra de lo que se espera y realiza un procesamiento demasiado largo en el constructor (¿FTP en el constructor? WTF, aunque supongo que se sabe que la gente llama a los servicios web de esta manera).

Estoy luchando por encontrar la cita, pero el libro de Grady Booch "Object Solutions" tiene una anécdota sobre un equipo de desarrolladores de C que está haciendo la transición a C ++. Aparentemente, habían pasado por una capacitación y la gerencia les dijo que tenían que hacer las cosas "orientadas a objetos". Presionado para descubrir qué es lo que no funciona, el autor usó una herramienta de código métrico y encontró que el número promedio de métodos por clase era exactamente 1, y eran variaciones de la frase "Hazlo". Debo decir que nunca antes había encontrado este problema en particular, pero aparentemente puede ser un síntoma de que las personas se ven obligadas a crear código orientado a objetos, sin entender realmente cómo hacerlo, y por qué.

    
respondido por el Daniel B 02.08.2012 - 16:05
13

No.

Si todo el trabajo se realiza en el constructor, significa que el objeto nunca fue necesario en primer lugar. Lo más probable es que el contratista simplemente estuviera utilizando el objeto para modularidad. En ese caso, una clase no instanciada con un método estático habría obtenido el mismo beneficio sin crear instancias de objetos superfluos.

Hay un solo caso en el que es aceptable hacer todo el trabajo en un constructor de objetos. Es entonces cuando el propósito del objeto es representar específicamente una identidad única a largo plazo, en lugar de realizar un trabajo. En tal caso, el objeto se mantendría alrededor por otras razones además de realizar un trabajo significativo. Por ejemplo, una instancia de singleton.

    
respondido por el Kevin A. Naudé 02.08.2012 - 19:32
6

Ciertamente, he creado muchas clases que hacen mucho trabajo para calcular algo en el constructor, pero el objeto es inmutable, por lo que hace que los resultados de ese cálculo estén disponibles a través de las propiedades, y luego nunca hace nada más. Esa es la inmutabilidad normal en acción.

Creo que lo más extraño aquí es poner código con efectos secundarios en un constructor. Construir un objeto y desecharlo sin acceder a las propiedades o métodos parece extraño (pero al menos en este caso es bastante obvio que algo más está sucediendo).

Esto sería mejor si la función se moviera a un método público, y luego se inyecta una instancia de la clase, luego se hace que el bucle for llame al método repetidamente.

    
respondido por el Scott Whitlock 02.08.2012 - 21:09
3
  

¿Hay alguna razón para hacer todo el trabajo de un objeto en un constructor?

No.

  • Un constructor no debería tener efectos secundarios.
    • Cualquier cosa más que la inicialización de un campo privado debe verse como un efecto secundario.
    • Un constructor con efectos secundarios rompe el principio de responsabilidad única (SRP) y es contrario al espíritu de la programación orientada a objetos (OOP).
  • Un constructor debe ser ligero y nunca debe fallar.
    • Por ejemplo, siempre me estremezco cuando veo un bloque try-catch dentro de un constructor. Un constructor no debe lanzar excepciones o errores de registro.

Uno podría cuestionar razonablemente estas pautas y decir: "¡Pero no sigo estas reglas y mi código funciona bien!" A eso, respondería: "Bueno, eso puede ser cierto, hasta que no lo sea".

  • Las excepciones y los errores dentro de un constructor son muy inesperados. A menos que se les diga que lo hagan, los futuros programadores no estarán dispuestos a rodear estas llamadas de constructor con código defensivo.
  • Si algo falla en la producción, el rastreo de pila generado puede ser difícil de analizar. La parte superior de la traza de la pila puede apuntar a la llamada del constructor, pero suceden muchas cosas en el constructor, y puede que no apunte al LOC real que falló.
    • He analizado muchos rastros de la pila .NET donde este fue el caso.
respondido por el Jim G. 09.09.2012 - 17:13
2

Me parece que la persona que hizo esto estaba tratando de evitar la limitación de Java que no permite pasar funciones como ciudadanos de primera clase. Siempre que tenga que pasar una función, debe envolverla dentro de una clase con un método como apply o similar.

Así que supongo que solo usó un atajo y usó directamente una clase como función de reemplazo. Cuando quiera ejecutar la función, simplemente cree una instancia de la clase.

Definitivamente no es un buen patrón, al menos porque estamos aquí tratando de resolverlo, pero creo que puede ser la forma menos detallada de hacer algo similar a la programación funcional en Java.

    
respondido por el Andrea 02.08.2012 - 16:52
1

Para mí, la regla de oro no es hacer nada en el constructor, excepto para inicializar variables de miembros. La primera razón para esto es que ayuda a seguir el principio de SRP, ya que generalmente un largo proceso de inicialización es una indicación de que la clase hace más de lo que debería, y la inicialización debe realizarse en algún lugar fuera de la clase. La segunda razón es que de esta manera solo se pasan los parámetros necesarios, por lo que creas menos código acoplado. Un constructor con una inicialización complicada generalmente necesita parámetros que se usan solo para construir otro objeto, pero que no son utilizados por la clase original.

    
respondido por el rmaruszewski 02.08.2012 - 17:03
1

Voy a ir contra la corriente aquí: en idiomas en los que todo tiene que estar en alguna clase Y no puedes tener una clase estática, puedes enfrentarte a la situación en la que tienes un poco de funcionalidad aislada que debe colocarse en algún lugar y no encaja con ninguna otra cosa. Sus opciones son una clase de utilidad que cubre varios bits de funcionalidad no relacionados, o una clase que básicamente solo hace esto.

Si solo tiene un bit como este, entonces su clase estática es su clase específica. Entonces, o tienes un constructor que hace todo el trabajo, o una sola función que hace todo el trabajo. La ventaja de hacer todo en el constructor es que ocurre solo una vez, le permite usar campos privados, propiedades y métodos sin tener que preocuparse de que sean reutilizados o de subprocesos. Si tiene un constructor / método, puede verse tentado a permitir que los parámetros se pasen al método único, pero si utiliza campos privados o propiedades que introducen posibles problemas de subprocesos.

Incluso con una clase de utilidad, la mayoría de las personas no pensarán tener clases estáticas anidadas (y eso tampoco podría ser posible dependiendo del idioma).

Básicamente, esta es una forma relativamente comprensible de aislar el comportamiento del resto del sistema, dentro de lo que permite el idioma, y al mismo tiempo le permite aprovechar la mayor cantidad de lenguaje posible.

Francamente, habría respondido tanto como lo hizo su contratista, es un pequeño ajuste convertirlo en dos llamadas, no hay mucho que recomendar una sobre la otra. ¿Qué cambios / cambios existen, son probablemente más hipotéticos que reales, por qué tratar de justificar la decisión cuando no importa y probablemente no se decidió tanto como la acción predeterminada?

    
respondido por el jmoreno 09.09.2012 - 21:37
1

Hay patrones comunes en idiomas en los que las funciones y las clases son mucho más similares, como Python, donde uno usa un objeto básicamente para funcionar con demasiados efectos secundarios o con múltiples objetos con nombre devueltos.

En este caso, la mayor parte, si no todo el trabajo, puede realizarse en el constructor, ya que el objeto en sí es solo un envoltorio alrededor de un único paquete de trabajo, y no hay una buena razón para meterlo en otro función. Algo como

var parser = XMLParser()
var x = parser.parse(string)
string a = x.LookupTag(s)

realmente no es mejor que

 var x = ParsedXML(string)
 string a = x.LookupTag(s)

En lugar de tener los efectos secundarios directamente, puede llamar al objeto para imponer el efecto secundario en la señal, lo que le brinda una mejor visibilidad. Si voy a tener un efecto secundario negativo en un objeto, puedo hacer todo mi trabajo y luego pasar el objeto, haré un mal efecto y dañarlo. Identificar el objeto y aislar la llamada de esta manera es más claro que pasar varios de estos objetos a una función a la vez.

x.UpdateView(v)

Puede hacer que los valores de retorno múltiples a través de los accesores en el objeto. Todo el trabajo está hecho, pero quiero todo eso en contexto, y realmente no quiero pasar varias referencias a mi función.

var x = new ComputeStatistics(inputFile)
int max = x.MaxOptions;
int mean = x.AverageOption;
int sd = x.StandardDeviation;
int k = x.Kurtosis;
...

Entonces, como programador conjunto de Python / C #, esto no me parece tan extraño.

    
respondido por el Jon Jay Obermark 25.09.2014 - 19:46
1

Un caso viene a la mente cuando el constructor / destructor hace todo el trabajo:

Cerraduras. Normalmente nunca interactúas con un candado durante su vida útil. El uso de la arquitectura de C # es bueno para manejar estos casos sin hacer referencia explícita al destructor. Sin embargo, no todos los bloqueos se implementan de esta manera.

También he escrito lo que equivalía a un bit de código solo para constructores para parchear un error de biblioteca en tiempo de ejecución. (En realidad, corregir el código podría haber causado otros problemas). No había destructor porque no había necesidad de deshacer el parche.

    
respondido por el Loren Pechtel 26.09.2014 - 01:25
-1

Estoy de acuerdo con AndyBursh. Parece un problema de falta de conocimiento.

Sin embargo, es importante mencionar marcos como NHibernate, que requieren que usted simplemente codifique en el constructor (al crear clases de mapas). Sin embargo, esto no es una limitación del marco, es justo lo que necesita. Cuando se trata de reflexión, el constructor es el único método que siempre existirá (aunque podría ser privado) y esto podría ser útil si tiene que llamar un método de una clase de forma dinámica.

    
respondido por el fabiopagoti 03.08.2012 - 01:16
-4
  

"Hace mucho tiempo me enseñaron que los objetos instanciados en un bucle son   generalmente una mala idea "

Sí, puede que estés recordando por qué necesitamos el StringBuilder.

Hay dos escuelas de Java.

El constructor fue promovido por el primero , que nos enseña a reutilizar (porque compartir = identidad con eficiencia). Sin embargo, a principios de 2000 comencé a leer que crear objetos en Java es tan barato (a diferencia de C ++) y GC es tan eficiente que la reutilización no vale nada y lo único que importa es la productividad del programador. Por productividad, debe escribir código manejable, lo que significa modularización (también conocida como reducción del alcance). Entonces,

esto es malo

byte[] array = new byte[kilos or megas]
for_loop:
  use(array)

esto es bueno.

for_loop:
  byte[] array = new byte[kilos or megas]
  use(array)

Hice esta pregunta cuando existía forum.java.sun, y la gente estuvo de acuerdo en que preferirían declarar la matriz lo más estrecha posible.

El programador java moderno nunca duda en declarar una nueva clase o crear un objeto. Se le anima a hacerlo. Java da todas las razones para hacerlo, con su idea de que "todo es un objeto" y una creación barata.

Además, el estilo de programación empresarial moderno es, cuando necesita ejecutar una acción, crea una clase especial (o, mejor aún, un marco) que ejecutará esa simple acción. Buenas IDE de Java, que ayudan aquí. Generan toneladas de basura automáticamente, como la respiración.

Entonces, creo que tus objetos carecen del patrón de Generador o Fábrica. No es decente crear instancias directamente, por constructor. Se aconseja una clase especial de fábrica para cada objeto.

Ahora, cuando la programación funcional entra en juego, la creación de objetos se vuelve aún más sencilla. Creará objetos a cada paso mientras respira, sin siquiera notarlo. Por lo tanto, espero que su código se vuelva aún más "eficiente" en la nueva era

  

"no hagas nada en tu constructor. Es solo para inicialización"

Personalmente, no veo ninguna razón en la guía. Considero que es solo otro método. El código de factorización es necesario solo cuando puedes compartirlo.

    
respondido por el Val 09.09.2012 - 11:14

Lea otras preguntas en las etiquetas