É uma prática de programação ruim passar parâmetros como Objetos? [duplicado]

97

Então, temos um cara que gosta de escrever métodos que usam objetos como parâmetros, para que eles possam ser "muito flexíveis". Então, internamente, ele faz a transmissão direta, reflexão ou sobrecarga de método para lidar com os diferentes tipos.

Eu sinto que esta é uma prática ruim, mas não posso explicar exatamente por quê, exceto que isso torna mais difícil a leitura. Existem outras razões mais concretas por que isso seria uma má prática? Quais seriam esses?

Então, algumas pessoas pediram um exemplo. Ele tem uma interface definida, algo como:

public void construct(Object input, Object output);

E ele planeja usá-los colocando vários em uma lista, então ele meio que constrói bits e os adiciona ao objeto de saída, assim:

for (ConstructingThing thing : constructingThings)
    thing.construct(input, output);
return output;

Então, na coisa que implementa construct , há uma coisa de reflexão instável que encontra o método correto que corresponde à entrada / saída e a chama, passando entrada / saída.

Eu continuo dizendo a ele, funciona, entendo que funciona, mas também colocando tudo em uma única aula. Estamos falando de reutilização e manutenção, e acho que ele está se constrangendo e tem menos reutilização do que pensa. A manutenção, embora provavelmente alta para ele agora, provavelmente será muito baixa para ele e qualquer outra pessoa no futuro.

    
por Risser 20.09.2013 / 19:57
fonte

18 respostas

103

De um ponto de vista prático , vejo esses problemas:

  • Um inchaço de possíveis erros de tipo de execução - a menos que muita verificação de tipo dinâmico que poderia ser evitada com o Java incluísse um verificador de tipos strong.
  • Muitos elencos desnecessários
  • Dificuldade em entender o que um método faz por sua assinatura

De um ponto de vista teórico , vejo esses problemas:

  • A falta de contratos da interface de uma classe. Se todos os parâmetros forem do tipo Object , você não estará declarando nada informativo para as classes do cliente.
  • A falta de possibilidades de sobrecarga
  • A incorreção da substituição. Você é capaz de substituir um método e alterar seus tipos de parâmetros, quebrando assim tudo o que é relacionado à herança.
por 20.09.2013 / 21:22
fonte
107

O método viola o princípio da substituição de Liskov .

Se um método aceitar um parâmetro do tipo Object , devo esperar que qualquer objeto que eu passar para o método funcione.

O que parece é que apenas certas subclasses de Object são permitidas. A única maneira de saber quais tipos são permitidos é conhecer os detalhes do método. Em outras palavras, o método é menos flexível do que o anunciado.

    
por 14.05.2013 / 20:14
fonte
53

Eu consideraria as seguintes implicações diretas:

Legibilidade

Acho que trabalhar com ele não é exatamente a experiência mais agradável do mundo. Você nunca sabe o que o tipo vai ser no final ou o que acontece com os parâmetros. Eu duvido que você aprecie desperdiçar seu tempo com isso.

Tipo de assinatura

Em Java tudo é um objeto. Então, o que esse método faz no final?

Velocidade

Reflexão, lançamentos, verificações de tipo, etc., todos contribuem para um desempenho mais lento. Por quê? Porque em sua opinião, torna as coisas flexíveis.

Contra a natureza

Java tem sido strongmente tipado desde o nascimento. Diga a ele para começar a escrever JavaScript ou Python ou Ruby se ele quiser um dialeto digitado dinamicamente. Sua experiência provavelmente é strongmente baseada em um deles.

Flexibilidade

Para usar o mesmo termo que o colega em questão, o Java já descobriu a flexibilidade há muito tempo. Não há nenhum ponto para olhar para ele com os óculos de cavalo colocados por uma linguagem digitada dinamicamente.

Interfaces, polimorfismo, covariância & contra-variância, genéricos, herança, classes abstratas e sobrecarga de métodos são apenas algumas das coisas que contribuem para a chamada "flexibilidade". A flexibilidade é dada pelo fato de ser trivial especializar virtualmente qualquer coisa, de qualquer tipo dado a outro (o caso usual significa especializar um tipo superior direto ou um tipo inferior).

Documentação

Um dos principais objetivos da programação é escrever código reutilizável e estável. Como é a documentação do seu exemplo? Qual é o tempo de aprendizado para outro desenvolvedor antes de poder usar essas coisas? Eu sugeriria que seu cara escrevesse código assembler auto-modificador e permanecesse responsável por isso.

    
por 15.05.2013 / 14:51
fonte
41

Há muitas boas respostas aqui já. Eu quero adicionar um pensamento secundário:

Esse padrão é provavelmente um " código cheiro ."

Em outras palavras, se você precisar passar um Objeto ao seu método para flexibilidade "final", você provavelmente terá um método muito mal definido . Ter parâmetros strongmente tipados indica um "contrato" para o chamador (preciso de coisas que se pareçam com "isto"). Ser incapaz de especificar tal contrato provavelmente significa que seu método faz muito ou tem um escopo mal definido.

    
por 20.09.2013 / 21:33
fonte
22

Ele tira o tipo de verificação que o compilador faz e pode levar a mais exceções de tempo de execução quando o objeto é convertido para o tipo concreto. Assim, o usuário final verá a exceção não tratada, a menos que as conversões inválidas sejam capturadas e tratadas adequadamente.

    
por 14.05.2013 / 18:55
fonte
16

Concordo com as outras respostas de que isso geralmente é uma prática ruim.

Há um caso específico em que acho que passar um Objeto pode ser superior: quando você está lidando com Strings, por exemplo, se estiver criando XML ou JSON. Nesse caso, em vez de algo como:

public void addAttribute(String s) { 
  //... add s to the XML
}

Eu prefiro:

public void addAttribute(Object o) {
   String s = String.valueOf(o);  // perhaps check for null explicitly
   // now add s to the XML
}

Isso economiza intermináveis String.valueOf no código de chamada. Além disso, funciona muito bem com vararg . E, com o autoboxing, você pode passar ints , floats , etc ... Como todos os Objetos possuem toString() , isso, sem dúvida, não viola Liskov etc ...

    
por 20.09.2013 / 21:47
fonte
5

sim. java deve ser estritamente typecast ed. Mas ter Object como parâmetro virtualmente violará essa regra. Além disso, tornará seu programa mais propenso a erros.

E essa é uma das principais razões pelas quais generics foram introduzidas.

    
por 14.05.2013 / 18:57
fonte
5

Ser flexível e capaz de lidar com vários tipos de dados é exatamente o método pelo qual a sobrecarga e as interfaces foram criadas. O Java já possui os recursos que tornam o manuseio dessas situações mais simples, garantindo a segurança do tempo por meio da verificação de tipos.

Além disso, se for impossível definir de forma restrita em quais dados o método opera, esse método pode estar fazendo muito e precisa ser decomposto em métodos menores.

Eu gosto de uma regra que eu li sobre (em Code Complete, eu acho). A regra afirma que você deve ser capaz de dizer o que uma função ou método faz apenas lendo seu nome. Se é realmente difícil nomear claramente um método \ função usando essa idéia (ou tem que usar And no nome) então a função / método está realmente fazendo muito.

    
por 14.05.2013 / 19:29
fonte
2

Primeiro, se você passar os parâmetros como o objeto concreto, qualquer erro de usar o objeto errado (instância de uma classe não esperada) será detectado em tempo de compilação, portanto, será mais rápido corrigi-lo. No outro caso, o erro será mostrado no tempo de execução e será mais difícil de detectar e corrigir.

Em segundo lugar, o código de verificação da classe real da instância, reflexão, etc, será mais lento do que uma chamada direta

    
por 14.05.2013 / 18:57
fonte
2

A regra que eu sigo é ... as definições de parâmetro devem ser tão específicas quanto as necessárias pelo método, e nem mais nem menos.

Se tudo que eu preciso é iterar um enumerável, o param é definido como um enumerável, não um array ou uma lista. Isso permite que qualquer tipo de enumerável seja usado, evitando a conversão para um tipo específico.

Eu também não o definirei como um objeto porque preciso de um enumerável. Qualquer objeto que não seja enumerável não funcionará nesse método. É confuso ler uma assinatura de método e não saber o que isso significa, e somente o teste de tempo de execução pode encontrar o erro.

    
por 14.05.2013 / 20:38
fonte
2

O que ele faz é jogar fora todas as vantagens de ter um sistema de tipo estático.

Simplesmente: É muito menos claro o que esses métodos fazem, tanto para um compilador quanto para as pessoas que leem o código.

Uma das maiores razões para um sistema de tipo estático é ter garantias em tempo de compilação sobre o que um programa faz . O compilador pode verificar se um trecho de código é usado corretamente e, caso contrário, emitir um erro no tempo do compilador. Métodos como o seu colega está escrevendo são muito mais propensos a erros de tempo de execução, porque as verificações do tempo do compilador estão faltando. Esteja preparado para erros em tempo de execução e para escrever muito mais testes.

Outro grande problema é que não está claro o que um método faz. Ler esse código, mantê-lo ou usá-lo é uma grande dor.

O Java adicionou genéricos para dar garantias mais strongs em tempo de compilação sobre o código, a Scala está indo ainda mais longe com sua abordagem funcional e um sistema de tipos sofisticado com tipos covariantes e contra-variantes. Seu colega está indo contra essa linha de desenvolvimento, jogando fora todas as suas vantagens.

Se ele usasse métodos adequadamente tipados e sobrecarregados, ele (e todos vocês) ganhariam:

  • Verificação do tempo do compilador para uso correto desses métodos.
  • Indicação clara de quais combinações de argumentos são permitidas e quais não são.
  • A possibilidade de documentar de forma adequada e separada cada uma dessas combinações.
  • Legibilidade e facilidade de manutenção do código.
  • O ajudaria a dividir seus métodos em partes menores, mais fáceis de depurar, testar e manter. (Veja também Como convencer seu colega desenvolvedor a escrever métodos curtos? .)
por 23.05.2017 / 14:40
fonte
2

Na verdade, eu tive que trabalhar em um pacote como esse uma vez, era um pacote GUI - o designer disse que ele veio de uma experiência Pascal e pensou em passar por aí, mas uma classe base em todos os lugares era uma idéia incrível.

Em 20 anos, trabalhar com este kit de ferramentas foi uma das experiências mais irritantes da minha carreira.

Até que você tenha que trabalhar com código como esse, você não percebe o quanto depende dos tipos de dados em seus parâmetros para orientá-lo. Se eu dissesse que tinha um método "connect (Object dest)", o que isso significa? Mesmo que você veja "connect (Object website)" não é de muita ajuda, mas "connect (website String)" ou "connect (URL website)" você nem precisa pensar sobre isso.

Você pode pensar que "connect (Object website)" é uma ótima idéia porque pode ter uma string ou uma URL, mas isso significa apenas que eu tenho que adivinhar quais casos o programador decidiu apoiar e quais não. Na melhor das hipóteses, ele move a maior parte da tentativa e erro do desenvolvimento do tempo de codificação para o tempo de compilação, na pior das hipóteses, torna todo o processo de desenvolvimento enlameado e chato.

Eu (e a maioria dos programadores Java, eu acredito) constantemente descobrem cadeias de chamadas API examinando um grupo de parâmetros para construtores para ver qual construtor usar, então como construir os objetos que você precisa passar até chegar aos objetos que você ter ou pode fazer - é como montar um quebra-cabeça em vez de procurar por exemplos incompletos (minha experiência pré-java com C).

Embora construir código como um quebra-cabeça pareça improvável, ele funciona incrivelmente bem - o tempo todo.

Eu sei que isso foi respondido, mas essa foi uma experiência bastante chata que eu realmente me senti motivada para ter certeza de que o mínimo possível de pessoas precisaria passar por isso novamente.

    
por 15.05.2013 / 07:06
fonte
1

Para dar algo, digite:

  1. Usuários uma interface para codificar com uma descrição semântica.
  2. Compila uma interface para analisar para permitir que seus otimizadores aumentem o tempo de execução do código ou diminuam o tamanho do código com mais facilidade.
  3. Ligação inicial (estática) que detecta erros de código no início do ciclo de desenvolvimento.

Usar apenas um objeto não transmite quase nenhuma informação, removendo assim todos esses recursos que foram colocados na linguagem pelas razões listadas acima.

Além disso, o sistema de tipos foi projetado para flexibilidade com suas relações is-a, permitindo todas essas condições. Então use-o para sua vantagem. Não desperdice fazendo código insustentável.

Pode haver uma razão específica para fazer algo assim em um determinado momento, mas, para isso, isso genericamente todo o tempo é uma prática ruim. Se você estiver fazendo isso, peça a outras pessoas que examinem o problema e ver se isso pode ser feito de outra maneira. Na maioria dos casos (99,999%) pode ser.

Naquele 0,001% que não pode ser, pelo menos você está a bordo e sabe o que está acontecendo (embora você provavelmente só precise de um novo olhar sobre o problema).

    
por 14.05.2013 / 20:31
fonte
1

Sim, isso é uma prática ruim porque não garante que o método seja capaz de fazer o que for necessário para qualquer entrada específica. O objetivo de fazer um método que seja amplamente utilizável não é uma meta ruim, mas existem maneiras melhores de realizá-lo.

Para este caso em particular, parece uma situação perfeita para o uso de interfaces. Uma interface garantiria que os objetos sendo passados para o método fornecessem as informações necessárias e permitissem que a transmissão fosse evitada. Há algumas situações em que isso não funcionará bem (como se ele tivesse que trabalhar com classes seladas ou base para as quais você não pode adicionar uma implementação de interface), mas, no geral, um método só deve ter entradas válidas para ele parâmetros.

    
por 14.05.2013 / 23:11
fonte
0

Grandes respostas detalhadas até agora, eu realmente tenho que dizer que antes mesmo de entrar em filosofia e metodologia de alto nível, elas estão sendo preguiçosas, curtas e simples.

Eles não estão fornecendo boas informações em seus códigos para os outros manterem e entenderem, e estão indo contra o design da linguagem de programação disponível. Eu amo o PERL, eu amo o Objective-C. No entanto, não faço esforços para dobrar Java, C # ou C ++ para esse assunto, em relação às minhas preferências, à custa de ter um código que possa ser mantido, legível e eficiente.

Sim, se eles precisarem alterar como uma função funciona ou quais são as entradas, eles salvam algumas alterações de linha aqui e algumas etapas de refatoração. No entanto, o custo para outros que precisam manter o código é maior, sem mencionar que os IDEs mais modernos, como eclipse e Xcode, podem manipular a refatoração automaticamente com um simples comando de clique direito. Além disso, como outros já mencionaram, o custo de processamento é aumentado com cada conversão, com cada tipo de objeto desconhecido que é passado, e imagino acelerações básicas que o tempo de execução executaria sabendo que as classes apropriadas também são lançadas pela janela.

A sintaxe do açúcar é sempre melhor que o sal para o desenvolvedor, mas uma dieta balanceada garante a saúde do projeto e dos outros envolvidos (cliente, aplicativo, outros desenvolvedores, etc.)

    
por 14.05.2013 / 22:22
fonte
-2

Não há problema com isso. Em linguagens que não possuem variáveis digitadas, é isso que acontece em todas as funções. Parâmetros formais são apenas valores, e o tipo está no objeto que é passado, não no parâmetro formal.

People Get Stuff Feito (TM) em Lisp, Python, Ruby, ...

Embora você se beneficie menos da verificação do tipo de tempo de compilação, também sofre menos com a verificação do tipo de tempo de compilação.

A solução real é usar alguma linguagem dinâmica que tenha como alvo a máquina virtual Java, em vez de escrever código Java feio em que cada terceiro identificador é a palavra Object .

Eu faz sentido ter uma função completamente genérica chamada construct , que aceita objetos arbitrários.

Acho que gostaria de colaborar com este programador. Levaria um pouco da picada de usar uma linguagem de programação macabra.

Observe que esse tipo de código ocorre em C na implementação do suporte em tempo de execução para linguagens dinâmicas implementadas em C. E.g. você pode ver algo assim em um Lisp implementado em C:

/* Map each object in a list through a function, in order to produce
   a list of the return values. */

val mapcar(val function, val list)
{
   val iter = car(list);
   val out = nil;  /* nil is really a null pointer */

   for (iter = car(list); iter; iter = cdr(iter))
      push(funcall_1(function, car(iter)), out);  /* push is a C macro */

   return nreverse(out); /* destructively reverse list */
}

O tipo val é realmente algo como:

typedef struct object *val;

e struct object é uma estrutura complicada que sobrepõe vários tipos de informações de tipos para vários tipos de objetos.

Desenvolvi uma linguagem de programação muito agradável, cujos elementos internos são todos feitos neste estilo, que eu mantinha deliberadamente o mais limpo possível: mais do que nos aspectos internos de alguma outra implementação de linguagem que adotam uma abordagem semelhante.

Neste estilo, coisas incríveis são possíveis em uma pequena quantidade de código C.

    
por 14.05.2013 / 23:35
fonte
-3

Suponha por um minuto que "Object guy" tenha razão para usar um objeto genérico "frouxamente". Nós não sabemos suas razões, mas isso me lembra o valor de usar herança e genéricos (modelos). Na chance de que Object-guy esteja realmente tentando fazer sentido, este é o sentido que ele pode estar tentando fazer:

Sempre baseio meus objetos em um único objeto base. Esse objeto base lida com tarefas adicionais, como manipulação de exceção, criação de log, criação de perfil e coleta de lixo.

Eu sempre baseio objetos matemáticos (aqueles que podem suportar operadores matemáticos) em uma única classe base. Isso ajuda a fornecer utilitários para manipular exceções no caso de uma classe derivada não manipular uma determinada operação. Por exemplo, usando uma série de Taylor no evento, uma representação explícita para uma função não está disponível.

Então eu não sei as razões dos objetos-objeto são: talvez elas sejam apenas porque ele gosta de digitação dinâmica oferecida por outras linguagens. Mas talvez ele esteja tentando avaliar o valor da herança.

Ele também pode querer explorar Generics (Templates). Eu os usei em C ++ para definir imagens de duas, três e quatro dimensões, em que os pixels podem ser bytes, ints, floats, complexos ou espectros. Eu tive que fazer um pouco de elenco, mas o pay-off foi significativo em termos de separar a geometria das imagens do cálculo de pixels.

    
por 14.05.2013 / 23:18
fonte
-6

Não se esqueça de que esse é um bom padrão para idiomas como JavaScript, que não suportam a passagem de parâmetros pelo nome. Considere uma função

function dialog( message, cancelButton, okText, notOkText ) { ... } 

que tem que ser chamado como

dialog( "...", true, "...", "..." );

Quando você estuda o código do cliente, não é fácil lembrar o significado, digamos, do terceiro parâmetro da chamada. A legibilidade do código do cliente sofre. Portanto, é melhor declará-lo com um objeto de parâmetro

function dialog( options ) { ... }

que permite chamá-lo com parâmetros nomeados:

dialog({ 
  message : "...",
  cancelButton: true,
  okText: "...",
  notOkText: "..."
  })

Princípio: Devemos reduzir o número de parâmetros de uma função (zero é excelente, um é frequente, dois e mais devem ser raros)!

    
por 14.05.2013 / 22:23
fonte