cuán complejo debe ser un constructor

18

Estoy teniendo una discusión con mi compañero de trabajo sobre cuánto trabajo puede hacer un constructor. Tengo una clase, B que requiere internamente otro objeto A. El objeto A es uno de los pocos miembros que la clase B necesita para hacer su trabajo. Todos sus métodos públicos dependen del objeto interno A. La información sobre el objeto A se almacena en la base de datos, así que trato de validarla y obtenerla buscándola en la base de datos en el constructor. Mi compañero de trabajo señaló que el constructor no debería hacer mucho trabajo más que capturar los parámetros del constructor. Dado que todos los métodos públicos fallarían de todos modos si el objeto A no se encuentra usando las entradas al constructor, argumentaba que en lugar de permitir que se cree una instancia y luego falle, en realidad es mejor lanzar temprano en el constructor.

¿Qué piensan los demás? Estoy usando C # si eso hace alguna diferencia.

Lectura ¿Hay alguna razón para hacer todo el trabajo de un objeto en un constructor? Me pregunto si buscar el objeto A yendo a DB es parte de "cualquier otra inicialización necesaria para que el objeto esté listo para usar" porque si el usuario pasara valores incorrectos al constructor, no podría usar ninguno de sus métodos públicos.

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.

Taein Kim
fuente
8
Estoy en desacuerdo. Eche un vistazo al principio de inversión de dependencia, sería mejor si el objeto A se entrega al objeto B en un estado válido en su constructor.
Jimmy Hoffa
55
¿Estás preguntando cuánto se puede hacer? ¿Cuánto se debe hacer? ¿O cuánto de su situación específica debe hacerse? Son tres preguntas muy diferentes.
Bobson el
1
Estoy de acuerdo con la sugerencia de Jimmy Hoffa de mirar la inyección de dependencia (o "inversión de dependencia"). Ese fue mi primer pensamiento al leer la descripción, porque parece que ya estás a medio camino; ya tiene una funcionalidad separada en la clase A, que la clase B está utilizando. No puedo hablar de su caso, pero generalmente encuentro que el código de refactorización para usar el patrón de inyección de dependencia hace que las clases sean más simples / menos entrelazadas.
Aprendiz del Dr. Wily
1
Bobson, cambié el título a 'debería'. Estoy pidiendo mejores prácticas aquí.
Taein Kim
1
@JimmyHoffa: tal vez deberías expandir tu comentario en una respuesta.
Kevin Cline

Respuestas:

22

Su pregunta se compone de dos partes completamente separadas:

¿Debería lanzar una excepción desde el constructor, o debería hacer que el método falle?

Esta es claramente la aplicación del principio de falla rápida . Hacer que el constructor falle es mucho más fácil de depurar en comparación con tener que encontrar por qué falla el método. Por ejemplo, puede obtener la instancia ya creada de alguna otra parte del código y obtener errores al llamar a los métodos. ¿Es obvio que el objeto se creó mal? No.

En cuanto al problema de "envolver la llamada en try / catch". Las excepciones están destinadas a ser excepcionales . Si sabe que algún código arrojará una excepción, no ajusta el código en try / catch, pero valida los parámetros de ese código antes de ejecutar el código que puede arrojar una excepción. Las excepciones son solo una forma de garantizar que el sistema no entre en un estado no válido. Si sabe que algunos parámetros de entrada pueden conducir a un estado no válido, asegúrese de que esos parámetros nunca sucedan. De esta manera, solo tiene que hacer try / catch en lugares, donde puede manejar lógicamente la excepción, que generalmente está en el límite del sistema.

¿Puedo acceder a "otras partes del sistema, como DB" desde el constructor.

Creo que esto va en contra del principio de menor asombro . No mucha gente esperaría que el constructor acceda a un DB. Entonces no, no deberías hacer eso.

Eufórico
fuente
2
La solución más apropiada para esto es generalmente agregar un método de tipo "abierto" donde la construcción es libre, pero no se puede hacer ningún uso antes de "abrir" / "conectar" / etc, que es donde ocurre toda la inicialización real. La falla de los constructores puede causar todo tipo de problemas cuando en el futuro quieras hacer una construcción parcial de objetos, lo cual es una habilidad útil.
Jimmy Hoffa
@JimmyHoffa No veo diferencia entre el método del constructor y el método específico para la construcción.
Eufórico
44
Puede que no, pero muchos lo hacen. Piense en cuando crea un objeto de conexión, ¿el constructor lo conecta? ¿Se puede usar el objeto si no está conectado? En ambas preguntas, la respuesta es no, porque es útil tener objetos de conexión parcialmente construidos para que pueda ajustar la configuración y otras cosas antes de abrir la conexión. Este es un enfoque común para este escenario. Todavía creo que la inyección del constructor es el enfoque correcto para escenarios en los que el objeto A tiene una dependencia de recursos, pero un método abierto sigue siendo mejor que hacer que el constructor haga el trabajo peligroso.
Jimmy Hoffa
@JimmyHoffa Pero ese es un requisito específico de la clase de conexión, no una regla general del diseño de OOP. Realmente lo llamaría un caso excepcional, que una regla. Básicamente estás hablando del patrón de construcción.
Eufórico
2
No es un requisito, fue una decisión de diseño. Podrían haber hecho que el constructor tome una cadena de conexión y se conecte inmediatamente cuando se construye. Decidieron no también porque es útil poder crear el objeto, luego ajustar la cadena de conexión u otros bits y bobbles y conectarlo más tarde ,
Jimmy Hoffa
19

Ugh

Un constructor debe hacer lo menos posible; el problema es que el comportamiento de excepción en los constructores es incómodo. Muy pocos programadores saben cuál es el comportamiento adecuado (incluidos los escenarios de herencia), y obligar a sus usuarios a intentar / capturar cada instancia es ... doloroso en el mejor de los casos.

Hay dos partes en esto, en el constructor y usando el constructor. Es incómodo en el constructor porque no puedes hacer mucho allí cuando ocurre una excepción. No puede devolver un tipo diferente. No puedes devolver nulo. Básicamente, puede volver a lanzar la excepción, devolver un objeto roto (constuctor defectuoso) o (en el mejor de los casos) reemplazar alguna parte interna con un valor predeterminado sensato. Usar el constructor es incómodo porque tus instancias (¡y las instancias de todos los tipos derivados!) Pueden arrojarse. Entonces no puede usarlos en inicializadores de miembros. Terminas usando excepciones para que la lógica vea "¿tuvo éxito mi creación?", Más allá de la legibilidad (y fragilidad) causada por try / catch en todas partes.

Si su constructor está haciendo cosas no triviales, o cosas que razonablemente pueden fallar, considere un Createmétodo estático (con un constructor no público). Es mucho más utilizable, más fácil de depurar, y aún así te ofrece una instancia completamente construida cuando tienes éxito.

Telastyn
fuente
2
Los escenarios de construcción complejos son exactamente la razón por la cual existen patrones de creación como usted menciona aquí. Este es un buen enfoque para estas construcciones problemáticas. Me da que pensar cómo en .NET un Connectionobjeto puede CreateCommandpara usted para que sepa que el comando está conectado adecuadamente.
Jimmy Hoffa
2
A esto agregaría que una base de datos es una gran dependencia externa, supongamos que desea cambiar las bases de datos en algún momento en el futuro (o simular un objeto para probar), moviendo este tipo de código a una clase Factory (o algo así como un IService proveedor) parece un enfoque más limpio
Jason Sperske
44
¿Puedes explicar "el comportamiento de excepción en los constructores es incómodo"? Si usara Create, ¿no necesitaría envolverlo en try catch al igual que envolvería call to constructor? Siento que Create es solo un nombre diferente para constructor. Tal vez no estoy recibiendo tu respuesta ¿verdad?
Taein Kim
1
Habiendo tenido que intentar detectar excepciones que brotan de los constructores, +1 a los argumentos en contra de esto. Sin embargo, después de haber trabajado con personas que dicen "No trabajar en el constructor", esto también puede crear algunos patrones extraños. He visto código donde cada objeto no solo tiene un constructor, sino también alguna forma de Initialize () donde, ¿adivina qué? El objeto no es realmente válido hasta que se llame tampoco. Y luego tienes dos cosas a las que llamar: ctor e Initialize (). El problema de hacer el trabajo para configurar un objeto no desaparece: C # puede proporcionar soluciones diferentes a las de C ++. ¡Las fábricas son buenas!
J Trana
1
@jtrana: sí, inicializar no es bueno. El constructor se inicializa con algún método por defecto o una fábrica o el método de creación lo construye y devuelve una instancia utilizable.
Telastyn
1

Constructor: el equilibrio de la complejidad del método es, de hecho, una cuestión de discusión sobre un buen estilo. Muy a menudo Class() {}se usa un constructor vacío , con un método Init(..) {..}para cosas más complicadas. Entre los argumentos a tener en cuenta:

  • Posibilidad de serialización de clase
  • Con el método Init de separación del constructor, a menudo necesita repetir la misma secuencia Class x = new Class(); x.Init(..);muchas veces, parcialmente en Pruebas unitarias. No tan bueno, sin embargo, claro como el cristal para leer. A veces, simplemente los pongo en una línea de código.
  • Cuando el objetivo de la Prueba de Unidad es solo la prueba de inicialización de Clase, es mejor tener una Inicia propia, para realizar acciones más complicadas, una por una, con los Activos intermedios.
Pavel Khrapkin
fuente
La ventaja, en mi opinión, del initmétodo es que manejar las fallas es mucho más fácil. En particular, el valor de retorno del initmétodo puede indicar si la inicialización es exitosa y el método puede garantizar que el objeto esté siempre en buen estado.
pqnet