A modificação de um parâmetro recebido é um antipadrão? [fechadas]

60

Estou programando em Java e sempre faço conversores assim:

public OtherObject MyObject2OtherObject(MyObject mo){
    ... Do the conversion
    return otherObject;
}

No novo local de trabalho, o padrão é:

public void MyObject2OtherObject(MyObject mo, OtherObject oo){
    ... Do the conversion
}

Para mim, é um pouco malcheiroso, pois me acostumei a não alterar os parâmetros recebidos. Essa alteração de parâmetro de entrada é um antipadrão ou está OK? Tem algumas desvantagens sérias?

CsBalazsHungary
fonte
4
A resposta certa para isso será específica do idioma, uma vez que aquelas em que os parâmetros de passagem por valor os transformam efetivamente em variáveis ​​locais.
Blrfl
11
O segundo padrão às vezes é usado como uma medida de eficiência para reutilizar objetos. Supondo que ooseja um objeto que é passado para o método, não um ponteiro definido para um novo objeto. É esse o caso aqui? Se este for Java provavelmente é, se o seu C ++ pode não ser
Richard Tingle
3
Parece otimização prematura, sim. Eu recomendo que você use o primeiro formulário em geral, mas se você tiver um gargalo de desempenho, poderá usar o segundo formulário com um comentário para justificá-lo . Um comentário impediria que você e outras pessoas vejam esse código e tenham essa mesma pergunta novamente.
Keen
2
O segundo padrão é útil quando a função retorna um valor também, por exemplo, sucesso / falha. Mas não há nada realmente "errado" nisso. Realmente depende de onde você deseja criar o objeto.
GrandmasterB
12
Eu não nomearia funções implementando esses dois padrões diferentes da mesma maneira.
Casey

Respostas:

70

Não é um antipadrão, é uma prática ruim.

A diferença entre um antipadrão e uma mera má prática está aqui: definição antipadrão .

O novo estilo de local de trabalho que você mostra é uma prática ruim , tempos vestigiais ou pré-POO, de acordo com o Código Limpo do tio Bob.

Os argumentos são naturalmente interpretados como entradas para uma função.

Tudo o que força você a verificar a assinatura da função é equivalente a uma olhada duas vezes. É uma quebra cognitiva e deve ser evitada. Nos dias anteriores à programação orientada a objetos, às vezes era necessário ter argumentos de saída. No entanto, grande parte da necessidade de argumentos de saída desaparece nas linguagens OO

Tulains Córdova
fonte
7
Não vejo por que a definição que você vinculou impede que isso seja considerado um antipadrão.
Samuel Edwin Ward
7
Suponho que seja porque a lógica de que "estamos salvando CPU / mem por não criar um novo objeto, em vez disso, deveríamos reciclar um que temos e passá-lo como argumento" é tão obviamente errada para todo mundo com histórico sério de OOP que isso poderia não constituem um padrão de sempre - então não há nenhuma maneira que nós poderíamos considerá-lo um anti-padrão pela definição ...
vaxquis
11
E o caso quando um parâmetro de entrada é uma grande coleção de objetos que precisam ser modificados?
Steve Chambers
Embora eu também prefira a opção 1, e se OtherObjectfor uma interface? Somente quem chama (sabe-se) sabe que tipo concreto deseja.
user949300
@SteveChambers Acho que, nesse caso, o design da classe ou método é ruim e deve ser refatorado para evitá-lo.
piotr.wittchen 30/07
16

Citando o famoso livro de Robert C. Martin "Clean Code":

Argumentos de saída devem ser evitados

As funções devem ter um pequeno número de argumentos

O segundo padrão viola as duas regras, particularmente o argumento "output output". Nesse sentido, é pior que o primeiro padrão.

claasz
fonte
11
Eu diria que viola o primeiro, muitos argumentos significam código confuso, mas acho que dois argumentos são totalmente aceitáveis. Eu acho que essa regra sobre código limpo significa que, se você precisar de 5 ou 6 dados recebidos, deseja fazer muito em um único método, portanto deve refatorar para aumentar a legibilidade do código e o escopo do OOP.
CsBalazsHungary
2
de qualquer maneira, suspeito que essa não seja uma lei estrita, mas uma forte sugestão. Eu sei que ele não funcionará com tipos primitivos, se for um padrão amplamente difundido em um projeto, um júnior obterá idéias para passar um int e esperará que ele seja alterado como Objetos.
CsBalazsHungary
27
Independentemente de quão correto seja o Código Limpo, não acho que essa seja uma resposta valiosa sem explicar por que eles devem ser evitados. Eles não são mandamentos caindo sobre uma tábua de pedra, o entendimento é importante. Esta resposta poderia ser melhorado, fornecendo um resumo do raciocínio no livro e uma referência capítulo
Daenyth
3
Bem, inferno, se alguém escreveu em um livro, deve ser um bom conselho para qualquer situação concebível. Não há necessidade de pensar.
Casey
2
@emodendroket Mas cara, é o cara!
Pierre Arlaud
15

O segundo idioma pode ser mais rápido, porque o chamador pode reutilizar uma variável em um loop longo, em vez de cada iteração criando uma nova instância.

Eu geralmente não usaria, mas por exemplo. na programação de jogos, ele tem seu lugar. Por exemplo, observe o lote de operações Vector3f do JavaMonkey permite passar a instância que deve ser modificada e retornada como resultado.

user470365
fonte
12
bem, posso concordar se esse é o gargalo, mas onde quer que trabalhei, os gargalos geralmente são algoritmos com baixa eficiência, não clonando ou criando objetos. Eu sei menos sobre desenvolvimento de jogos.
precisa saber é o seguinte
4
@CsBalazsHungary Acredito que os problemas quando a criação de objetos é uma preocupação tendem a estar relacionados a alocações de memória e coleta de lixo, o que provavelmente seria o próximo nível de gargalos após a complexidade algorítmica (especialmente em um ambiente de memória limitada, por exemplo, smartphones e similares) .
JAB
O JMonkey também é onde eu já vi isso muito, pois geralmente é crítico para o desempenho. Eu nunca ouvi-lo chamado de "JavaMonkey", embora
Richard Tingle
3
@JAB: O GC da JVM é ajustado especificamente para o caso de objetos efêmeros. Grandes quantidades de objetos de vida muito curta são fáceis de coletar e, em muitos casos, toda a sua geração efêmera pode ser coletada com um único movimento do ponteiro.
Phoshi
2
na verdade, se for preciso criar um objeto , ele não será eficaz em termos de desempenho; se um bloco de código precisar ser fortemente otimizado para velocidade, você deve usar primitivas e matrizes nativas; por isso, considero que a afirmação nesta resposta não é válida.
vaxquis
10

Eu não acho que essas são duas partes equivalentes de código. No primeiro caso, você deve criar o otherObject. Você pode modificar a instância existente no segundo. Ambos têm seus usos. O cheiro do código preferiria um ao outro.

Eufórico
fonte
Eu sei que eles não são equivalentes. Você está certo, estamos persistindo nessas coisas, por isso faria diferença. Então você está dizendo que nenhum deles é antipadrão, é apenas uma questão de caso de uso?
CsBalazsHungary
@CsBalazsHungary Eu diria que sim. A julgar pelo pequeno pedaço de código que você forneceu.
Euphoric
Suponho que se torne um problema mais sério se você tiver um parâmetro primitivo de entrada, que obviamente não será atualizado, assim como o @claasz escreveu: deve ser evitado a menos que seja necessário.
CsBalazsHungary
11
@CsBalazsHungary No caso de argumento primitivo, a função nem funcionaria. Então você tem um problema pior do que alterar argumentos.
Euphoric
Eu diria que um júnior cairia na armadilha de tentar transformar um tipo primitivo em um parâmetro de saída. Então, eu diria que o primeiro conversor deve ser preferido, se possível.
CsBalazsHungary
7

Realmente depende do idioma.

Em Java, a segunda forma pode ser um antipadrão, mas algumas linguagens tratam a passagem de parâmetros de maneira diferente. Em Ada e VHDL por exemplo, em vez de passar por valor ou por referência, os parâmetros podem ter modos in, out, ou in out.

É um erro modificar um inparâmetro ou ler um outparâmetro, mas qualquer alteração em um in outparâmetro é passada de volta ao chamador.

Portanto, as duas formas no Ada (também são VHDL legais) são

function MyObject2OtherObject(mo : in MyObject) return OtherObject is
begin
    ... Do the conversion
    return otherObject;
end MyObject2OtherObject;

e

procedure MyObject2OtherObject(mo : in MyObject; oo : out OtherObject) is
begin
    ... Do the conversion
    oo := ... the conversion result;
end MyObject2OtherObject;

Ambos têm seus usos; um procedimento pode retornar vários valores em vários Outparâmetros enquanto uma função pode retornar apenas um único resultado. Como o objetivo do segundo parâmetro está claramente indicado na declaração do procedimento, não pode haver objeção a este formulário. Costumo preferir a função de legibilidade. mas haverá casos em que o procedimento é melhor, por exemplo, onde o chamador já criou o objeto.

Brian Drummond
fonte
3

Realmente parece mal cheiroso, e sem ver mais contexto é impossível dizer com certeza. Pode haver duas razões para fazer isso, embora existam alternativas para ambas.

Primeiro, é uma maneira concisa de implementar a conversão parcial ou deixar o resultado com valores padrão se a conversão falhar. Ou seja, você pode ter o seguinte:

public void ConvertFoo(Foo from, Foo to) {
    if (can't convert) {
        return;
    }
    ...
}

Foo a;
Foo b = DefaultFoo();
ConvertFoo(a, b);
// If conversion fails, b is unchanged

Obviamente, isso geralmente é tratado usando exceções. No entanto, mesmo que exceções precisem ser evitadas por qualquer motivo, existe uma maneira melhor de fazer isso - o padrão TryParse é uma opção.

Outro motivo é que pode ser por motivos de consistência pura, por exemplo, faz parte de uma API pública em que esse método é usado para todas as funções de conversão por qualquer motivo (como outras funções de conversão com várias saídas).

O Java não é bom em lidar com várias saídas - ele não pode ter parâmetros somente de saída, como algumas linguagens, ou ter vários valores de retorno como outros -, mas mesmo assim, você pode usar objetos de retorno.

A razão da consistência é um pouco ruim, mas, infelizmente, pode ser a mais comum.

  • Talvez os policiais de estilo no seu local de trabalho (ou na sua base de código) venham de um background não Java e tenham relutado em mudar.
  • Seu código pode ter sido uma porta de um idioma em que esse estilo é mais idiomático.
  • Talvez sua organização precise manter a consistência da API em diferentes idiomas e esse foi o estilo de denominador comum mais baixo (é idiota, mas acontece até no Google ).
  • Ou talvez o estilo tenha feito mais sentido no passado distante e tenha se transformado em sua forma atual (por exemplo, poderia ter sido o padrão TryParse, mas algum antecessor bem-intencionado removeu o valor de retorno depois de descobrir que ninguém o verificava).
congusbongus
fonte
2

A vantagem do segundo padrão é que ele força o chamador a assumir a propriedade e a responsabilidade pelo objeto criado. Não há dúvida de que o método criou ou não o objeto ou o obteve de um pool reutilizável. O chamador sabe que eles são responsáveis ​​pela vida útil e pelo descarte do novo objeto.

As desvantagens deste método são:

  1. Não pode ser usado como um método de fábrica, o chamador deve conhecer o subtipo exato OtherObjectdele e construí-lo com antecedência.
  2. Não pode ser usado com objetos que requerem parâmetros em seu construtor se esses parâmetros vierem MyObject. OtherObjectdeve ser construtível sem conhecer MyObject.

A resposta é baseada na minha experiência em c #, espero que a lógica se traduza em Java.

Rotem
fonte
Eu acho que com a responsabilidade que você mencionou, também cria um ciclo de vida "mais difícil de ver" dos objetos. Em um sistema complexo de métodos, eu preferiria uma modificação direta de um objeto, em vez de começar a examinar todo o método que estamos passando.
CsBalazsHungary
Era isso que eu estava pensando. Um tipo primitivo de injeção
Lyndon White
Outra vantagem é se OtherObject for abstrato ou uma interface. A função não tem idéia de qual tipo criar, mas o chamador pode.
user949300
2

Dada a semântica solta - myObjectToOtherObjecttambém pode significar que você move alguns dados do primeiro objeto para o segundo ou que os converte inteiramente de novo, o segundo método parece mais adequado.

No entanto, se o nome do método fosse Convert(como deveria ser se olharmos para a parte "... faça a conversão"), eu diria que o segundo método não faria sentido. Você não converte um valor em outro valor que já exista, IMO.

guillaume31
fonte