quão complexo um construtor deve ser

18

Estou tendo uma discussão com meu colega de trabalho sobre quanto trabalho um construtor pode fazer. Eu tenho uma classe B que internamente requer outro objeto A. O objeto A é um dos poucos membros que a classe B precisa fazer seu trabalho. Todos os seus métodos públicos dependem do objeto interno A. As informações sobre o objeto A são armazenadas no banco de dados, então eu tento validar e obtê-lo procurando-o no banco de dados no construtor. Meu colega de trabalho apontou que o construtor não deve fazer muito trabalho além de capturar os parâmetros do construtor. Como todos os métodos públicos falhariam de qualquer maneira, se o objeto A não fosse encontrado usando as entradas do construtor, argumentei que, em vez de permitir que uma instância seja criada e depois falhe, é melhor lançar o construtor mais cedo.

O que os outros pensam? Eu estou usando c # se isso faz alguma diferença.

Leitura Existe alguma razão para fazer todo o trabalho de um objeto em um construtor? Gostaria de saber que a busca do objeto A, indo ao DB, faz parte de "qualquer outra inicialização necessária para tornar o objeto pronto para uso" porque, se o usuário passasse valores incorretos para o construtor, eu não seria capaz de usar nenhum de seus métodos públicos.

Os construtores devem instanciar os campos de um objeto e fazer qualquer outra inicialização necessária para tornar o objeto pronto para uso. Isso geralmente significa que os construtores são pequenos, mas há cenários em que isso seria uma quantidade substancial de trabalho.

Taein Kim
fonte
8
Discordo. Dê uma olhada no princípio da inversão de dependência; seria melhor se o objeto A fosse entregue ao objeto B em um estado válido em seu construtor.
Jimmy Hoffa
5
Você está perguntando quanto pode ser feito? Quanto deve ser feito? Ou quanto da sua situação específica deve ser feita? São três perguntas muito diferentes.
Bobson
1
Concordo com a sugestão de Jimmy Hoffa de examinar a injeção de dependência (ou "inversão de dependência"). Esse foi o meu primeiro pensamento ao ler a descrição, porque parece que você já está no meio do caminho; você já possui funcionalidade separada na classe A, que a classe B está usando. Não posso falar com o seu caso, mas geralmente acho que o código de refatoração para usar o padrão de injeção de dependência torna as classes mais simples / menos interligadas.
Aprendiz do Dr. Wily
1
Bobson, mudei o título para 'should'. Estou pedindo as melhores práticas aqui.
Taein Kim
1
@ JimmyHoffa: talvez você deva expandir seu comentário para uma resposta.
kevin Cline

Respostas:

22

Sua pergunta é composta de duas partes completamente separadas:

Devo lançar exceção do construtor, ou devo ter método falhar?

Esta é claramente a aplicação do princípio Fail-fast . Fazer com que o construtor falhe é muito mais fácil de depurar do que descobrir por que o método está falhando. Por exemplo, você pode obter a instância já criada a partir de outra parte do código e obter erros ao chamar métodos. É óbvio que o objeto foi criado errado? Não.

Quanto ao problema "encerre a chamada em try / catch". Exceções devem ser excepcionais . Se você souber que algum código lançará uma exceção, você não quebrará o código em try / catch, mas validará os parâmetros desse código antes de executar o código que pode lançar uma exceção. Exceções são apenas uma forma de garantir que o sistema não entre em estado inválido. Se você sabe que alguns parâmetros de entrada podem levar a um estado inválido, verifique se esses parâmetros nunca acontecem. Dessa forma, você só precisa tentar / capturar em locais onde é possível lidar logicamente com a exceção, que geralmente fica nos limites do sistema.

Posso acessar "outras partes do sistema, como DB" do construtor.

Eu acho que isso vai contra o princípio de menor espanto . Poucas pessoas esperariam que o construtor acesse um banco de dados. Então não, você não deve fazer isso.

Eufórico
fonte
2
A solução mais apropriada para isso é adicionar um método do tipo "aberto" onde a construção é livre, mas não é possível fazer nada antes de "abrir" / "conectar" / etc, que é onde ocorre toda a inicialização real. A falha dos construtores pode causar todos os tipos de problemas quando, no futuro, você desejar construir parcialmente os objetos, o que é uma habilidade útil.
Jimmy Hoffa
@ JimmyHoffa Não vejo diferença entre o método construtor e o método específico para construção.
Euphoric
4
Você pode não, mas muitos fazem. Pense em quando você cria um objeto de conexão, o construtor o conecta? O objeto é utilizável se não estiver conectado? Nas duas perguntas, a resposta é não, porque é útil ter objetos de conexão parcialmente construídos para que você possa ajustar as configurações e outras coisas antes de abrir a conexão. Essa é uma abordagem comum para esse cenário. Eu ainda acho que a injeção de construtor é a abordagem correta para cenários em que o objeto A tem uma dependência de recursos, mas um método aberto ainda é melhor do que fazer com que o construtor faça o trabalho perigoso.
Jimmy Hoffa
@ JimmyHoffa Mas esse é um requisito específico da classe de conexão, não regra geral do design do OOP. Na verdade, eu diria que é um caso excepcional, e não uma regra. Você está basicamente falando sobre o padrão do construtor.
Euphoric
2
Não é um requisito, foi uma decisão de design. Eles poderiam ter feito o construtor pegar uma cadeia de conexão e conectar-se imediatamente quando construído. Eles decidiram não muito porque é útil para ser capaz de criar o objeto, em seguida, figura ajuste a seqüência de conexão ou outros bits e borbotos e conectá-lo mais tarde ,
Jimmy Hoffa
19

Ugh.

Um construtor deve fazer o mínimo possível - o problema é que o comportamento de exceção nos construtores é estranho. Muito poucos programadores sabem qual é o comportamento adequado (incluindo cenários de herança) e forçar seus usuários a tentar / capturar cada instanciação é ... doloroso, na melhor das hipóteses.

Há duas partes nisso, no construtor e no uso do construtor. É constrangedor no construtor porque você não pode fazer muita coisa quando ocorre uma exceção. Você não pode retornar um tipo diferente. Você não pode retornar nulo. Basicamente, você pode repetir a exceção, retornar um objeto quebrado (mau construtor) ou (na melhor das hipóteses) substituir alguma parte interna por um padrão sadio. Usar o construtor é estranho, porque suas instanciações (e as instâncias de todos os tipos derivados!) Podem ser lançadas. Então você não pode usá-los nos inicializadores de membros. Você acaba usando exceções para que a lógica veja "minha criação teve sucesso?", Além da legibilidade (e fragilidade) causada pela tentativa / captura em todos os lugares.

Se o seu construtor estiver fazendo coisas não triviais, ou coisas que possam razoavelmente falhar, considere um Createmétodo estático (com um construtor não público). É muito mais utilizável, mais fácil de depurar e ainda oferece uma instância totalmente construída quando bem-sucedida.

Telastyn
fonte
2
Cenários complexos de construção são exatamente o motivo pelo qual existem padrões de criação como você mencionou aqui. Essa é uma boa abordagem para essas construções problemáticas. Fico pensando em como o .NET Connectionpode um objeto CreateCommandpara você, para que você saiba que o comando está conectado adequadamente.
Jimmy Hoffa
2
Para isso, eu acrescentaria que um banco de dados é uma dependência externa pesada, suponha que você queira alternar bancos de dados em algum momento no futuro (ou zombar de um objeto para teste), movendo esse tipo de código para uma classe Factory (ou algo como um IService provedor) parece ser uma abordagem mais limpo
Jason Sperske
4
você pode elaborar "o comportamento de exceção nos construtores é estranho"? Se você usasse Create, não precisaria agrupá-lo no try catch, como faria com a chamada ao construtor? Sinto que Create é apenas um nome diferente para construtor. Talvez eu não esteja acertando sua resposta?
Taein Kim
1
Tendo tido que tentar capturar exceções borbulhando dos construtores, +1 aos argumentos contra isso. Tendo trabalhado com pessoas que dizem "Nenhum trabalho no construtor", isso também pode criar alguns padrões estranhos. Eu vi código em que todo objeto tem não apenas um construtor, mas também alguma forma de Initialize () onde, adivinhem? O objeto não é realmente válido até que seja chamado também. E então você tem duas coisas para chamar: o ctor e Initialize (). O problema de trabalhar para configurar um objeto não desaparece - o C # pode gerar soluções diferentes do que o C ++. Fábricas são boas!
J Trana
1
@ jtrana - sim, inicializar não é bom. O construtor inicializa para algum padrão são ou uma fábrica ou o método create cria e retorna uma instância utilizável.
Telastyn
1

O equilíbrio entre complexidade do construtor e do método é realmente uma questão de discussão sobre o bom estilo. Muitas vezes, Class() {}é usado um construtor vazio , com um método Init(..) {..}para coisas mais complicadas. Entre os argumentos a serem levados em consideração:

  • Possibilidade de serialização de classe
  • Com o método Init de separação do construtor, muitas vezes é necessário repetir a mesma sequência Class x = new Class(); x.Init(..);várias vezes, parcialmente nos testes de unidade. Não é tão bom, porém, claro para a leitura. Às vezes, eu apenas os coloco em uma linha de código.
  • Quando o objetivo do teste de unidade é apenas o teste de inicialização de classe, é melhor ter o próprio Init, para executar ações mais complicadas, realizadas uma a uma, com os Asserts intermediários.
Pavel Khrapkin
fonte
a vantagem, na minha opinião, do initmétodo é que lidar com falhas é muito mais fácil. Em particular, o valor de retorno do initmétodo pode indicar se a inicialização foi bem-sucedida e o método pode garantir que o objeto esteja sempre em bom estado.
Pqnet 12/0318