Eu estava tentando encontrar alternativas para o uso da variável global em algum código legado. Mas esta questão não é sobre as alternativas técnicas, estou principalmente preocupada com a terminologia .
A solução óbvia é passar um parâmetro para a função em vez de usar um global. Nesta base de código herdada, isso significa que eu tenho que alterar todas as funções na longa cadeia de chamadas entre o ponto em que o valor será eventualmente usado e a função que recebe o parâmetro primeiro.
higherlevel(newParam)->level1(newParam)->level2(newParam)->level3(newParam)
onde newParam
anteriormente era uma variável global no meu exemplo, mas poderia ter sido um valor anteriormente codificado anteriormente. O ponto é que agora o valor de newParam é obtido em higherlevel()
e precisa "viajar" até o fim level3()
.
Eu queria saber se havia um nome para este tipo de situação / padrão em que você precisa adicionar um parâmetro a muitas funções que apenas "passam" o valor não modificado.
Felizmente, usar a terminologia adequada me permitirá encontrar mais recursos sobre soluções para redesenhar e descrever essa situação para os colegas.
Respostas:
Os dados em si são chamados de "dados vagabundos" . É um "cheiro de código", indicando que um pedaço de código está se comunicando com outro pedaço de código à distância, por intermediários.
A refatoração para remover variáveis globais é difícil, e os dados vagabundos são um método para fazê-lo, e geralmente a maneira mais barata. Tem seus custos.
fonte
Eu não acho que isso, por si só, seja um anti-padrão. Acho que o problema é que você está pensando nas funções como uma cadeia, quando na verdade você deve pensar em cada uma delas como uma caixa preta independente ( NOTA : métodos recursivos são uma exceção notável a esse conselho.)
Por exemplo, digamos que eu precise calcular o número de dias entre duas datas do calendário para criar uma função:
Para fazer isso, crio uma nova função:
Então minha primeira função se torna simplesmente:
Não há nada antipadrão nisso. Os parâmetros do método daysBetween estão sendo passados para outro método e nunca são referenciados no método, mas ainda são necessários para que esse método faça o que precisa.
O que eu recomendaria é analisar cada função e começar com algumas perguntas:
Se você estiver visualizando uma confusão de códigos sem um único objetivo agrupado em um método, comece por desvendar isso. Isso pode ser entediante. Comece com as coisas mais fáceis de usar, passe para um método separado e repita até obter algo coerente.
Se você tiver muitos parâmetros, considere a refatoração de Método para objeto .
fonte
BobDalgleish já observou que esse (anti-) padrão é chamado de " dados vagabundos ".
Na minha experiência, a causa mais comum de dados excessivos de tramp é ter um monte de variáveis de estado vinculadas que realmente devem ser encapsuladas em um objeto ou uma estrutura de dados. Às vezes, pode até ser necessário aninhar um monte de objetos para organizar adequadamente os dados.
Para um exemplo simples, considere um jogo que tem um personagem personalizável player, com propriedades como
playerName
,playerEyeColor
e assim por diante. Obviamente, o jogador também tem uma posição física no mapa do jogo e várias outras propriedades como, por exemplo, o nível de saúde atual e máximo, e assim por diante.Em uma primeira iteração de um jogo desse tipo, pode ser uma escolha perfeitamente razoável transformar todas essas propriedades em variáveis globais - afinal, existe apenas um jogador e quase tudo no jogo de alguma forma envolve o jogador. Portanto, seu estado global pode conter variáveis como:
Mas, em algum momento, você pode achar que precisa mudar esse design, talvez porque queira adicionar um modo multiplayer ao jogo. Como primeira tentativa, você pode tentar tornar todas essas variáveis locais e passá-las para funções que precisam delas. No entanto, você pode descobrir que uma ação específica do seu jogo pode envolver uma cadeia de chamadas de funções como, digamos:
... e a
interactWithShopkeeper()
função faz o lojista endereçar o jogador pelo nome, de modo que agora você precisa repentinamente passarplayerName
como dados vagabundos por todas essas funções. E, é claro, se o lojista achar que os jogadores de olhos azuis são ingênuos e cobrar preços mais altos por eles, será necessário passarplayerEyeColor
por toda a cadeia de funções e assim por diante.A solução adequada , nesse caso, é definir um objeto de jogador que encapsule o nome, a cor dos olhos, a posição, a saúde e quaisquer outras propriedades do personagem do jogador. Dessa forma, você só precisa passar esse único objeto para todas as funções que de alguma forma envolvem o jogador.
Além disso, várias das funções acima podem ser naturalmente transformadas em métodos desse objeto de jogador, o que lhes daria acesso automaticamente às propriedades do jogador. De certa forma, isso é apenas um açúcar sintático, já que chamar um método em um objeto efetivamente passa a instância do objeto como um parâmetro oculto para o método, mas faz com que o código pareça mais claro e natural se usado corretamente.
Obviamente, um jogo típico teria muito mais estado "global" do que apenas o jogador; por exemplo, você quase certamente teria algum tipo de mapa no qual o jogo ocorre, e uma lista de personagens não-jogadores que se movem no mapa, e talvez itens colocados nele, e assim por diante. Você também pode passar todos esses objetos ao redor como objetos vagabundos, mas isso atrapalhará novamente seus argumentos de método.
Em vez disso, a solução é fazer com que os objetos armazenem referências a quaisquer outros objetos com os quais eles tenham relacionamentos permanentes ou temporários. Assim, por exemplo, o objeto jogador (e provavelmente também qualquer objeto NPC) provavelmente deve armazenar uma referência ao objeto "mundo do jogo", que teria uma referência ao nível / mapa atual, para que um método como
player.moveTo(x, y)
esse não precise receber explicitamente o mapa como parâmetro.Da mesma forma, se o personagem do jogador tivesse, digamos, um cachorro de estimação que os seguisse, naturalmente agruparíamos todas as variáveis de estado que descrevem o cachorro em um único objeto, e forneceríamos ao objeto do jogador uma referência ao cachorro (para que o jogador possa , digamos, chame o cachorro pelo nome) e vice-versa (para que ele saiba onde está o jogador). E, é claro, provavelmente quereríamos fazer com que o jogador e o cachorro objetem as duas subclasses de um objeto "ator" mais genérico, para que possamos reutilizar o mesmo código para, digamos, mover os dois ao redor do mapa.
Ps. Embora eu tenha usado um jogo como exemplo, há outros tipos de programas em que esses problemas também surgem. Na minha experiência, porém, o problema subjacente tende a ser sempre o mesmo: você tem um monte de variáveis separadas (locais ou globais) que realmente desejam agrupar-se em um ou mais objetos interligados. Se os "dados de rastreamento" que se intrometem em suas funções consistem em configurações de opções "globais" ou consultas em banco de dados em cache ou vetores de estado em uma simulação numérica, a solução é sempre identificar o contexto natural ao qual os dados pertencem e transformá-lo em um objeto (ou o equivalente mais próximo no idioma escolhido).
fonte
foo.method(bar, baz)
emethod(foo, bar, baz)
há outras razões (incluindo polimorfismo, encapsulamento, localidade etc.) para preferir o primeiro.Não conheço um nome específico para isso, mas acho que vale a pena mencionar que o problema que você descreve é apenas o problema de encontrar o melhor compromisso para o escopo desse parâmetro:
como variável global, o escopo é muito grande quando o programa atinge um determinado tamanho
como um parâmetro puramente local, o escopo pode ser muito pequeno quando leva a muitas listas de parâmetros repetitivas nas cadeias de chamadas
portanto, como uma troca, muitas vezes você pode transformar esse parâmetro em uma variável de membro em uma ou mais classes, e isso é o que eu chamaria de design de classe adequado .
fonte
Acredito que o padrão que você está descrevendo é exatamente a injeção de dependência . Vários comentaristas argumentaram que esse é um padrão , não um antipadrão , e eu tenderia a concordar.
Também concordo com a resposta de @ JimmyJames, onde ele afirma que é uma boa prática de programação tratar cada função como uma caixa preta que aceita todas as suas entradas como parâmetros explícitos. Ou seja, se você estiver escrevendo uma função que faz um sanduíche de manteiga de amendoim e geléia, você pode escrevê-la como
mas seria uma prática melhor aplicar a injeção de dependência e escrevê-la assim:
Agora você tem uma função que documenta claramente todas as suas dependências em sua assinatura de função, o que é ótimo para facilitar a leitura. Afinal, é verdade que, para
make_sandwich
você precisar de acesso a umRefrigerator
; portanto, a assinatura antiga da função era basicamente falsa por não levar a geladeira como parte de suas entradas.Como bônus, se você fizer sua hierarquia de classes corretamente, evitar fatias e assim por diante, você pode até testar a
make_sandwich
função da unidade passando em aMockRefrigerator
! (Pode ser necessário testá-lo dessa maneira, porque seu ambiente de teste de unidade pode não ter acesso a nenhumPhysicalRefrigerator
s.)Entendo que nem todos os usos da injeção de dependência exigem o encanamento de um parâmetro com nome semelhante em muitos níveis na pilha de chamadas, por isso não estou respondendo exatamente à pergunta que você fez ... mas se você estiver procurando por mais leituras sobre esse assunto, "injeção de dependência" é definitivamente uma palavra-chave relevante para você.
fonte
Refrigerator
em umIngredientSource
, ou até generalizar a noção de "sanduíche" emtemplate<typename... Fillings> StackedElementConstruction<Fillings...> make_sandwich(ElementSource&)
; isso é chamado de "programação genérica" e é razoavelmente poderoso, mas certamente é muito mais misterioso do que o OP realmente deseja entrar agora. Sinta-se à vontade para abrir uma nova pergunta sobre o nível adequado de abstração para programas sanduíche. ;)make_sandwich()
.Essa é praticamente a definição de acoplamento do livro , um módulo com uma dependência que afeta profundamente outro e que cria um efeito cascata quando alterado. Os outros comentários e respostas estão corretos, dizendo que isso é uma melhoria em relação ao global, porque o acoplamento agora é mais explícito e mais fácil para o programador ver, em vez de subversivo. Isso não significa que não deva ser consertado. Você deve refatorar para remover ou reduzir o acoplamento, embora se ele estiver lá há um tempo, pode ser doloroso.
fonte
level3()
necessárionewParam
, isso é acoplamento, com certeza, mas de alguma forma diferentes partes do código precisam se comunicar. Eu não chamaria necessariamente um parâmetro de função de mau acoplamento se essa função fizesse uso do parâmetro. Penso que o aspecto problemático da cadeia é o acoplamento adicional introduzidolevel1()
elevel2()
que não tem utilidadenewParam
senão transmiti-lo. Boa resposta, +1 para acoplamento.Embora essa resposta não responda diretamente à sua pergunta, acho que seria uma negligência deixar passar sem mencionar como melhorá-la (já que, como você diz, pode ser um antipadrão). Espero que você e outros leitores possam obter valor com este comentário adicional sobre como evitar "tramp data" (como Bob Dalgleish o nomeou tão útil para nós).
Concordo com as respostas que sugerem fazer algo mais OO para evitar esse problema. No entanto, outra maneira de ajudar a reduzir profundamente essa transmissão de argumentos sem simplesmente pular para " apenas passar uma classe em que você costumava passar muitos argumentos! " É refatorar para que algumas etapas do seu processo ocorram no nível mais alto, em vez de no nível mais baixo. 1. Por exemplo, aqui estão alguns códigos anteriores :
Observe que isso se torna ainda pior quanto mais coisas precisam ser feitas
ReportStuff
. Você pode ter que passar na instância do Reporter que deseja usar. E todos os tipos de dependências que precisam ser entregues, funcionam em função aninhada.Minha sugestão é puxar tudo isso para um nível superior, onde o conhecimento das etapas requer vida em um único método, em vez de se espalhar por uma cadeia de chamadas de método. Claro que seria mais complicado em código real, mas isso lhe dá uma idéia:
Observe que a grande diferença aqui é que você não precisa passar as dependências por uma longa cadeia. Mesmo se você nivelar não apenas um nível, mas alguns níveis de profundidade, se esses níveis também atingirem algum "nivelamento", de modo que o processo seja visto como uma série de etapas nesse nível, você terá feito uma melhoria.
Embora isso ainda seja processual e nada tenha sido transformado em objeto ainda, é um bom passo para decidir que tipo de encapsulamento você pode obter transformando algo em classe. O método profundamente encadeado chama no cenário anterior oculta os detalhes do que realmente está acontecendo e pode tornar o código muito difícil de entender. Embora você possa exagerar e acabar divulgando o código de nível superior sobre coisas que não deveriam, ou criar um método que faça muitas coisas violando o princípio da responsabilidade única, em geral, descobri que achatar as coisas um pouco ajuda com clareza e em fazer alterações incrementais em direção a um código melhor.
Observe que enquanto você faz tudo isso, considere a testabilidade. As chamadas do método encadeado realmente tornam o teste de unidade mais difícil, porque você não possui um bom ponto de entrada e ponto de saída na montagem para a fatia que deseja testar. Observe que, com esse achatamento, como seus métodos não precisam mais de tantas dependências, eles são mais fáceis de testar, não exigindo tantas zombarias!
Recentemente, tentei adicionar testes de unidade a uma classe (que não escrevi) que usava algo como 17 dependências, e todas tinham que ser ridicularizadas! Ainda não resolvi tudo, mas dividi a classe em três classes, cada uma lidando com um dos substantivos separados com os quais se preocupava, e reduzi a lista de dependências para 12 para o pior e para 8 para o pior. o melhor.
A testabilidade forçará você a escrever um código melhor. Você deve escrever testes de unidade, porque descobrirá que ele faz com que você pense sobre seu código de maneira diferente e escreverá um código melhor desde o início, independentemente de quantos erros você possa ter antes de escrever os testes de unidade.
fonte
Você não está literalmente violando a Lei de Demeter, mas seu problema é semelhante ao de algumas maneiras. Como o objetivo da sua pergunta é encontrar recursos, sugiro que você leia sobre a Lei de Demeter e veja quanto desses conselhos se aplica à sua situação.
fonte
Há casos em que o melhor (em termos de eficiência, capacidade de manutenção e facilidade de implementação) para ter determinadas variáveis como globais, em vez da sobrecarga de sempre passar tudo ao redor (digamos que você tenha 15 ou mais variáveis que precisam persistir). Portanto, faz sentido encontrar uma linguagem de programação que ofereça suporte ao escopo melhor (como variáveis estáticas privadas do C ++) para aliviar a confusão potencial (de espaço para nome e que as coisas sejam adulteradas). Claro que isso é apenas conhecimento comum.
Mas, a abordagem declarada pelo OP é muito útil se alguém estiver fazendo Programação Funcional.
fonte
Não existe nenhum anti-padrão aqui, porque o chamador não conhece todos esses níveis abaixo e não se importa.
Alguém está chamando o upperLevel (params) e espera que o upperLevel faça seu trabalho. O que o upperLevel faz com os parâmetros não é da conta dos chamadores. upperLevel lida com o problema da melhor maneira possível, nesse caso, passando params para level1 (params). Tudo bem.
Você vê uma cadeia de chamadas - mas não há uma cadeia de chamadas. Existe uma função no topo fazendo seu trabalho da melhor maneira possível. E há outras funções. Cada função pode ser substituída a qualquer momento.
fonte