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


49

Permítanme introducir esto diciendo que este no es mi código ni el código de mis compañeros de trabajo. Hace años, cuando nuestra empresa era más pequeña, teníamos algunos proyectos que necesitábamos hacer para los que no teníamos capacidad, por lo que se subcontrataron. Ahora, no tengo nada en contra de la contratación externa o los contratistas en general, pero la base de código que produjeron es una masa de WTF. Dicho esto, funciona (principalmente), por lo que supongo que se encuentra en el 10% de los proyectos subcontratados que he visto.

A medida que nuestra empresa ha crecido, hemos tratado de aprovechar más nuestro desarrollo internamente. 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 increíblemente horrible que me pregunté si tal vez hay una razón y simplemente no lo veo. El patrón es un objeto sin miembros o métodos 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 pregunta, este tipo de código a menudo se llama de la siguiente manera:

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

Ahora, me enseñaron hace mucho tiempo que los objetos instanciados en un bucle generalmente son una mala idea, y que los constructores deben hacer un mínimo de trabajo. Al mirar este código, parece que sería mejor soltar el constructor y crear executeun 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 cual no fue realmente útil.

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


18
Me parece que fue escrito por desarrolladores que conocen public static void main(string[] args)y han oído hablar de objetos, y luego trataron de combinarlos.
Andy Hunt

Si nunca te vuelven a llamar, debes hacerlo allí. Muchos marcos modernos para, por ejemplo, la inyección de dependencias necesitan que se hagan los beans, establecer propiedades y ENTONCES invocarlos, y luego no puede usar estos trabajadores pesados.

44
Pregunta relacionada: ¿ Algún problema con hacer el trabajo principal de una clase en su constructor? . Sin embargo, este es un poco diferente, porque la instancia creada se usa después.
sleske


Realmente no quiero expandir esto a una respuesta completa, pero permítanme decir: hay escenarios, donde esto es posiblemente aceptable. Este está muy lejos de eso. Aquí se instancia un objeto sin necesitarlo realmente. En contraste con eso, si estaba creando algún tipo de estructura de datos a partir de un archivo XML, iniciar el análisis desde el constructor no es del todo horrible. De hecho, en los idiomas que permiten sobrecargar los constructores, no es nada por lo que te mataría como mantenedor;)
back2dos

Respuestas:


60

Ok, 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 sus variables por adelantado, pero eso es diferente de lo que dijo. Puede ser un poco más rápido si declara objetos por encima del ciclo y los reutiliza, pero hay muchos lenguajes en los que este aumento de velocidad no tendrá sentido (y probablemente algunos compiladores que hacen 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 instancias 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 supondría una cantidad considerable de trabajo.

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

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

  • Uso incorrecto de las clases: la clase básicamente actúa como una función. Como mencionó, debería reemplazarse con una función estática, o la función debería implementarse en la clase que la llama. Depende de lo que haga y de dónde se use.
  • Sobrecarga de rendimiento: según el idioma, crear un objeto puede ser más lento que llamar a una función.
  • Confusión general: generalmente es confuso para el programador cómo usar este código. Sin verlo usado, nadie sabría cómo el autor pretendía usar el código en cuestión.

Gracias por su respuesta. En referencia a la "creación de instancias de objetos en un bucle", esto es algo sobre lo que advierten muchas herramientas de análisis estático, y también escuché eso de algunos profesores universitarios. De acuerdo, eso podría ser un retraso de los años 90 cuando el éxito en el rendimiento fue mayor. Por supuesto, si lo necesita, hágalo, pero me sorprende escuchar un punto de vista opuesto. Gracias por ampliar mis horizontes.
Kane

8
Crear instancias de objetos en un bucle es un olor a código menor; deberías mirarlo y preguntar "¿realmente necesito instanciar este objeto varias veces?". Si está instanciando el mismo objeto con los mismos datos y diciéndole que haga lo mismo, ahorrará ciclos (y evitará agotar la memoria) al instanciarlo una vez y luego solo repetir la instrucción con los cambios incrementales que puedan ser necesarios para el estado del objeto . Sin embargo, si, por ejemplo, está leyendo un flujo de datos desde una base de datos u otra entrada y está creando una serie de objetos para contener esos datos, se debe crear una instancia.
KeithS

3
Versión corta: no use instancias de objetos como si fueran una función.
Erik Reppen

27

Para mí, es un poco sorprendente cuando obtienes un objeto que hace mucho trabajo en el constructor, así que solo recomendaría eso (principio de menor sorpresa).

Un intento de un buen ejemplo :

No estoy seguro de si este uso es un antipatrón, pero en C # he visto código que estaba en la línea de (ejemplo 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 ImpersonationContextclase hace todo su trabajo en el constructor y el método Dispose. Aprovecha la usingdeclaración de C # , que los desarrolladores de C # entienden bastante bien, y por lo tanto garantiza que la configuración que se produce en el constructor se revierte una vez que estamos fuera de la usingdeclaración, incluso si se produce una excepción. Este es probablemente el mejor uso que he visto para esto (es decir, "trabajo" en el constructor), aunque todavía no estoy seguro de considerarlo "bueno". En este caso, es bastante autoexplicativo, 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 se puede serializar o pasar para su uso posterior, lo cual es inmensamente útil. Quizás sus contratistas intentaron algo similar, aunque lo dudo mucho.

Comentarios sobre tu ejemplo:

Yo diría que es un antipatró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 de hecho, aunque supongo 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 en transición a C ++. Aparentemente, habían recibido capacitación y la gerencia les dijo que tenían que hacer cosas "orientadas a objetos". Presentado para descubrir qué está mal, el autor usó una herramienta de métrica de código y descubrió 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 me he encontrado con 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 comprender realmente cómo hacerlo y por qué.


1
Su buen ejemplo es una instancia de Asignación de recursos es Inicialización . Es un patrón recomendado en C ++ porque hace que sea mucho más fácil escribir código seguro para excepciones. Sin embargo, no sé si eso se traslada a C #. (En este caso, el "recurso" que se está asignando son privilegios administrativos.)
zwol

@Zack Nunca lo había pensado en esos términos, aunque he tenido conocimiento de RAII en C ++. En el mundo C #, se conoce como el patrón desechable, y es muy recomendable para cualquier cosa que tenga recursos (generalmente no administrados) para liberarse después de su vida útil. La principal diferencia con este ejemplo es que normalmente no se hacen operadores caros en el constructor. Por ejemplo, un método Open () de SqlConnection todavía necesita ser llamado después del constructor.
Daniel B

14

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 usando 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 ninguna instancia de objeto superflua.

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


7

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 propiedades, y luego nunca hace nada más. Esa es la inmutabilidad normal en acción.

Creo que lo extraño aquí es poner código con efectos secundarios en un constructor. Construir un objeto y tirarlo 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 inyectara una instancia de la clase, luego se hiciera que el bucle for llame al método repetidamente.


4

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

No.

  • Un constructor no debería tener ningún efecto secundario.
    • Cualquier cosa más que la inicialización de 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".

  • Excepciones y errores dentro de un constructor son muy inesperados. A menos que se les indique que lo hagan, los futuros programadores no estarán inclinados a rodear estas llamadas de constructor con código defensivo.
  • Si algo falla en la producción, el seguimiento de la pila generada 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 pila .NET donde este fue el caso.

1
"A menos que se les diga que lo hagan, los futuros programadores no estarán inclinados a rodear estas llamadas de constructores con código defensivo". - Buen punto.
Graham

2

Me parece que la persona que estaba haciendo esto intentaba hackear 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 similar applyo similar.

Así que supongo que solo usó un atajo y utilizó directamente una clase como reemplazo de una función. Cada vez que desee ejecutar la función, solo crea una instancia de la clase.

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


3
Estoy bastante seguro de que este programador en particular nunca ha escuchado la frase 'programación funcional'.
Kane

No creo que el programador intentara crear una abstracción de cierre. El equivalente de Java requeriría un método abstracto (planificación para posible) polimorfismo. No hay evidencia de eso aquí.
Kevin A. Naudé

1

Para mí, la regla general es no hacer nada en el constructor, excepto inicializar las variables miembro. La primera razón de esto es que ayuda a seguir el principio SRP, ya que generalmente un proceso de inicialización prolongado es una indicación de que la clase hace más de lo que debería hacer, y la inicialización debe hacerse 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 crea 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.


1

Voy a ir contra la corriente aquí: en idiomas donde todo tiene que estar en alguna clase Y no puedes tener una clase estática, puedes toparte con la situación en la que tienes una funcionalidad aislada que debe ser poner en algún lugar y no encaja con nada más. Sus opciones son una clase de utilidad que cubre varios bits de funcionalidad no relacionados, o una clase que básicamente hace solo esto.

Si tiene solo 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 se vuelvan a usar o enhebrar. Si tiene un constructor / método, puede sentirse tentado a dejar que los parámetros pasen al método único, pero si usa campos o propiedades privadas que introducen posibles problemas de subprocesos.

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

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

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


1

Hay patrones comunes en lenguajes donde las funciones y las clases son mucho más lo mismo, 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 se puede hacer en el constructor, ya que el objeto en sí mismo es solo un envoltorio alrededor de un solo paquete de trabajo, y no hay una buena razón para incluirlo en una función separada. 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 directamente los efectos secundarios, puede llamar al objeto para que aplique el efecto secundario en el momento justo, 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 que afectaré gravemente y le haré daño. 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 realizar los múltiples valores de retorno a través de los accesos en el objeto. Todo el trabajo está hecho, pero quiero que todo esté en contexto, y realmente no quiero pasar múltiples 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.


Necesito aclarar que el ejemplo dado sigue siendo engañoso. En el ejemplo sin accesores y sin valor de retorno, esto es probablemente un vestigio. Quizás el original proporcionó devoluciones de depuración o algo así.
Jon Jay Obermark

1

Se me ocurre un caso en el que el constructor / destructor hace todo el trabajo:

Cabellos. Normalmente nunca interactúa con un candado durante su vida útil. El uso de la arquitectura de C # es bueno para manejar estos casos sin referirse explícitamente 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 de constructor para parchear un error de biblioteca en tiempo de ejecución. (En realidad, arreglar el código habría causado otros problemas). No hubo destructor porque no había necesidad de deshacer el parche.


-1

Estoy de acuerdo con AndyBursh. Parece una cuestión de falta de conocimiento.

Sin embargo, es importante mencionar marcos como NHibernate que requieren que solo 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 a un método de una clase dinámicamente.


-4

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

Sí, quizás recuerdes 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 a 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 tiene valor y lo único que importa es la productividad del programador. Para la productividad, debe escribir código manejable, lo que significa modularización (es decir, reducir el 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 las personas acordaron que preferirían declarar la matriz lo más estrecha posible.

El programador moderno de Java 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 moderno de programación empresarial es, cuando necesita ejecutar una acción, crea una clase especial (o incluso mejor, un marco) que ejecutará esa acción simple. Buenos IDEs de Java, que ayudan aquí. Generan toneladas de basura automáticamente, como la respiración.

Entonces, creo que sus objetos carecen del patrón Builder o Factory. No es decente crear instancias directamente, por constructor. Se recomienda una clase de fábrica especial para cada objeto.

Ahora, cuando entra en juego la programación funcional, la creación de objetos se vuelve aún más simple. Creará objetos a cada paso como respiración, sin siquiera darse cuenta de esto. Entonces, espero que su código sea 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. Lo considero un método más. El código de factorización es necesario solo cuando puede compartirlo.


3
La explicación no está bien estructurada y es difícil de seguir. Simplemente recorres el lugar con afirmaciones discutibles que no tienen vínculos con el contexto.
Nueva Alejandría

La afirmación realmente discutible es "Los constructores deben instanciar los campos de un objeto y hacer cualquier otra inicialización necesaria para que el objeto esté listo para usar". Díselo al colaborador más popular.
Val
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.