O construtor geralmente não deve chamar métodos

12

Descrevi a um colega por que um construtor que chama um método pode ser um antipadrão.

exemplo (no meu enferrujado 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
}

Gostaria de motivar melhor esse fato por meio de sua contribuição adicional. Se você tiver exemplos, referências de livros, páginas de blog ou nomes de princípios, eles serão muito bem-vindos.

Edit: Eu estou falando em geral, mas estamos codificando em python.

Stefano Borini
fonte
Essa é uma regra geral ou específica para idiomas específicos?
ChrisF
Qual língua? Em C ++ que é mais do que um anti-padrão: parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5
LennyProgrammers
@ Lenny222, o OP fala sobre "métodos de classe", que - pelo menos para mim - significa métodos que não são de instância . O que, portanto, não pode ser virtual.
Péter Török
3
@ Alb Em Java, está perfeitamente bem. O que você não deve fazer é passar explicitamente thispara qualquer um dos métodos que você chama do construtor.
biziclop
3
@ Stefano Borini: Se você está codificando em Python, por que não mostrar o exemplo em Python em vez de C ++ enferrujado? Além disso, explique por que isso é uma coisa ruim. Nós fazemos isso o tempo todo.
S.Lott 17/02

Respostas:

26

Você não especificou um idioma.

No C ++, um construtor deve tomar cuidado ao chamar uma função virtual, pois a função real que está chamando é a implementação da classe. Se for um método virtual puro sem uma implementação, isso será uma violação de acesso.

Um construtor pode chamar funções não virtuais.

Se sua linguagem é Java, em que as funções geralmente são virtuais por padrão, faz sentido que você precise ser extremamente cuidadoso.

O C # parece lidar com a situação da maneira que você esperaria: você pode chamar métodos virtuais em construtores e chama a versão mais final. Portanto, em C # não é um anti-padrão.

Um motivo comum para chamar métodos de construtores é que você possui vários construtores que desejam chamar um método "init" comum.

Observe que os destruidores terão o mesmo problema com os métodos virtuais; portanto, você não pode ter um método de "limpeza" virtual que fica fora do seu destruidor e espere que ele seja chamado pelo destruidor de classe base.

Java e C # não têm destruidores, eles têm finalizadores. Eu não sei o comportamento com Java.

C # parece manipular a limpeza corretamente nesse sentido.

(Observe que, embora Java e C # possuam coleta de lixo, isso gerencia apenas a alocação de memória. Há outra limpeza que seu destruidor precisa fazer para não liberar memória).

CashCow
fonte
13
Existem alguns pequenos erros aqui. Métodos em C # não são virtuais por padrão. C # possui semântica diferente de C ++ ao chamar um método virtual em um construtor; o método virtual no tipo mais derivado será chamado, não o método virtual na parte do tipo que está sendo construída atualmente. O C # chama seus métodos de finalização de "destruidores", mas você está certo de que eles têm a semântica dos finalizadores. Os métodos virtuais chamados nos destruidores de C # funcionam da mesma maneira que nos construtores; o método mais derivado é chamado.
Eric Lippert
@ Péter: Eu pretendia métodos de instância. Desculpe pela confusão.
Stefano Borini
1
@Eric Lippert. Obrigado por sua experiência em C #, editei minha resposta de acordo. Não conheço essa linguagem, conheço muito bem o C ++ e o Java menos.
CashCow
5
Seja bem-vindo. Observe que chamar um método virtual em um construtor de classe base em C # ainda é uma péssima idéia.
Eric Lippert
Se você chamar um método (virtual) em Java a partir de um construtor, ele sempre chamará a substituição mais derivada. No entanto, o que você chama de "do jeito que você esperaria" é o que eu chamaria de confuso. Como embora o Java chame a substituição mais derivada, esse método verá apenas inicializadores arquivados processados, mas não o construtor de sua própria classe. Invocar um método em uma classe que ainda não tem seu invariante estabelecido pode ser perigoso. Então, eu acho que o C ++ fez a melhor escolha aqui.
precisa saber é o seguinte
18

OK, agora que a confusão sobre métodos de classe contra métodos de instância seja esclarecido, posso dar uma resposta :-)

O problema não é chamar métodos de instância em geral de um construtor; é com a chamada de métodos virtuais (direta ou indiretamente). E a principal razão é que, enquanto dentro do construtor, o objeto ainda não está totalmente construído . E, especialmente, suas partes de subclasse não são construídas enquanto o construtor da classe base está em execução. Portanto, seu estado interno é inconsistente de uma maneira dependente do idioma, e isso pode causar diferentes erros sutis em diferentes idiomas.

C ++ e C # já foram discutidos por outros. Em Java, o método virtual do tipo mais derivado será chamado, porém esse tipo ainda não foi inicializado. Portanto, se esse método estiver usando quaisquer campos do tipo derivado, esses campos ainda não poderão ser inicializados corretamente naquele momento. Esse problema é discutido em detalhes no Effecive Java 2nd Edition , Item 17: Design e documento para herança ou então a proíbe .

Observe que este é um caso especial do problema geral de publicar referências de objetos prematuramente . Os métodos de instância têm um thisparâmetro implícito , mas a passagem thisexplícita para um método pode causar problemas semelhantes. Especialmente em programas simultâneos em que, se a referência do objeto for publicada prematuramente em outro encadeamento, esse encadeamento já poderá chamar métodos antes que o construtor no primeiro encadeamento seja concluído.

Péter Török
fonte
3
(+1) "enquanto estiver dentro do construtor, o objeto ainda não está totalmente construído." O mesmo que "métodos de classe versus instância". Algumas linguagens de programação consideram-na construída ao entrar no contratante, como se o programador atribuísse valores ao construtor.
umlcat
7

Eu não consideraria as chamadas de método aqui um antipadrão em si, mais um cheiro de código. Se uma classe fornece um resetmétodo, que retorna um objeto ao seu estado original, a chamada reset()no construtor é DRY. (Não estou fazendo nenhuma declaração sobre métodos de redefinição).

Aqui está um artigo que pode ajudar a satisfazer seu pedido de autoridade: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

Não se trata realmente de chamar métodos, mas de construtores que fazem muito. IMHO, chamar métodos em um construtor é um cheiro que pode indicar que um construtor é muito pesado.

Isso está relacionado a como é fácil testar seu código. Os motivos incluem:

  1. O teste de unidade envolve muita criação e destruição - portanto, a construção deve ser rápida.

  2. Dependendo do que esses métodos fazem, pode ser difícil testar unidades de código discretas sem depender de alguma pré-condição (potencialmente não testável) configurada no construtor (por exemplo, obter informações de uma rede).

Paul Butcher
fonte
3

Filosoficamente, o objetivo do construtor é transformar um pedaço bruto de memória em uma instância. Enquanto o construtor está sendo executado, o objeto ainda não existe, portanto, chamar seus métodos é uma má idéia. Você pode não saber o que eles fazem internamente, afinal, e eles podem considerar com razão que o objeto existe pelo menos (duh!) Quando são chamados.

Tecnicamente, pode não haver nada de errado nisso, em C ++ e especialmente em Python, cabe a você tomar cuidado.

Praticamente, você deve limitar as chamadas apenas aos métodos que inicializam os membros da classe.


fonte
2

Não é uma questão de uso geral. É um problema no C ++, especificamente ao usar métodos virtuais e de herança, porque a construção do objeto ocorre ao contrário e o (s) ponteiro (s) da vtable são redefinidos com cada camada de construtor na hierarquia de herança, portanto, se você estiver chamando um método virtual, talvez não acabamos obtendo o que realmente corresponde à classe que você está tentando criar, o que anula todo o propósito de usar métodos virtuais.

Em idiomas com suporte a OOP, que definem o ponteiro vtable corretamente desde o início, esse problema não existe.

Mason Wheeler
fonte
2

Há dois problemas ao chamar um método:

  • chamando um método virtual, que pode fazer algo inesperado (C ++) ou usar partes dos objetos que ainda não foram inicializados
  • chamar um método público (que deve impor os invariantes de classe), já que o objeto ainda não está necessariamente completo (e, portanto, seu invariável pode não ser válido)

Não há nada errado em chamar uma função auxiliar, desde que ela não caia nos dois casos anteriores.

Matthieu M.
fonte
1

Eu não compro isso. Em um sistema orientado a objetos, chamar um método é praticamente a única coisa que você pode fazer. De fato, essa é mais ou menos a definição de "orientado a objetos". Portanto, se um construtor não pode chamar nenhum método, o que ele pode fazer?

Jörg W Mittag
fonte
Inicialize o objeto.
Stefano Borini
@ Stefano Borini: Como? Em um sistema orientado a objetos, a única coisa que você pode fazer é chamar métodos. Ou, olhando do ângulo oposto: tudo é feito chamando métodos. E "qualquer coisa" obviamente inclui a inicialização do objeto. Portanto, se, para inicializar o objeto, você precisa chamar métodos, mas os construtores não podem chamar métodos, como um construtor pode inicializar o objeto?
Jörg W Mittag 17/02
absolutamente não é verdade que a única coisa que você pode fazer é chamar métodos. Você pode apenas inicializar o estado sem nenhuma chamada, diretamente para as partes internas do seu objeto ... O objetivo do construtor é tornar um objeto em um estado consistente. Se você chamar outros métodos, estes podem ter problemas de manipulação de um objeto em um estado parcial, a menos que eles são métodos especificamente feito para ser chamado a partir do construtor (tipicamente como métodos auxiliares)
Stefano Borini
@ Stefano Borini: "Você pode apenas inicializar o estado sem qualquer chamada, diretamente para as partes internas do seu objeto." Infelizmente, quando isso envolve um método, o que você faz? Copiar e colar o código?
S.Lott
1
@ S.Lott: não, eu chamo, mas tento manter uma função de módulo em vez de um método de objeto e fornecer dados de retorno que eu possa colocar no estado do objeto no construtor. Se eu realmente precisar de um método de objeto, o tornarei privado e esclarecerei que é para inicialização, como dar um nome adequado. Eu nunca chamaria um método público para definir o status do objeto do construtor.
Stefano Borini
0

Na teoria OOP, isso não deve importar, mas na prática, cada linguagem de programação OOP lida com os construtores de maneira diferente . Não uso métodos estáticos com muita frequência.

Em C ++ e Delphi, se eu precisasse fornecer valores iniciais para algumas propriedades ("membros do campo") e o código for muito extenso, adiciono alguns métodos secundários como extensão dos construtores.

E não chame outros métodos que fazem coisas mais complexas.

Quanto aos métodos "getters" e "setters" das propriedades, geralmente uso variáveis ​​privadas / protegidas para armazenar seu estado, além dos métodos "getters" e "setters".

No construtor, atribuo valores "padrão" aos campos de estado das propriedades, SEM chamar os "acessadores".

umlcat
fonte