2010-03-08 12 views
27

Duplicar posibles:
How much work should be done in a constructor?¿Debería un constructor C++ hacer un trabajo real?

estoy strugging con algunos consejos que tengo en el fondo de mi mente, pero para los que no puedo recordar el razonamiento.

Creo recordar en algún momento que leí algunos consejos (no recuerdo la fuente) de que los constructores de C++ no deberían hacer un trabajo real. Por el contrario, deberían inicializar solo las variables. El consejo continuó explicando que el trabajo real debería hacerse en algún tipo de método init(), que se llamará por separado después de que se haya creado la instancia.

La situación es que tengo una clase que representa un dispositivo de hardware. Tiene sentido lógico para mí que el constructor llame a las rutinas que consultan el dispositivo para construir las variables de instancia que describen el dispositivo. En otras palabras, una vez que la instancia nueva crea el objeto, el desarrollador recibe un objeto que está listo para ser utilizado, no se requiere una llamada por separado a object-> init().

¿Hay alguna buena razón por la cual los constructores no deberían hacer un trabajo real? Obviamente, podría retrasar el tiempo de asignación, pero eso no sería diferente si llama a un método por separado inmediatamente después de la asignación.

Estoy tratando de descubrir qué problemas que actualmente no considero que podrían haber llevado a tal consejo.

+3

Posible candidato para la fusión. – dmckee

Respuesta

26

Recuerdo que Scott Meyers en Más Efectivo C++ recomienda no tener un constructor predeterminado superfluo. En ese artículo, también abordó el uso de métodos como Init() para 'crear' los objetos. Básicamente, ha introducido un paso adicional que asigna la responsabilidad al cliente de la clase.Además, si desea crear una matriz de dichos objetos, cada uno de ellos debería llamar manualmente a Init(). Puede tener una función Init que el constructor puede llamar para mantener el código ordenado, o para que el objeto llame si implementa un Reset(), pero a partir de las experiencias es mejor eliminar un objeto y recrearlo en lugar de intentar restablecerlo sus valores predeterminados, a menos que los objetos se creen y destruyan muchas veces en tiempo real (por ejemplo, efectos de partículas).

Además, tenga en cuenta que los constructores pueden realizar listas de inicialización que las funciones normales no podrían.

Una de las razones por las que se debe advertir contra el uso de constructores para hacer una gran asignación de recursos es que puede ser difícil detectar excepciones en los constructores. Sin embargo, hay formas de evitarlo. De lo contrario, creo que los constructores deben hacer lo que se supone que deben hacer: preparar un objeto para su estado inicial de ejecución (lo importante para la creación de objetos es la asignación de recursos).

+24

Ese sería Scott Meyers - Sid Meier es el chico de Civilization :) –

+0

Oops. Hice la edición. – Extrakun

9

Depende de lo que quiere decir con trabajo real. El constructor debe poner el objeto en un estado utilizable, incluso si ese estado es una bandera, lo que significa que aún no se ha inicializado :-)

La única razón que me he encontrado para no hacer un trabajo real sería la hecho de que la única forma en que un constructor puede fallar es con una excepción (y no se llamará al destructor en ese caso). No hay oportunidad de devolver un buen código de error.

La pregunta que hay que preguntarse es:

es el objeto utilizable sin llamar al método init?

Si la respuesta es "No", estaría haciendo todo ese trabajo en el constructor. De lo contrario, tendrá que detectar la situación cuando un usuario haya creado una instancia pero aún no se haya inicializado y devuelva algún tipo de error.

Por supuesto, si se puede re-inicializar el dispositivo, se debe proporcionar algún tipo de init método, pero, en ese caso, me gustaría todavía llamada de ese método desde el constructor si anteriormente se cumple la condición.

18

La única razón para no hacer "trabajo" en el constructor es que si se lanza una excepción desde allí, no se llamará al destructor de clase. Pero si usa los principios RAII y no confía en su destructor para hacer el trabajo de limpieza, entonces creo que es mejor no introducir un método que no sea necesario.

+8

Hubiera agregado que incluso si un constructor arroja cada variable miembro completamente construida tendrá su derstructor llamado (por lo tanto, si usted está en el bloque de código del constructor todos los miembros serán destruidos correctamente). –

+0

¿Cómo se hace la limpieza en su constructor una vez que se lanza una excepción? (Digamos que asignó un 'FILE *' por ejemplo) ** no confíe en su destructor ** pero todo lo que puede hacer es dejar que la pila se desenrolle ... –

+0

Uno de los miembros de su clase sería un objeto de tipo RAII que cerrará el archivo * en su destructor. Algo así como la clase FileCloser de esto: http://tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/ – Warpin

3

La única razón real es Testability. Si sus constructores están llenos de "trabajo real", eso significa que los objetos solo se pueden crear instancias dentro de una aplicación en ejecución totalmente inicializada. Es un signo de que el objeto/clase necesita más descomposición.

+0

El argumento de la capacidad de prueba es bueno. Pero probablemente pueda ser manejado por objetos falsos en la mayoría de los casos – neuro

+1

Si el constructor crea sus propios objetos, entonces no puede configurar el objeto con objetos simulados a menos que escriba un nuevo constructor. Al separar la responsabilidad de hacer que un objeto complejo establezca sus variables miembro, se obtiene la costura requerida. – mabraham

6

Además de las otras sugerencias relacionadas con el manejo de excepciones, una cosa a considerar cuando se conecta a un dispositivo de hardware es cómo su clase manejará la situación donde un dispositivo no está presente o la comunicación falla.

En caso de que no pueda comunicarse con el dispositivo, es posible que deba proporcionar algunos métodos en su clase para realizar una inicialización posterior de todos modos. En ese caso, puede tener más sentido simplemente crear una instancia del objeto y luego ejecutar una llamada de inicialización. Si falla la inicialización, puede mantener el objeto e intentar reiniciar la comunicación nuevamente más adelante. O puede necesitar manejar la situación donde la comunicación se pierde después de la inicialización. En cualquier caso, es probable que desee pensar en cómo diseñará la clase para manejar los problemas de comunicación en general y eso puede ayudarlo a decidir qué desea hacer en el constructor versus un método de inicialización.

Cuando implementé clases que se comunican con hardware externo, me pareció más fácil crear una instancia de un objeto "desconectado" y proporcionar métodos para conectar y configurar el estado inicial. Esto generalmente proporciona más flexibilidad para conectar/desconectar/reconectar con el dispositivo.

+0

¡Sí! La mayoría de la gente no parece entender esto: poner el trabajo en un constructor vincula implícitamente el éxito a la duración del objeto, lo que no funciona bien para objetos desconectables (como un cliente TCP "persistente"). – Tom

+0

Estoy de acuerdo. Uso una interfaz/paradigma open/close/isOpen para marcar/manejar ese tipo de objetos – neuro

3

Al utilizar un constructor y un método Init(), tiene una fuente de error. En mi experiencia, encontrarás situaciones en las que alguien olvida llamarlas, y es posible que tengas un error sutil en tus manos. Diría que no debería hacer mucho trabajo en su constructor, pero si se necesita algún método init, entonces tiene un escenario de construcción no trivial, y ya es hora de mirar los patrones creacionales. Una función de constructor o una fábrica sean prudentes para echar un vistazo. Con un constructor privado, asegúrese de que nadie, excepto su fábrica o su función de constructor, realmente construya los objetos, por lo que puede estar seguro de que siempre se construye correctamente.

Si su diseño permite errores en la implementación, alguien cometerá esos errores. Mi amigo Murphy me dijo eso;)

En mi campo trabajamos con montones de situaciones similares relacionadas con el hardware. Las fábricas nos ofrecen comprobabilidad, seguridad y mejores formas de fallar la construcción.

2

Vale la pena considerar los problemas de por vida y la conexión/reconexión, como señala Neal S.

Si no puede conectarse a un dispositivo en el otro extremo de un enlace, a menudo es el caso que el "dispositivo" en su extremo es utilizable y será más tarde si el otro extremo actúa conjuntamente. Ejemplos de conexiones de red, etc.

Por otro lado, si usted trata de acceder a algún dispositivo de hardware local que no existe y nunca existirá en el marco de su programa (por ejemplo, una tarjeta gráfica que no está presente), entonces creo que este es un caso en el que quieres saber esto en el constructor, para que el constructor pueda lanzar y el objeto no pueda existir. Si no lo hace, puede terminar con un objeto que no es válido y siempre lo será. Al lanzar el constructor significa que el objeto no existirá y, por lo tanto, no se pueden invocar funciones sobre ese objeto. Obviamente, usted necesita estar al tanto de los problemas de limpieza si agrega un constructor, pero si no lo hace en casos como este, normalmente termina con verificaciones de validación en todas las funciones que pueden ser llamadas.

Así que creo que usted debe hacer lo suficiente en el constructor para asegurarse de tener una, utilizable, objeto válido creado.

1

Me gustaría agregar mi propia experiencia allí.

No diré mucho sobre el debate tradicional Constructor/Init ... por ejemplo Google directrices desaconsejan cualquier cosa en el Constructor pero eso es porque desaconsejan las Excepciones y los 2 trabajan juntos.

puedo hablar de una clase Connection utilizo sin embargo.

Cuando se crea la clase Connection, se intentará conectarse realmente (al menos, si no está construido por defecto). Si el Connection falla ... el objeto aún está construido y usted no lo sabe.

Cuando intenta utilizar la clase Connection usted es así en uno de los 3 casos:

  • ningún parámetro nunca se ha precisado> excepción o error de código
  • el objeto está realmente conectado> bien
  • el objeto no está conectado, se intentará conectarse> esto tiene éxito, bien, esto falla, se obtiene una excepción o un código de error

Creo que es bastante útil para tener ambos. Sin embargo, significa que en todos los métodos que usan la conexión, debes probar si funciona o no.

Vale la pena aunque debido a los acontecimientos de desconexión. Cuando está conectado, puede perder la conexión sin que el objeto lo sepa. Al encapsular el autocontrol de la conexión en un método reconnect llamado internamente por todos los métodos que necesitan una conexión funcional, realmente aíslas a los desarrolladores para que no se ocupen de los problemas ... o al menos tanto como puedas, ya que cuando todo falla, tienes no hay otra solución que haciéndoles saber :)

0

Haciendo "trabajo real" en un constructor es mejor evitarlo.

Si las conexiones de base de datos de configuración que los archivos abiertos, etc dentro de un constructor y en caso de hacerlo, una de ellas plantean una excepción a continuación, que daría lugar a una pérdida de memoria. Esto comprometerá el exception safety de su aplicación.

Otra razón para evitar hacer el trabajo en un constructor es que haría que su aplicación menos comprobable. Supongamos que está escribiendo un procesador de pagos con tarjeta de crédito. Si dices en el constructor de la clase CreditCardProcessor, haces todo el trabajo de conectarte a una pasarela de pago, autenticar y facturar la tarjeta de crédito, ¿cómo escribo las pruebas unitarias para la clase CreditCardProcessor?

Llegando a su escenario, si las rutinas que consultan el dispositivo no generan excepciones y no va a probar la clase aisladamente, entonces es probablemente preferible trabajar en el constructor y evitar llamadas a ese extra init método.

0

Hay un par de razones por las que usaría separada constructor/init():

  • Lazy/inicialización retardada. Esto le permite crear el objeto rápidamente, una respuesta rápida del usuario y demorar una inicialización más prolongada para un procesamiento posterior o en segundo plano. Esto también forma parte de uno o más patrones de diseño relacionados con grupos de objetos reutilizables para evitar costosas asignaciones.
  • No estoy seguro de si tiene un nombre propio, pero tal vez cuando se crea el objeto, la información de inicialización no está disponible o no la entiende quien crea el objeto (por ejemplo, creación de objetos genéricos masivos). Otra sección de código tiene los conocimientos para inicializarla, pero no para crearla.
  • Como una razón personal, el destructor debería poder deshacer todo lo que hizo el constructor. Si eso implica el uso de init/deinit interno(), no hay problema, siempre que sean imágenes especulares entre sí.
+0

Estos objetivos se logran de forma más segura al crear un objeto de un tipo cuya responsabilidad es crear el tipo real más adelante. Lo importante es que cuando tienes un objeto del tipo real, sabes que está en un estado seguro de usar. En lugar de "construir una Cosa, entonces llama a su método init(), entonces siempre tienes que verificar que llames a init()" tienes "construir el ThingBuilder, llamar a su método createThing(), y luego usar lo tuyo" – mabraham

Cuestiones relacionadas