Existe algum nome para o (anti-) padrão de passagem de parâmetros que somente serão utilizados em vários níveis no fundo da cadeia de chamadas?

205

Eu estava tentando encontrar alternativas para o uso de variáveis globais em alguns códigos legados. Mas esta questão não é sobre as alternativas técnicas, estou principalmente preocupado 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 legado isso significaria que eu tenho que mudar todas as funções na longa cadeia de chamadas entre o ponto onde o valor eventualmente será usado e a função que recebe o parâmetro primeiro.

higherlevel(newParam)->level1(newParam)->level2(newParam)->level3(newParam)

em que newParam era anteriormente uma variável global no meu exemplo, mas poderia ter sido um valor previamente codificado em vez disso. O ponto é que agora o valor de newParam é obtido em higherlevel() e tem que "viajar" todo o caminho até level3() .

Eu queria saber se havia um nome (s) para esse tipo de situação / padrão onde você precisa adicionar um parâmetro a muitas funções que apenas "passam" o valor não modificado.

Espero que, usando a terminologia adequada, permita-me encontrar mais recursos sobre soluções para redesenhar e descrever essa situação para os colegas.

    
por ecerulm 31.10.2016 / 15:55
fonte

10 respostas

198

Os dados em si são chamados "tramp data" . É 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, através de intermediários.

  • Aumenta a rigidez do código, especialmente na cadeia de chamadas. Você está muito mais restrito em como refatorar qualquer método na cadeia de chamadas.
  • Distribui conhecimento sobre dados / métodos / arquitetura para lugares que não se importam nem um pouco com isso. Se você precisar declarar os dados que estão passando e a declaração exigir uma nova importação, você poluiu o espaço de nomes.

A refatoração para remover variáveis globais é difícil, e os dados de vagabundos são um método para fazê-lo, e geralmente o caminho mais barato. Tem seus custos.

    
por 31.10.2016 / 17:26
fonte
98

Eu não acho que isso, por si só, é um anti-padrão. Eu acho que o problema é que você está pensando nas funções como uma cadeia, quando na verdade você deveria pensar em cada uma delas como uma caixa preta independente ( NOTA : métodos recursivos são uma exceção notável a este conselho). / p>

Por exemplo, digamos que preciso calcular o número de dias entre duas datas do calendário para criar uma função:

int daysBetween(Day a, Day b)

Para isso, criamos uma nova função:

int daysSinceEpoch(Day day)

Então minha primeira função se torna simples:

int daysBetween(Day a, Day b)
{
    return daysSinceEpoch(b) - daysSinceEpoch(a);
}

Não há nada de 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 o método faça o que ele precisa fazer.

O que eu recomendaria é analisar cada função e começar com algumas perguntas:

  • Esta função tem um objetivo claro e focado ou é um método de "fazer algumas coisas"? Normalmente, o nome da função ajuda aqui e, se houver alguma coisa que não seja descrita pelo nome, é uma bandeira vermelha.
  • Existem muitos parâmetros? Às vezes, um método pode legitimamente precisar de muitas entradas, mas ter tantos parâmetros dificulta o uso ou o entendimento.

Se você está olhando para um amontoado de código sem um único propósito incluído em um método, você deve começar por desvendar isso. Isso pode ser entediante. Comece com as coisas mais fáceis de retirar e mude para um método separado e repita até ter algo coerente.

Se você tiver muitos parâmetros, considere refatoração de método para objeto .

    
por 31.10.2016 / 16:21
fonte
59

BobDalgleish já observou que esse padrão (anti-) é chamado de " Dados de acampamento ".

Na minha experiência, a causa mais comum de dados excessivos de tramp é ter um monte de variáveis de estado vinculadas que devem ser realmente encapsuladas em um objeto ou em uma estrutura de dados. Às vezes, pode até ser necessário aninhar um monte de objetos para organizar adequadamente os dados.

Por exemplo, considere um jogo que tenha um personagem de jogador personalizável, com propriedades como playerName , playerEyeColor e assim por diante. É claro que o jogador também tem uma posição física no mapa do jogo e várias outras propriedades, como, por exemplo, nível de vida atual e máximo, e assim por diante.

Em uma primeira iteração de tal jogo, pode ser uma escolha perfeitamente razoável transformar todas essas propriedades em variáveis globais - afinal, há apenas um jogador, e quase tudo no jogo envolve o jogador de alguma forma. Então, seu estado global pode conter variáveis como:

playerName = "Bob"
playerEyeColor = GREEN
playerXPosition = -8
playerYPosition = 136
playerHealth = 100
playerMaxHealth = 100

Mas em algum momento, você pode achar que precisa alterar esse design, talvez porque deseja adicionar um modo multijogador 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 determinada ação em seu jogo pode envolver uma cadeia de chamadas de função como, por exemplo:

mainGameLoop()
 -> processInputEvent()
     -> doPlayerAction()
         -> movePlayer()
             -> checkCollision()
                 -> interactWithNPC()
                     -> interactWithShopkeeper()

... e a função interactWithShopkeeper() faz o lojista endereçar o jogador pelo nome, então você de repente precisa passar playerName como dados de tramp através de todas essas funções. E, claro, se o lojista acha que os jogadores de olhos azuis são ingênuos e cobrará preços mais altos por eles, então você precisaria passar playerEyeColor por toda a cadeia de funções, e assim por diante.

A solução correta , neste caso, é claro para definir um objeto jogador que encapsula o nome, cor dos olhos, posição, estado de 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 player.

Além disso, várias das funções acima poderiam ser naturalmente feitas em métodos daquele objeto jogador, que automaticamente lhes daria acesso às propriedades do jogador. De certa forma, isso é apenas 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 mais natural se usado corretamente.

É claro que 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 acontece, e uma lista de personagens que não são jogadores movendo-se no mapa, e talvez itens colocados nele, e assim por diante. Você poderia passar todos aqueles ao redor como objetos tramp também, mas isso iria novamente atrapalhar 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 tenham relacionamentos permanentes ou temporários. Assim, por exemplo, o objeto jogador (e provavelmente qualquer objeto NPC também) provavelmente deveria 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) fizesse não precisa ser explicitamente dado o mapa como um parâmetro.

Da mesma forma, se nosso personagem tivesse, digamos, um cachorro de estimação que os seguisse, agruparíamos naturalmente todas as variáveis de estado que descrevem o cachorro em um único objeto e daríamos ao objeto jogador uma referência ao cachorro (para que o jogador pode, digamos, chamar o cachorro pelo nome) e vice-versa (para que o cão saiba onde o jogador está). E, é claro, nós provavelmente quereríamos fazer com que o jogador e o cachorro obtivessem as duas subclasses de um objeto "ator" mais genérico, para que pudéssemos reutilizar o mesmo código para, digamos, mover ambos pelo mapa.

Ps. Embora eu tenha usado um jogo como exemplo, existem outros tipos de programas em que tais problemas surgem também. 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 querem ser agrupadas em um ou mais objetos interligados. Se os "dados de tramp" intrusivos em suas funções consistem em configurações de opção "globais" ou consultas de banco de dados em cache ou vetores de estado em uma simulação numérica, a solução é invariavelmente identificar o contexto natural ao qual os dados pertencem e faça isso em um objeto (ou qualquer que seja o equivalente mais próximo em seu idioma escolhido).

    
por 31.10.2016 / 23:39
fonte
33

Não tenho conhecimento de 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 de tal parâmetro:

  • como uma 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 repetitivos em cadeias de chamadas

  • Então, como um trade-off, muitas vezes você pode tornar esse parâmetro uma variável de membro em uma ou mais classes, e é isso que eu chamaria de design de classe adequado .

por 31.10.2016 / 16:38
fonte
21

Eu acredito que o padrão que você está descrevendo é exatamente injeção de dependência . Vários comentadores argumentaram que este é um padrão , não um anti-padrão , e eu tenderia a concordar.

Eu também concordo com a resposta do @JimmyJames, onde ele afirma que é uma boa prática de programação tratar cada função como uma caixa preta que toma todas as suas entradas como parâmetros explícitos. Isto é, se você está escrevendo uma função que faz um sanduíche de manteiga de amendoim e geléia, você pode escrever como

Sandwich make_sandwich() {
    PeanutButter pb = get_peanut_butter();
    Jelly j = get_jelly();
    return pb + j;
}
extern PhysicalRefrigerator g_refrigerator;
PeanutButter get_peanut_butter() {
    return g_refrigerator.get("peanut butter");
}
Jelly get_jelly() {
    return g_refrigerator.get("jelly");
}

mas seria prática recomendada aplicar a injeção de dependência e escrevê-la desta forma:

Sandwich make_sandwich(Refrigerator& r) {
    PeanutButter pb = get_peanut_butter(r);
    Jelly j = get_jelly(r);
    return pb + j;
}
PeanutButter get_peanut_butter(Refrigerator& r) {
    return r.get("peanut butter");
}
Jelly get_jelly(Refrigerator& r) {
    return r.get("jelly");
}

Agora você tem uma função que documenta claramente todas as suas dependências em sua assinatura de função, o que é ótimo para a legibilidade. Afinal, é verdadeiro que, para make_sandwich , você precisa de acesso a um Refrigerator ; então a assinatura da função antiga era basicamente falsa ao não levar a geladeira como parte de suas entradas.

Como um bônus, se você fizer sua hierarquia de classes corretamente, evite o fatiamento, e assim por diante, você pode até mesmo testar a função make_sandwich , passando em MockRefrigerator ! (Pode ser necessário testar a unidade dessa maneira porque o ambiente de teste de unidade pode não ter acesso a qualquer PhysicalRefrigerator s.)

Eu entendo que nem todos os usos da injeção de dependência exigem o encanamento de um parâmetro com nomes semelhantes, muitos níveis abaixo da pilha de chamadas, então eu não estou respondendo exatamente a pergunta que você perguntou ... mas se você está procurando mais informações sobre este assunto, "injeção de dependência" é definitivamente uma palavra-chave relevante para você.

    
por 31.10.2016 / 22:27
fonte
15

Essa é basicamente a definição de acoplamento do livro didático, com um módulo tendo uma dependência que afeta profundamente outro, e isso cria um efeito cascata quando alterado. Os outros comentários e respostas estão corretos de que isso é uma melhoria em relação ao global, porque o acoplamento é agora mais explícito e mais fácil para o programador enxergar, em vez de subversivo. Isso não significa que não deva ser consertado. Você deve ser capaz de refatorar para remover ou reduzir o acoplamento, embora se estiver lá por um tempo, pode ser doloroso.

    
por 31.10.2016 / 17:16
fonte
3

Embora essa resposta não responda diretamente à sua pergunta, sinto que seria negligente deixar isso passar sem mencionar como melhorá-la (já que, como você diz, pode ser um padronizar). Espero que você e outros leitores possam obter valor a partir deste comentário adicional sobre como evitar "dados vagabundos" (como Bob Dalgleish tão proveitosamente o nomeou para nós).

Concordo com respostas que sugerem fazer algo mais OO para evitar esse problema. No entanto, outra forma de ajudar a reduzir profundamente essa passagem 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 em o nível mais alto em vez do inferior. Por exemplo, aqui está um código anterior :

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   FilterAndReportStuff(stuffs, desiredName);
}

public void FilterAndReportStuff(IEnumerable<Stuff> stuffs, string desiredName) {
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   ReportStuff(stuffs.Filter(filter));
}

public void ReportStuff(IEnumerable<Stuff> stuffs) {
   stuffs.Report();
}

Observe que isso se torna ainda pior quanto mais coisas precisam ser feitas em 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 de acordo com a função aninhada.

Minha sugestão é puxar tudo isso para um nível mais alto, onde o conhecimento das etapas requer vidas 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 dá uma ideia:

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   var filteredStuffs = stuffs.Filter(filter)
   filteredStuffs.Report();
}

Observe que a grande diferença aqui é que você não precisa passar as dependências por uma longa cadeia. Mesmo se você se achatar não apenas em um nível, mas em alguns níveis de profundidade, se esses níveis também alcançarem algum "nivelamento" de forma que o processo seja visto como uma série de etapas nesse nível, você terá feito uma melhoria. p>

Embora isso ainda seja procedimental e ainda não tenha sido transformado em objeto, é um bom passo para decidir que tipo de encapsulamento você pode alcançar ao transformar algo em uma classe. O método deep-encadeado chama no cenário before ocultar os detalhes do o que realmente está acontecendo e pode tornar o código muito difícil de entender. Embora você possa exagerar e acabar fazendo com que o código de nível mais alto saiba coisas que não deveria, ou fazer um método que faz muitas coisas, violando assim o princípio da responsabilidade única, em geral eu descobri que achatar as coisas ajuda um pouco em clareza e em fazer mudanças incrementais em direção a um código melhor.

Observe que, enquanto você está fazendo tudo isso, você deve considerar a testabilidade. As chamadas de método encadeadas realmente tornam o teste de unidade mais difícil porque você não tem um bom ponto de entrada e um ponto de saída na montagem para a fatia que deseja testar. Observe que com esse nivelamento, já que seus métodos não usam mais tantas dependências, eles são mais fáceis de testar, não exigindo o mesmo número de mocks!

Eu recentemente tentei adicionar testes de unidade a uma classe (que eu não escrevi) que levou algo como 17 dependências, e todas elas tiveram que ser ridicularizadas! Eu ainda não terminei tudo, mas eu dividi a classe em três classes, cada uma lidando com um dos nomes separados com os quais se preocupou, e baixei a lista de dependências para 12 para a pior e 8 para a melhor um.

Testabilidade irá forçar você a escrever um código melhor. Você deve estar escrevendo testes de unidade porque descobrirá que isso 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 bugs você possa ter antes de escrever os testes de unidade.

    
por 02.11.2016 / 04:03
fonte
1

Você não está literalmente violando a Lei de Deméter, mas seu problema é semelhante a isso de algumas maneiras. Como o objetivo de sua pergunta é encontrar recursos, sugiro que você leia sobre Lei de Deméter e veja quanto desse conselho se aplica à sua situação.

    
por 31.10.2016 / 20:59
fonte
1

Há casos em que os melhores (em termos de eficiência, facilidade de manutenção e facilidade de implementação) têm certas 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 suporte melhor o escopo (como variáveis estáticas privadas do C ++) para aliviar a potencial confusão (do espaço de nomes e fazer com que as coisas sejam adulteradas). Claro que isso é apenas conhecimento comum.

Mas, a abordagem declarada pelo OP é muito útil se estiver fazendo Programação Funcional.

    
por 01.11.2016 / 05:18
fonte
0

Não há nenhum padrão aqui, porque o chamador não conhece todos esses níveis abaixo e não se importa.

Alguém está chamando higherLevel (params) e espera que o higherLevel faça seu trabalho. O que o higherLevel faz com params não é da conta dos chamadores. higherLevel lida com o problema da melhor maneira possível, neste caso passando params para level1 (params). Isso é absolutamente Ok.

Você vê uma cadeia de chamadas, mas não há uma cadeia de chamadas. Existe uma função no topo fazendo o seu trabalho da melhor maneira possível. E existem outras funções. Cada função pode ser substituída a qualquer momento.

    
por 12.07.2017 / 10:28
fonte