Existe alguma razão para fazer todo o trabalho de um objeto em um construtor?


49

Permitam-me que antecipe dizendo que este não é o meu código nem o código dos meus colegas de trabalho. Anos atrás, quando nossa empresa era menor, tínhamos alguns projetos que precisávamos realizar e que não tínhamos capacidade para, portanto eles eram terceirizados. Agora, não tenho nada contra terceirização ou contratados em geral, mas a base de código que eles produziram é uma massa de WTFs. Dito isto, ele funciona (principalmente), então suponho que esteja entre os 10% dos projetos terceirizados que já vi.

À medida que nossa empresa cresce, tentamos levar mais do nosso desenvolvimento internamente. Esse projeto em particular caiu no meu colo, então eu o reparei, limpei, adicionei testes, etc.

Há um padrão que vejo repetido muito e parece tão terrivelmente horrível que me perguntei se talvez houvesse um motivo e simplesmente não o vi. O padrão é um objeto sem métodos ou membros públicos, apenas um construtor público que faz todo o trabalho do objeto.

Por exemplo, (o código está em Java, se isso importa, mas espero que seja uma pergunta mais geral):

public class Foo {
  private int bar;
  private String baz;

  public Foo(File f) {
    execute(f);
  }

  private void execute(File f) {
     // FTP the file to some hardcoded location, 
     // or parse the file and commit to the database, or whatever
  }
}

Se você está se perguntando, esse tipo de código geralmente é chamado da seguinte maneira:

for(File f : someListOfFiles) {
   new Foo(f);
}

Agora, fui ensinado há muito tempo que os objetos instanciados em um loop geralmente são uma má idéia e que os construtores devem fazer um trabalho mínimo. Olhando para este código, parece que seria melhor descartar o construtor e criar executeum método público estático.

Perguntei ao empreiteiro por que isso foi feito dessa maneira, e a resposta que tive foi "Podemos mudar, se você quiser". O que não foi realmente útil.

De qualquer forma, existe alguma razão para fazer algo assim, em qualquer linguagem de programação, ou é apenas mais uma submissão ao Daily WTF?


18
Parece-me que foi escrito por desenvolvedores que conhecem public static void main(string[] args)e ouviram falar de objetos e depois tentaram misturá-los.
Andy Hunt

Se você nunca for chamado novamente, deve fazê-lo lá. Muitas estruturas modernas para, por exemplo, injeção de dependência, precisam que os beans sejam feitos, defina propriedades e, em seguida, invoque-os, e então você não poderá usar esses trabalhadores pesados.

4
Pergunta relacionada: Algum problema em realizar o trabalho principal de uma classe em seu construtor? . Essa é um pouco diferente, porque a instância criada é usada posteriormente.
sleske


Eu realmente não quero expandir isso para uma resposta completa, mas deixe-me dizer: há cenários em que isso é discutivelmente aceitável. Este está muito longe disso. Aqui, um objeto é instanciado sem realmente precisar do objeto. Por outro lado, se você estava criando algum tipo de estrutura de dados a partir de um arquivo XML, iniciar a análise do construtor não é totalmente horrível. De fato, em línguas que permitir a sobrecarga de construtores, não é nada que eu iria matá-lo para como um mantenedor;)
back2dos

Respostas:


60

Ok, descendo a lista:

Fui ensinado há muito tempo que objetos instanciados em um loop geralmente são uma má ideia

Não em nenhum idioma que eu usei.

Em C, é uma boa ideia declarar suas variáveis ​​antecipadamente, mas isso é diferente do que você disse. Pode ser um pouco mais rápido se você declarar objetos acima do loop e reutilizá-los, mas há muitas linguagens em que esse aumento na velocidade não faz sentido (e provavelmente alguns compiladores por aí que fazem a otimização para você :)).

Em geral, se você precisar de um objeto em um loop, crie um.

construtores devem fazer um trabalho mínimo

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.

existe alguma razão para fazer algo assim, em qualquer linguagem de programação, ou isso é apenas mais uma submissão ao Daily WTF?

É uma submissão ao Daily WTF. É verdade que existem coisas piores que você pode fazer com o código. O problema é que o autor tem um vasto mal-entendido sobre o que são classes e como usá-las. Especificamente, aqui está o que vejo de errado com este código:

  • Uso indevido de classes: A classe está basicamente agindo como uma função. Como você mencionou, ela deve ser substituída por uma função estática ou a função deve ser implementada apenas na classe que a está chamando. Depende do que faz e de onde é usado.
  • Sobrecarga de desempenho: dependendo do idioma, a criação de um objeto pode ser mais lenta do que chamar uma função.
  • Confusão geral: geralmente é confuso para o programador como usar esse código. Sem vê-lo usado, ninguém saberia como o autor pretendia usar o código em questão.

Obrigado pela sua resposta. Em referência a "instanciar objetos em um loop", isso é algo sobre o qual muitas ferramentas de análise estática alertam, e eu também ouvi isso de alguns professores universitários. Concedido, isso pode ser um impedimento dos anos 90, quando o impacto no desempenho foi maior. Claro, se você precisar, faça-o, mas estou surpreso ao ouvir um ponto de vista oposto. Obrigado por ampliar meus horizontes.
Kane

8
Instanciar objetos em um loop é um pequeno cheiro de código; você deve olhar e perguntar "eu realmente preciso instanciar esse objeto várias vezes?". Se você estiver instanciando o mesmo objeto com os mesmos dados e instruindo-o a fazer a mesma coisa, salvará ciclos (e evitará sobrecarregar a memória) instanciando-o uma vez e repetindo apenas a instrução com as alterações incrementais necessárias para o estado do objeto . No entanto, se você estiver, por exemplo, lendo um fluxo de dados de um banco de dados ou outra entrada e criando uma série de objetos para armazenar esses dados, instale de qualquer maneira.
Keiths

3
Versão curta: não use instanciações de objetos como se fossem uma função.
Erik Reppen

27

Para mim, é uma surpresa quando você obtém um objeto que faz muito trabalho no construtor, por isso recomendo que não seja recomendado (princípio da menor surpresa).

Uma tentativa de um bom exemplo :

Não tenho certeza se esse uso é um antipadrão, mas em C # eu vi código que estava ao longo das linhas de (exemplo um pouco inventado):

using (ImpersonationContext administrativeContext = new ImpersonationContext(ADMIN_USER))
{
    // Perform actions here under the administrator's security context
}
// We're back to "normal" privileges here

Nesse caso, a ImpersonationContextclasse faz todo o seu trabalho no construtor e no método Dispose. Ele tira proveito da usinginstrução C # , que é bastante bem entendida pelos desenvolvedores de C # e, portanto, garante que a instalação que ocorre no construtor seja revertida quando estivermos fora da usinginstrução, mesmo que ocorra uma exceção. Este é provavelmente o melhor uso que eu já vi para isso (ou seja, "trabalho" no construtor), embora ainda não tenha certeza se consideraria "bom". Nesse caso, é bastante auto-explicativo, funcional e sucinto.

Um exemplo de um padrão bom e semelhante seria o padrão de comando . Você definitivamente não executa a lógica no construtor, mas é semelhante no sentido de que toda a classe é equivalente a uma invocação de método (incluindo os parâmetros necessários para invocar o método). Dessa maneira, as informações de chamada de método podem ser serializadas ou transmitidas para uso posterior, o que é imensamente útil. Talvez seus contratados estivessem tentando algo semelhante, embora eu duvide muito.

Comentários sobre o seu exemplo:

Eu diria que é um anti-padrão muito definido. Ele não acrescenta nada, contraria o esperado e executa um processamento excessivamente longo no construtor (FTP no construtor? WTF, de fato, embora eu ache que as pessoas chamam serviços da Web dessa maneira).

Estou lutando para encontrar a citação, mas o livro de Grady Booch, "Object Solutions", tem uma anedota sobre uma equipe de desenvolvedores de C em transição para C ++. Aparentemente, eles haviam passado por treinamento e a gerência lhes disse que eles absolutamente tinham que fazer coisas "orientadas a objetos". Trazido para descobrir o que há de errado, o autor usou uma ferramenta de métrica de código e descobriu que o número médio de métodos por classe era exatamente 1 e eram variações da frase "Do It". Devo dizer que nunca encontrei esse problema específico antes, mas aparentemente pode ser um sintoma de pessoas serem forçadas a criar código orientado a objetos, sem realmente entender como fazê-lo e por quê.


11
Seu bom exemplo é uma instância de Alocação de recursos é Inicialização . É um padrão recomendado em C ++, pois facilita muito a gravação de códigos com exceção de segurança. Não sei se isso é transferido para C #. (Neste caso, o "recurso" a ser alocado é privilégios administrativos.)
Zwol

@ Zack Eu nunca pensei nisso nesses termos, mesmo que eu tenha conhecimento do RAII em C ++. No mundo C #, é chamado de padrão descartável e é muito recomendado para qualquer coisa que possua recursos (geralmente não gerenciados) para liberar após sua vida útil. A principal diferença neste exemplo é que você normalmente não faz operadores caros no construtor. Por exemplo, o método Open () de um SqlConnection ainda precisa ser chamado após o construtor.
Daniel B

14

Não.

Se todo o trabalho for realizado no construtor, significa que o objeto nunca foi necessário. Muito provavelmente, o contratado estava apenas usando o objeto para modularidade. Nesse caso, uma classe não instanciada com um método estático teria ganho o mesmo benefício sem criar nenhuma instância de objeto supérflua.

Há apenas um caso em que todo o trabalho em um construtor de objetos é aceitável. É quando o objetivo do objeto é especificamente representar uma identidade única de longo prazo, em vez de executar o trabalho. Nesse caso, o objeto seria mantido por outras razões além da realização de um trabalho significativo. Por exemplo, uma instância singleton.


7

Certamente criei muitas classes que fazem muito trabalho para calcular algo no construtor, mas o objeto é imutável, por isso disponibiliza os resultados dessa computação por meio de propriedades e nunca mais faz nada. Isso é imutabilidade normal em ação.

Eu acho que o estranho aqui é colocar código com efeitos colaterais em um construtor. Construir um objeto e jogá-lo fora sem acessar as propriedades ou métodos parece estranho (mas pelo menos nesse caso, é bastante óbvio que algo está acontecendo).

Isso seria melhor se a função fosse movida para um método público e, em seguida, injetada uma instância da classe, e o loop for chamasse o método repetidamente.


4

Existe alguma razão para fazer todo o trabalho de um objeto em um construtor?

Não.

  • Um construtor não deve ter efeitos colaterais.
    • Qualquer coisa além da inicialização de campo privado deve ser vista como um efeito colateral.
    • Um construtor com efeitos colaterais quebra o Princípio de responsabilidade única (SRP) e contraria o espírito da programação orientada a objetos (OOP).
  • Um construtor deve ser leve e nunca deve falhar.
    • Por exemplo, eu sempre tremo quando vejo um bloco try-catch dentro de um construtor. Um construtor não deve lançar exceções ou erros de log.

Alguém poderia razoavelmente questionar essas diretrizes e dizer: "Mas eu não sigo essas regras, e meu código funciona bem!" A isso, eu respondia: "Bem, isso pode ser verdade, até que não seja".

  • Exceções e erros dentro de um construtor são muito inesperados. A menos que sejam instruídos a fazer isso, os futuros programadores não estarão inclinados a cercar essas chamadas de construtores com código defensivo.
  • Se algo falhar na produção, o rastreamento de pilha gerado pode ser difícil de analisar. A parte superior do rastreamento da pilha pode apontar para a chamada do construtor, mas muitas coisas acontecem no construtor e pode não apontar para o LOC real que falhou.
    • Analisei muitos rastreamentos de pilha do .NET onde esse era o caso.

11
"A menos que sejam instruídos a fazer isso, futuros programadores não estarão inclinados a cercar essas chamadas de construtores com código defensivo". Bom ponto.
Graham

2

Parece-me que a pessoa que estava fazendo isso estava tentando burlar a limitação do Java que não permite passar funções como cidadãos de primeira classe. Sempre que você precisa passar uma função, é necessário envolvê-la em uma classe com um método semelhante applyou semelhante.

Então eu acho que ele apenas usou um atalho e usou diretamente uma classe como uma substituição de função. Sempre que você quiser executar a função, basta instanciar a classe.

Definitivamente, não é um bom padrão, pelo menos porque estamos aqui tentando descobrir, mas acho que pode ser a maneira menos detalhada de fazer algo semelhante à programação funcional em Java.


3
Tenho certeza de que esse programador em particular nunca ouviu a frase 'programação funcional'.
Kane

Eu não acho que o programador estava tentando criar uma abstração de fechamento. O equivalente em Java exigiria um polimorfismo de método abstrato (planejando possível). Não há evidência disso aqui.
27568 Kevin Naudé

1

Para mim, a regra geral é não fazer nada no construtor, exceto para inicializar variáveis ​​de membro. A primeira razão para isso é que ajuda a seguir o princípio do SRP, pois geralmente um longo processo de inicialização é uma indicação de que a classe faz mais do que deveria e a inicialização deve ser feita em algum lugar fora da classe. A segunda razão é que, dessa maneira, apenas os parâmetros necessários são passados, para que você crie menos código acoplado. Um construtor com inicialização complicada geralmente precisa de parâmetros que são usados ​​apenas para construir outro objeto, mas que não são usados ​​pela classe original.


1

Eu vou contra a maré aqui - em idiomas onde tudo tem que estar em alguma classe E você não pode ter uma classe estática, pode se deparar com a situação em que você tem um pouco isolado de funcionalidade que precisa ser coloque em algum lugar e não se encaixa em mais nada. Suas escolhas são uma classe de utilidade que cobre vários bits de funcionalidade não relacionados, ou uma classe que faz basicamente exatamente isso.

Se você tiver apenas um pouco assim, sua classe estática é sua classe específica. Portanto, você tem um construtor que faz todo o trabalho ou uma única função que faz todo o trabalho. A vantagem de fazer tudo no construtor é que isso acontece apenas uma vez, permitindo que você use campos particulares, propriedades e métodos sem se preocupar com a reutilização ou a segmentação. Se você possui um construtor / método, pode ficar tentado a permitir que os parâmetros sejam passados ​​para o método único, mas se usar campos ou propriedades particulares que apresentem possíveis problemas de encadeamento.

Mesmo com uma classe de utilitário, a maioria das pessoas não pensa ter classes estáticas aninhadas (e isso pode não ser possível, dependendo do idioma).

Basicamente, essa é uma maneira relativamente compreensível de isolar o comportamento do resto do sistema, dentro do que é permitido pelo idioma, enquanto ainda permite que você aproveite o máximo possível do idioma.

Sinceramente, eu teria respondido da mesma forma que o contratado, é um pequeno ajuste para transformá-lo em duas chamadas, não há muito o que recomendar uma sobre a outra. Quais são as dis / advatanges, provavelmente mais hipotéticas do que reais, por que tentar justificar a decisão quando ela não importa e provavelmente não foi tão decidida quanto apenas a ação padrão?


1

Existem padrões comuns em linguagens em que funções e classes são muito mais a mesma coisa, como Python, onde se usa um objeto basicamente para funcionar com muitos efeitos colaterais ou com vários objetos nomeados retornados.

Nesse caso, o trabalho em massa, se não todo, pode ser feito no construtor, pois o próprio objeto é apenas um invólucro em torno de um único pacote de trabalho e não há um bom motivo para colocá-lo em uma função separada. Algo como

var parser = XMLParser()
var x = parser.parse(string)
string a = x.LookupTag(s)

realmente não é melhor do que

 var x = ParsedXML(string)
 string a = x.LookupTag(s)

Em vez de ter diretamente os efeitos colaterais diretamente, você pode chamar o objeto para aplicar o efeito colateral na sugestão, o que oferece uma melhor visibilidade. Se eu tiver um efeito colateral ruim em um objeto, posso fazer todo o meu trabalho e, em seguida, passar pelo objeto, afetarei mal e causarei danos a ele. Identificar o objeto e isolar a chamada dessa maneira é mais claro do que passar vários desses objetos para uma função ao mesmo tempo.

x.UpdateView(v)

Você pode fazer os vários valores de retorno por meio de acessadores no objeto. Todo o trabalho está feito, mas quero tudo em contexto e não quero passar várias referências à minha função.

var x = new ComputeStatistics(inputFile)
int max = x.MaxOptions;
int mean = x.AverageOption;
int sd = x.StandardDeviation;
int k = x.Kurtosis;
...

Portanto, como um programador conjunto de Python / C #, isso não parece tão estranho para mim.


Preciso esclarecer que o exemplo dado permanece enganoso. No exemplo sem acessadores e sem valor de retorno, isso provavelmente é vestigial. Talvez o original tenha fornecido retornos de depuração ou algo assim.
precisa saber é o seguinte

1

Um caso vem à mente em que o construtor / destruidor faz todo o trabalho:

Fechaduras. Você normalmente nunca interage com um cadeado durante a vida inteira. O C # que usa arquitetura é bom para lidar com esses casos sem se referir explicitamente ao destruidor. Porém, nem todos os bloqueios são implementados dessa maneira.

Também escrevi o que equivalia a um bit de código apenas para construtor para corrigir um bug da biblioteca de tempo de execução. (Na verdade, corrigir o código teria causado outros problemas.) Não havia destruidor porque não havia necessidade de desfazer o patch.


-1

Eu concordo com AndyBursh. Parece alguma falta de conhecimento.

No entanto, é importante mencionar estruturas como NHibernate, que exigem que você codifique apenas no construtor (ao criar classes de mapa). No entanto, isso não é uma limitação da estrutura, é exatamente o que você precisa. Quando se trata de reflexão, o construtor é o único método que sempre existirá (embora possa ser privado) e isso pode ser útil se você precisar chamar um método de uma classe dinamicamente.


-4

"Fui ensinado há muito tempo que objetos instanciados em um loop geralmente são uma má idéia"

Sim, talvez você esteja se lembrando por que precisamos do StringBuilder.

Existem duas escolas de Java.

O construtor foi promovido pelo primeiro , que nos ensina a reutilizar (porque compartilhar = identidade com eficiência). No entanto, no começo de 2000, comecei a ler que a criação de objetos em Java é tão barata (em oposição ao C ++) e o GC é tão eficiente que a reutilização é inútil e a única coisa que importa é a produtividade do programador. Para produtividade, ele deve escrever código gerenciável, o que significa modularização (também conhecida como redução do escopo). Assim,

isto é mau

byte[] array = new byte[kilos or megas]
for_loop:
  use(array)

isso é bom.

for_loop:
  byte[] array = new byte[kilos or megas]
  use(array)

Fiz essa pergunta quando o forum.java.sun existia e as pessoas concordaram que prefeririam declarar a matriz o mais estreita possível.

O programador java moderno nunca hesita em declarar uma nova classe ou criar um objeto. Ele é encorajado a fazê-lo. Java dá todo o motivo para fazê-lo, com a ideia de que "tudo é um objeto" e criação barata.

Além disso, o estilo moderno de programação corporativa é que, quando você precisa executar uma ação, cria uma classe especial (ou melhor ainda, uma estrutura) que executará essa ação simples. Bons Java IDEs, que ajudam aqui. Eles geram toneladas de lixo automaticamente, como a respiração.

Então, acho que seus objetos não possuem o padrão Builder ou Factory. Não é decente criar instâncias diretamente, pelo construtor. Uma classe de fábrica especial para cada objeto é recomendada.

Agora, quando a programação funcional entra em ação, a criação de objetos se torna ainda mais simples. Você criará objetos a cada passo como respiração, sem nem perceber. Então, espero que seu código se torne ainda mais "eficiente" na nova era

"não faça nada no seu construtor. É apenas para inicialização"

Pessoalmente, não vejo razão na diretriz. Eu considero isso apenas mais um método. O código de fatoração é necessário somente quando você pode compartilhá-lo.


3
a explicação não é bem estruturada e difícil de seguir. Você apenas percorre todo o lugar com afirmações contestáveis ​​que não têm links para o contexto.
Nova Alexandria

A afirmação realmente contestável é "Os construtores devem instanciar os campos de um objeto e fazer qualquer outra inicialização necessária para deixar o objeto pronto para uso". Diga para o colaborador mais popular.
Val
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.