Legítimo "trabalho real" em um construtor?


23

Estou trabalhando em um design, mas continue atingindo um obstáculo. Eu tenho uma classe específica (ModelDef) que é essencialmente o proprietário de uma árvore de nó complexa criada pela análise de um esquema XML (pense em DOM). Desejo seguir os bons princípios de design (SOLID) e garantir que o sistema resultante seja facilmente testável. Eu tenho toda a intenção de usar o DI para passar dependências para o construtor de ModelDef (para que elas possam ser facilmente trocadas, se necessário, durante o teste).

No entanto, estou lutando com a criação da árvore de nós. Essa árvore será composta inteiramente de objetos simples de "valor" que não precisarão ser testados independentemente. (No entanto, ainda posso passar uma Abstract Factory para o ModelDef para ajudar na criação desses objetos.)

Mas continuo lendo que um construtor não deve fazer nenhum trabalho real (por exemplo, Falha: Construtor faz um trabalho real ). Isso faz todo o sentido para mim, se "trabalho real" significa construir objetos dependentes de peso pesado que mais tarde você pode querer esboçar para teste. (Esses devem ser repassados ​​via DI.)

Mas e os objetos de valor leve, como essa árvore de nós? A árvore tem que ser criada em algum lugar, certo? Por que não através do construtor de ModelDef (usando, digamos, um método buildNodeTree ())?

Eu realmente não quero criar a árvore de nós fora do ModelDef e depois passá-la (via construtor DI), porque criar a árvore de nós analisando o esquema requer uma quantidade significativa de código complexo - código que precisa ser exaustivamente testado . Não quero relegá-lo para "colar" o código (que deve ser relativamente trivial e provavelmente não será testado diretamente).

Pensei em colocar o código para criar a árvore de nós em um objeto "construtor" separado, mas hesite em chamá-lo de "construtor", porque realmente não corresponde ao padrão do construtor (que parece estar mais preocupado em eliminar a telescopia) construtores). Mas mesmo que eu chamei de algo diferente (por exemplo, NodeTreeConstructor), ainda parece um hack apenas para evitar que o construtor ModelDef construa a árvore de nós. Tem que ser construído em algum lugar; por que não no objeto que o possui?


7
Você deve sempre ter cuidado com declarações gerais como essa. A regra geral é o código de uma maneira clara, funcional, fácil de testar, reutilizar e manter, da maneira que for, que varia de acordo com a sua situação. Se você ganhar nada além de complexidade e confusão de código, tentando seguir uma "regra" como essa, não seria uma regra apropriada para sua situação. Todos esses "padrões" e recursos de linguagem são ferramentas; use o melhor para o seu trabalho específico.
Jason C

Respostas:


26

E, além do que Ross Patterson sugeriu, considere esta posição que é exatamente o oposto:

  1. Tome máximas como "Não Realizarás Trabalho Real em Teus Construtores" com um grão de sal.

  2. Um construtor é, na verdade, nada além de um método estático. Portanto, estruturalmente, realmente não há muita diferença entre:

    a) um construtor simples e um monte de métodos complexos de fábrica estática; e

    b) um construtor simples e um monte de construtores mais complexos.

Uma parte considerável do sentimento negativo de realizar qualquer trabalho real em construtores vem de um certo período da história do C ++, quando houve um debate sobre exatamente em que estado o objeto será deixado se uma exceção for lançada no construtor e se o destruidor deve ser chamado nesse evento. Essa parte da história do C ++ acabou e o problema foi resolvido, enquanto em linguagens como Java nunca houve nenhum problema desse tipo para começar.

Minha opinião é que, se você simplesmente evitar o uso newno construtor (como indica sua intenção de empregar a Injeção de Dependência), estará bem. Eu rio de afirmações como "a lógica condicional ou de loop em um construtor é um sinal de aviso de uma falha".

Além de tudo isso, pessoalmente, eu retiraria a lógica de análise XML do construtor, não porque é ruim ter lógica complexa em um construtor, mas porque é bom seguir o princípio "separação de preocupações". Portanto, eu moveria a lógica de análise XML para uma classe separada completamente, não para alguns métodos estáticos que pertencem à sua ModelDefclasse.

Alteração

Suponho que, se você tiver um método fora do ModelDefqual cria um ModelDefXML, precisará instanciar alguma estrutura de dados de árvore temporária dinâmica, preenchê-la analisando seu XML e, em seguida, criar sua nova ModelDefpassagem nessa estrutura como um parâmetro construtor. Portanto, isso poderia ser pensado como uma aplicação do padrão "Construtor". Existe uma analogia muito próxima entre o que você quer fazer e o par String& StringBuilder. No entanto, encontrei estas perguntas e respostas que parecem discordar, por razões que não estão claras para mim: Stackoverflow - StringBuilder e Builder Pattern . Portanto, para evitar um longo debate aqui sobre se o StringBuildermétodo "construtor" implementa ou não, eu diria que sinta-se à vontade para se inspirar em comoStrungBuilder trabalha para encontrar uma solução que atenda às suas necessidades e adie chamá-lo de aplicativo do padrão "Construtor" até que esse pequeno detalhe seja resolvido.

Veja esta nova pergunta: Programadores SE: “StringBuilder” é um aplicativo do Builder Design Pattern?


3
@RichardLevasseur Acabei de me lembrar como um tópico de preocupação e debate entre os programadores de C ++ no início e meados dos anos 90. Se você olhar para este post: gotw.ca/gotw/066.htm , verá que é bastante complicado, e coisas bastante complicadas tendem a ser controversas. Não sei ao certo, mas acho que no início dos anos 90 parte dessas coisas ainda nem havia sido padronizada. Mas desculpe, não posso fornecer uma boa referência.
quer

1
@Gurtz Eu consideraria uma classe como "utilitários xml" específicos de aplicativos, já que o formato do arquivo xml (ou a estrutura do documento) provavelmente está vinculado ao aplicativo específico que você está desenvolvendo, independentemente de qualquer possibilidade de reutilize seu "ModelDef".
quer

1
@ Gurtz, então eu provavelmente os tornaria métodos de instância da classe "Application" principal, ou, se isso for muito trabalhoso, métodos estáticos de alguma classe auxiliar, de uma maneira muito semelhante à sugerida por Ross Patterson.
precisa

1
O @Gurtz pede desculpas por não ter abordado especificamente a abordagem "construtor" anteriormente. Eu alterei minha resposta.
precisa

3
@ Gurtz É possível, mas fora da curiosidade acadêmica, não importa. Não seja sugado para o "padrão anti-padrão". Padrões são realmente apenas nomes para descrever as técnicas de codificação comuns / úteis para outras pessoas convenientemente. Faça o que você precisa fazer, coloque um rótulo nele mais tarde, se precisar descrevê-lo. Não há problema em implementar algo que é "como o padrão do construtor, de certa forma, talvez", desde que seu código faça sentido. É razoável focar nos padrões ao aprender novas técnicas, apenas não caia na armadilha de pensar que tudo que você faz deve ser um padrão nomeado.
Jason C

9

Você já fornece os melhores motivos para não fazer este trabalho no ModelDefconstrutor:

  1. Não há nada "leve" em analisar um documento XML em uma árvore de nós.
  2. Não há nada óbvio em um ModelDefque diga que ele só pode ser criado a partir de um documento XML.

Parece que sua classe deve ter uma variedade de métodos estáticos, como ModelDef.FromXmlString(string xmlDocument), ModelDef.FromXmlDoc(XmlDoc parsedNodeTree), etc.


Obrigado pela resposta! Em relação à sugestão de métodos estáticos. Seriam fábricas estáticas que criam uma instância do ModelDef (das várias fontes xml)? Ou eles seriam responsáveis ​​por carregar um objeto ModelDef já criado? Neste último caso, eu ficaria preocupado em ter o objeto inicializado apenas parcialmente (já que um ModelDef precisa que uma árvore de nós seja totalmente inicializada). Pensamentos?
Gurtz

3
Desculpe-me por me intrometer, mas sim, o que Ross quer dizer com métodos estáticos de fábrica que retornam instâncias totalmente construídas. O protótipo completo seria algo parecido public static ModelDef createFromXmlString( string xmlDocument ). Essa é uma prática bastante comum. Às vezes eu também faço. Minha sugestão de que você também pode fazer apenas construtores é um tipo padrão de resposta minha em situações em que suspeito que abordagens alternativas sejam descartadas como "não kosher" sem uma boa razão.
Mike Nakis

1
@ Mike-Nakis, obrigado por esclarecer. Portanto, nesse caso, o método estático de fábrica criaria a árvore de nós e passaria isso para o construtor (possivelmente privado) do ModelDef. Faz sentido. Obrigado.
Gurtz

@Gurtz Exatamente.
Ross Patterson

5

Já ouvi essa "regra" antes. Na minha experiência, isso é verdadeiro e falso.

Em orientação a objetos mais "clássica", falamos sobre objetos que encapsulam estado e comportamento. Assim, um construtor de objetos deve garantir que o objeto seja inicializado com um estado válido (e sinalizar uma falha se os argumentos fornecidos não tornarem o objeto válido). Garantir que um objeto seja inicializado em um estado válido certamente parece um trabalho real para mim. E essa ideia tem mérito, se você tiver um objeto que permita apenas a inicialização para um estado válido por meio do construtor e o objeto encapsular adequadamente seu estado, para que cada método que altere o estado verifique também se ele não altera o estado para algo ruim. ... então esse objeto em essência garante que é "sempre válido". Essa é uma propriedade muito boa!

Portanto, o problema geralmente surge quando tentamos dividir tudo em pedaços pequenos para testar e zombar de coisas. Como se um objeto é realmente encapsulado de maneira adequada, você não pode realmente entrar lá e substituir o FooBarService por seu FooBarService zombado e você (provavelmente) não pode simplesmente alterar valores, quer queira quer não, para se adequar aos seus testes (ou, pode levar uma muito mais código do que uma tarefa simples).

Assim, temos a outra "escola de pensamento", que é o SOLID. E nessa escola de pensamento é muito mais provável que não façamos um trabalho real no construtor. O código SOLID é frequentemente (mas nem sempre) mais fácil de testar. Mas também pode ser mais difícil de raciocinar. Dividimos nosso código em objetos pequenos com uma única responsabilidade e, portanto, a maioria dos nossos objetos não encapsula seu estado (e geralmente contém estado ou comportamento). O código de validação geralmente é extraído em uma classe de validador e mantido separado do estado. Mas agora que perdemos a coesão, não podemos mais ter certeza de que nossos objetos são válidos quando os obtemos e completamenteCertamente, devemos sempre validar se as pré-condições que achamos que temos sobre o objeto são verdadeiras antes de tentarmos fazer algo com o objeto. (É claro que, em geral, você faz a validação em uma camada e assume que o objeto é válido nas camadas inferiores.) Mas é mais fácil testar!

Então quem está certo?

Ninguém realmente. Ambas as escolas de pensamento têm seus méritos. Atualmente, o SOLID está na moda e todo mundo está falando sobre SRP e Open / Closed e todo esse jazz. Mas apenas porque algo é popular, não significa que é a opção de design correta para cada aplicativo. Então depende. Se você estiver trabalhando em uma base de código que segue fortemente os princípios do SOLID, sim, o trabalho real no construtor provavelmente é uma má ideia. Mas, caso contrário, observe a situação e tente usar seu julgamento. Quais propriedades seu objeto obtém ao trabalhar no construtor, quais propriedades ele perde ? Quão bem ele se encaixa na arquitetura geral do seu aplicativo?

O trabalho real no construtor não é um antipadrão, pode ser exatamente o oposto quando usado nos lugares corretos. Mas deve ser documentado claramente (junto com as exceções que podem ser lançadas, se houver) e como em qualquer decisão de design - deve se encaixar no estilo geral usado na base de código atual.


Esta é uma resposta fantástica.
jrahhali

0

Existe um problema fundamental com esta regra e é isso, o que constitui "trabalho real"?

Você pode ver no artigo original publicado na pergunta que o autor tenta definir o que é "trabalho real", mas é severamente falho. Para que uma prática seja boa, ela precisa ser um princípio bem definido. Com isso, quero dizer que, no que diz respeito à engenharia de software, a idéia deve ser portátil (independente de qualquer idioma), testada e comprovada. A maior parte do que é discutido nesse artigo não se encaixa nesse primeiro critério. Aqui estão alguns indicadores que o autor menciona nesse artigo sobre o que constitui "trabalho real" e por que eles não são más definições.

Uso da newpalavra - chave . Essa definição é fundamentalmente falha porque é específica do domínio. Alguns idiomas não usam a newpalavra - chave. Em última análise, o que ele está sugerindo é que não deveria estar construindo outros objetos. No entanto, em muitos idiomas, mesmo os valores mais básicos são objetos. Portanto, qualquer valor atribuído no construtor também está construindo um novo objeto. Isso limita essa idéia a certas línguas e é um mau indicador do que constitui "trabalho real".

Objeto não totalmente inicializado após a conclusão do construtor . Essa é uma boa regra, mas também contradiz várias das outras regras mencionadas nesse artigo. Um bom exemplo de como isso poderia contradizer os outros é mencionado na pergunta que me trouxe aqui. Nessa pergunta, alguém está preocupado em usar osort método em um construtor no que parece ser JavaScript por causa desse princípio. Neste exemplo, a pessoa estava criando um objeto que representava uma lista classificada de outros objetos. Para fins de discussão, imagine que tínhamos uma lista não classificada de objetos e precisávamos de um novo objeto para representar uma lista classificada. Precisamos desse novo objeto, porque parte do nosso software espera uma lista classificada e vamos chamá-loSortedList. Esse novo objeto aceita uma lista não classificada e o objeto resultante deve representar uma lista agora classificada de objetos. Se seguíssemos as outras regras mencionadas nesse documento, a saber, nenhuma chamada de método estático, nenhuma estrutura de fluxo de controle, nada além de atribuição, o objeto resultante não seria construído em um estado válido, quebrando a outra regra de que ele estava totalmente inicializado. após o construtor terminar. Para consertar isso, precisaríamos fazer algum trabalho básico para tornar a lista não classificada classificada no construtor. Fazer isso violaria as outras três regras, tornando as outras regras irrelevantes.

Em última análise, essa regra de não fazer "trabalho real" em um construtor é mal definida e falha. Tentando definir o que "trabalho real" é um exercício de futilidade. A melhor regra nesse artigo é que, quando um construtor terminar, ele deverá ser totalmente inicializado. Existem inúmeras outras práticas recomendadas que limitariam a quantidade de trabalho realizado em um construtor. A maioria deles pode ser resumida nos princípios do SOLID, e esses mesmos princípios não impediriam que você trabalhasse no construtor.

PS. Sinto-me obrigado a dizer que, embora afirme aqui que não há nada errado em fazer algum trabalho no construtor, também não é o lugar para fazer um monte de trabalho. O SRP sugere que um construtor deve fazer o trabalho necessário para torná-lo válido. Se o seu construtor tem muitas linhas de código (eu sei muito subjetivo), provavelmente está violando esse princípio e provavelmente poderia ser dividido em objetos e métodos menores e melhor definidos.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.