2012-03-19 12 views
7

Actualmente estoy a cargo de encontrar todas las malas prácticas en nuestra base de códigos y convencer a mis colegas para corregir el código ofensivo. Durante mi espeleología, he notado que muchas personas aquí utilizan el siguiente patrón:Argumentos en contra del método "initialize()" en lugar de los constructores

class Foo 
{ 
    public: 
    Foo() { /* Do nothing here */ } 
    bool initialize() { /* Do all the initialization stuff and return true on success. */ } 
    ~Foo() { /* Do all the cleanup */ } 
}; 

Ahora puedo estar equivocado, pero para mí esta cosa initialize() método es horrible. Creo que cancela todo el propósito de tener constructores.

Cuando pregunto a mis colegas por qué se tomó esta decisión de diseño, siempre responder que no tienen otra opción porque no se puede salir de un constructor sin lanzar (supongo que asumen que lanza es siempre malo).

No logré convencerlos hasta el momento y admito que puedo carecer de argumentos valiosos ... así que aquí está mi pregunta: ¿Tengo razón en que este constructo es un dolor? Si es así, ¿qué problemas ves en él? ?

Gracias.

+1

FTR, esto se conoce como la inicialización de dos fases. –

+1

aquí hay una pregunta obvia: si la construcción falla, * ¿qué quieren hacer sus compañeros de trabajo con el objeto construido a medio camino *? Es inutil. Bien podría haber arrojado una excepción en la construcción – jalf

+0

@jalf: Buen punto de hecho. – ereOn

Respuesta

8

La inicialización de un solo paso (constructor) y la inicialización de dos pasos (con un método init) son patrones útiles. Personalmente creo que excluir cualquiera es un error, aunque si sus convenciones prohíben el uso de excepciones por completo, entonces prohíbe la inicialización en un solo paso para los constructores que pueden fallar.

En general, prefiero la inicialización en un solo paso porque esto significa que sus objetos pueden tener invariantes más fuertes.Solo utilizo la inicialización en dos pasos cuando considero que es significativo o útil que un objeto pueda existir en un estado "no inicializado".

Con la inicialización en dos pasos, es válido para su objeto en un estado no inicializado, por lo que cada método que funcione con el objeto debe tener en cuenta y manejar correctamente el hecho de que podría estar en un estado no inicializado. Esto es análogo a trabajar con punteros, donde es mala forma suponer que un puntero no es NULO. Por el contrario, si realiza toda su inicialización en su constructor y falla con excepciones, puede agregar 'el objeto siempre se inicializa' a su lista de invariantes, y por lo tanto, es más fácil y seguro hacer suposiciones sobre el estado del objeto .

2

Esto se conoce generalmente como Inicialización de dos fases o multifase y es particularmente malo porque una vez que una llamada al constructor ha finalizado correctamente, debe tener un objeto listo para usar, en este caso no tendrá un Ready-to- usar objeto.

No puedo evitar insistir más en lo siguiente:
Lanzar una excepción desde el constructor en caso de falla es la mejor y la única manera concisa de manejar fallas de construcción de objetos.

+0

Esta debería ser la respuesta aceptada. – maxschlepzig

0

Depende del caso.

Si un constructor puede fallar debido a algunos argumentos, se debe lanzar una excepción. Pero, por supuesto, debe documentarse al arrojar excepciones de los constructores.

Si Foo contiene objetos, que serán inicializados dos veces, una vez en el constructor, una vez en el método initialize, por lo que es un inconveniente.

IMO, el mayor inconveniente es que debe recordar llamar al initialize. ¿De qué sirve crear un objeto si no es válido?

Por lo tanto, si su único argumento es que no desean lanzar excepciones desde el constructor, es un argumento bastante malo.

Sin embargo, si desean algún tipo de inicialización diferida, es válido.

0

Es un problema, pero no tiene otra opción si quiere evitar lanzar una excepción desde el constructor. También hay otra opción, igualmente dolorosa: hacer toda la inicialización en el constructor y luego debe verificar si el objeto se ha construido con éxito (por ejemplo, operador de conversión a bool o método IsOK). La vida es difícil, ..... y luego mueres :(

+0

Pero si un objeto no se pudo inicializar (y, por lo tanto, no se pudo construir), ¿qué usos tiene de todos modos? Para mí, el método 'IsOK' es igualmente erróneo. – ereOn

+0

La idea es que en el constructor solo estableces todos los miembros a los valores iniciales (para que todos estén en estado definido), luego invocas 'initialize' que puede (potencialmente) arrojar una excepción. Justo después del constructor, el 'IsOK' (o operador bool) devuelve falso. El objeto siempre se puede inicializar a algunos valores iniciales (conocidos). No estoy argumentando que sea una buena idea, solo a veces es la única alternativa sensata. Por cierto: si quieres ver una excepción lanzada desde el constructor en la lista de inicialización, entonces la sintaxis se vuelve realmente fea, de ambos males escogería el IsOK/oper_bool. – sirgeorge

+0

¿Pero qué representa el objeto después de que se construyó pero antes de que llame 'initialize()'? – ereOn

1

Depende de la semántica de su objeto. Si la inicialización es algo crucial para la estructura de datos de la clase en sí, una falla se manejaría mejor lanzando una excepción desde el constructor (por ejemplo, si no tiene memoria), o mediante una afirmación (si sabe que su código no debería fallar, nunca).

Por otro lado, si el éxito o no de la construcción depende de la entrada del usuario, entonces la falla no es una condición excepcional, sino más bien parte del comportamiento de tiempo de ejecución esperado normal que necesita probar. En ese caso, debe tener un constructor predeterminado que crea un objeto en un estado "no válido" y una función de inicialización que puede invocarse en un constructor o posterior y que puede o no tener éxito. Tome std::ifstream como ejemplo.

Así que un esqueleto de su clase podría tener este aspecto:

class Foo 
{ 
    bool valid; 
    bool initialize(Args... args) { /* ... */ } 

public: 
    Foo() : valid(false) { } 
    Foo(Args... args) : valid (false) { valid = initialize(args...); } 

    bool reset(Args... args) // atomic, doesn't change *this on failure 
    { 
     Foo other(args...); 
     if (other) { using std::swap; swap(*this, other); return true; } 
     return false; 
    } 

    explicit operator bool() const { return valid; } 
}; 
Cuestiones relacionadas