El constructor generalmente no debe llamar a métodos

12

Le describí a un colega por qué un constructor que llama a un método puede ser un antipatrón.

ejemplo (en mi oxidado C ++)

class C {
public :
    C(int foo);
    void setFoo(int foo);
private:
    int foo;
}

C::C(int foo) {
    setFoo(foo);
}

void C::setFoo(int foo) {
    this->foo = foo
}

Me gustaría motivar mejor este hecho a través de su contribución adicional. Si tiene ejemplos, referencias de libros, páginas de blogs o nombres de principios, serían muy bienvenidos.

Editar: estoy hablando en general, pero estamos codificando en Python.

Stefano Borini
fuente
¿Es esta una regla general o específica de idiomas particulares?
ChrisF
¿Cual idioma? En C ++ es más que un antipatrón: parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5
LennyProgrammers
@ Lenny222, el OP habla de "métodos de clase", que, al menos para mí, significa métodos que no son de instancia . Que por lo tanto no puede ser virtual.
Péter Török
3
@Alb En Java está perfectamente bien. Sin embargo, lo que no debe hacer es pasar explícitamente thisa cualquiera de los métodos que llame desde el constructor.
biziclop
3
@Stefano Borini: Si está codificando en Python, ¿por qué no muestra el ejemplo en Python en lugar de C ++ oxidado? Además, explique por qué esto es algo malo. Lo hacemos todo el tiempo.
S.Lott

Respuestas:

26

No ha especificado un idioma.

En C ++, un constructor debe tener cuidado al llamar a una función virtual, ya que la función real a la que llama es la implementación de la clase. Si es un método virtual puro sin una implementación, esto será una violación de acceso.

Un constructor puede llamar a funciones no virtuales.

Si su lenguaje es Java, donde las funciones son generalmente virtuales de manera predeterminada, tiene sentido que tenga que tener mucho cuidado.

C # parece manejar la situación de la manera que cabría esperar: puede llamar a métodos virtuales en constructores y llama a la versión más final. Entonces, en C # no es un antipatrón.

Una razón común para llamar a métodos desde constructores es que tiene múltiples constructores que desean llamar a un método "init" común.

Tenga en cuenta que los destructores tendrán el mismo problema con los métodos virtuales, por lo tanto, no puede tener un método de "limpieza" virtual que se encuentre fuera de su destructor y espere que sea llamado por el destructor de clase base.

Java y C # no tienen destructores, tienen finalizadores. No sé el comportamiento con Java.

C # parece manejar la limpieza correctamente a este respecto.

(Tenga en cuenta que aunque Java y C # tienen recolección de basura, eso solo administra la asignación de memoria. Hay otra limpieza que su destructor necesita hacer que no libera memoria).

CashCow
fuente
13
Hay algunos pequeños errores aquí. Los métodos en C # no son virtuales de manera predeterminada. C # tiene una semántica diferente a C ++ cuando se llama a un método virtual en un constructor; Se llamará al método virtual en el tipo más derivado, no al método virtual en la parte del tipo que se está construyendo actualmente. C # llama a sus métodos de finalización "destructores", pero tiene razón en que tienen la semántica de los finalizadores. Los métodos virtuales llamados destructores en C # funcionan de la misma manera que en los constructores; Se llama el método más derivado.
Eric Lippert
@ Péter: pretendía métodos de instancia. perdón por la confusion.
Stefano Borini
1
@Eric Lippert. Gracias por su experiencia en C #, he editado mi respuesta en consecuencia. No conozco ese lenguaje, conozco muy bien C ++ y Java menos.
CashCow
55
De nada. Tenga en cuenta que llamar a un método virtual en un constructor de clase base en C # sigue siendo una muy mala idea.
Eric Lippert
Si llama a un método (virtual) en Java desde un constructor, siempre invocará la anulación más derivada. Sin embargo, lo que llamas "de la manera que esperarías" es lo que yo llamaría confuso. Porque si bien Java invoca la anulación más derivada, ese método solo verá procesados ​​los inicializadores archivados pero no el constructor de su propia ejecución de clase. Invocar un método en una clase que aún no tiene su invariante establecido puede ser peligroso. Así que creo que C ++ hizo la mejor elección aquí.
5gon12eder
18

Bien, ahora que la confusión con respecto a los métodos de clase frente a los métodos de instancia está aclarada, puedo dar una respuesta :-)

El problema no es llamar a métodos de instancia en general desde un constructor; es con la llamada a métodos virtuales (directa o indirectamente). Y la razón principal es que mientras está dentro del constructor, el objeto aún no está completamente construido . Y especialmente sus partes de subclase no se construyen en absoluto mientras se ejecuta el constructor de la clase base. Por lo tanto, su estado interno es inconsistente en una forma dependiente del idioma, y ​​esto puede causar diferentes errores sutiles en diferentes idiomas.

C ++ y C # ya han sido discutidos por otros. En Java, se llamará al método virtual del tipo más derivado, sin embargo, ese tipo aún no se ha inicializado. Por lo tanto, si ese método usa algún campo del tipo derivado, es posible que esos campos aún no se inicialicen correctamente en ese momento. Este problema se discute en detalle en Effecive Java 2nd Edition , Artículo 17: Diseño y documento para herencia o de lo contrario lo prohíbe .

Tenga en cuenta que este es un caso especial del problema general de publicar referencias de objetos prematuramente . Los métodos de instancia tienen un thisparámetro implícito , pero pasar thisexplícitamente a un método puede causar problemas similares. Especialmente en programas concurrentes en los que si la referencia del objeto se publica prematuramente en otro hilo, ese hilo ya puede invocar métodos antes de que finalice el constructor en el primer hilo.

Péter Török
fuente
3
(+1) "mientras está dentro del constructor, el objeto aún no está completamente construido". Igual que "métodos de clase vs instancia". Algunos lenguajes de programación lo consideran construido al ingresar al contructor, como si el programador asignara valores al constructor.
umlcat
7

No consideraría que las llamadas a métodos aquí sean un antipatrón en sí mismo, más un olor a código. Si una clase proporciona un resetmétodo, que devuelve un objeto a su estado original, entonces llamar reset()al constructor es DRY. (No estoy haciendo ninguna declaración sobre los métodos de reinicio).

Aquí hay un artículo que puede ayudarlo a satisfacer su apelación de autoridad: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

No se trata realmente de llamar a métodos, sino de constructores que hacen demasiado. En mi humilde opinión, llamar a métodos en un constructor es un olor que podría indicar que un constructor es demasiado pesado.

Esto está relacionado con lo fácil que es probar su código. Las razones incluyen:

  1. Las pruebas unitarias implican mucha creación y destrucción, por lo tanto, la construcción debe ser rápida.

  2. Dependiendo de lo que hagan esos métodos, puede ser difícil probar unidades de código discretas sin depender de alguna precondición (potencialmente no comprobable) configurada en el constructor (por ejemplo, obtener información de una red).

Carnicero paul
fuente
3

Filosóficamente, el propósito del constructor es convertir una parte bruta de memoria en una instancia. Mientras se ejecuta el constructor, el objeto aún no existe, por lo que llamar a sus métodos es una mala idea. Después de todo, es posible que no sepa lo que hacen internamente, y pueden considerar legítimamente que el objeto al menos existe (¡duh!) Cuando se les llama.

Técnicamente, puede que no haya nada de malo en eso, en C ++ y especialmente en Python, depende de usted tener cuidado.

Prácticamente, debe limitar las llamadas solo a los métodos que inicializan a los miembros de la clase.


fuente
2

No es un problema de propósito general. Es un problema en C ++, específicamente cuando se utilizan métodos virtuales y de herencia, porque la construcción de objetos ocurre al revés, y los punteros vtable se restablecen con cada capa de constructor en la jerarquía de herencia, por lo que si está llamando a un método virtual, es posible que no terminan obteniendo el que realmente corresponde a la clase que están tratando de crear, lo que anula el propósito de usar métodos virtuales.

En lenguajes con soporte de OOP sano, que establece el puntero vtable correctamente desde el principio, este problema no existe.

Mason Wheeler
fuente
2

Hay dos problemas al llamar a un método:

  • llamando a un método virtual, que puede hacer algo inesperado (C ++) o usar partes de los objetos que aún no se han inicializado
  • llamar a un método público (que debería aplicar los invariantes de clase), ya que el objeto aún no está necesariamente completo (y, por lo tanto, su invariante puede no mantenerse)

No hay nada de malo en llamar a una función auxiliar, siempre que no se encuentre en los dos casos anteriores.

Matthieu M.
fuente
1

Yo no compro esto. En un sistema orientado a objetos, llamar a un método es prácticamente lo único que puede hacer. De hecho, esa es más o menos la definición de "orientado a objetos". Entonces, si un constructor no puede llamar a ningún método, ¿qué puede hacer?

Jörg W Mittag
fuente
Inicializa el objeto.
Stefano Borini
@Stefano Borini: ¿Cómo? En un sistema orientado a objetos, lo único que puede hacer es llamar a métodos. O para mirarlo desde el ángulo opuesto: cualquier cosa se hace mediante métodos de llamada. Y "cualquier cosa" obviamente incluye la inicialización del objeto. Entonces, si, para inicializar el objeto, necesita llamar a métodos, pero los constructores no pueden llamar a métodos, entonces ¿cómo puede un constructor inicializar el objeto?
Jörg W Mittag
No es absolutamente cierto que lo único que puede hacer es llamar a los métodos. Puede simplemente inicializar el estado sin ninguna llamada, directamente a las partes internas de su objeto ... El objetivo del constructor es hacer que un objeto esté en un estado consistente. Si llama a otros métodos, estos pueden tener problemas para manejar un objeto en un estado parcial, a menos que sean métodos diseñados específicamente para ser llamados desde el constructor (generalmente como métodos auxiliares)
Stefano Borini
@Stefano Borini: "Puede simplemente inicializar el estado sin ninguna llamada, directamente a las partes internas de su objeto". Lamentablemente, cuando eso implica un método, ¿qué haces? Copiar y pegar el código?
S.Lott
1
@ S.Lott: no, lo llamo, pero trato de mantenerlo como una función de módulo en lugar de un método de objeto, y hacer que proporcione datos de retorno que pueda poner en el estado del objeto en el constructor. Si realmente tengo que tener un método de objeto, lo haré privado y aclararé que es para la inicialización, como darle un nombre propio. Sin embargo, nunca llamaría a un método público para establecer el estado del objeto desde el constructor.
Stefano Borini
0

En la teoría OOP, no debería importar, pero en la práctica, cada lenguaje de programación OOP maneja constructores diferentes . No uso métodos estáticos muy a menudo.

En C ++ y Delphi, si tuviera que dar valores iniciales a algunas propiedades ("miembros de campo"), y el código es muy extendido, agrego algunos métodos secundarios como extensión de los constructores.

Y no llame a otros métodos que hacen cosas más complejas.

En cuanto a los métodos "getters" y "setters" de propiedades, generalmente utilizo variables privadas / protegidas para almacenar su estado, además de los métodos "getters" y "setters".

En el constructor, asigno valores "predeterminados" a los campos de estado de propiedades, SIN llamar a los "accesores".

umlcat
fuente