Por que a codificação dos nomes dos argumentos nos nomes das funções é mais comum? [fechadas]

47

Em Código Limpo, o autor fornece um exemplo de

assertExpectedEqualsActual(expected, actual)

vs

assertEquals(expected, actual)

com o primeiro alegado ser mais claro, porque elimina a necessidade de lembrar para onde vão os argumentos e o potencial uso indevido resultante disso. No entanto, nunca vi um exemplo do antigo esquema de nomenclatura em nenhum código e o vi o tempo todo. Por que os codificadores não adotam o primeiro, se é, como afirma o autor, mais claro que o último?

EternalStudent
fonte
9
Eu acho que essa é uma ótima pergunta para uma discussão. Mas não é algo que possa ser respondido com uma resposta objetiva. Portanto, essa pergunta pode ser encerrada como baseada em opiniões.
Euphoric
54
Muitas pessoas argumentariam contra o primeiro esquema de nomenclatura porque é excessivamente detalhado , muito além do ponto em que isso ajudaria na clareza. Especialmente assertEquals(), esse método é usado centenas de vezes em uma base de código, portanto, pode-se esperar que os leitores se familiarizem com a convenção uma vez. Estruturas diferentes têm convenções diferentes (por exemplo, (actual, expected) or an agnostic (esquerda, direita) `), mas, na minha experiência, isso é no mínimo uma fonte menor de confusão.
amon
5
Como o ganho é tão pequeno, comparado aos seus benefícios, que qualquer pessoa sã provavelmente se afastaria. Se você deseja uma abordagem mais fluente , tente assert(a).toEqual(b)(mesmo que a IMO ainda seja desnecessariamente detalhada) onde você pode encadear algumas afirmações relacionadas.
Adriano Repetti
18
Como sabemos que valores reais e esperados são? Certamente deveria ser assertExpectedValueEqualsActualValue? Mas espere, como lembramos se ele usa ==ou .equalsou Object.equals? Deveria ser assertExpectedValueEqualsMethodReturnsTrueWithActualValueParameter?
precisa saber é o seguinte
6
Dado que, para esse método específico, a ordem dos dois argumentos não importa, parece um péssimo exemplo optar por adotar os benefícios desse esquema de nomenclatura.
Steven Rands

Respostas:

66

Porque é mais para digitar e mais para ler

A razão mais simples é que as pessoas gostam de digitar menos e codificar essas informações significa mais digitação. Ao ler, toda vez que tenho que ler a coisa toda, mesmo que eu esteja familiarizado com o que deve ser a ordem dos argumentos. Mesmo que não esteja familiarizado com a ordem dos argumentos ...

Muitos desenvolvedores usam IDEs

Os IDEs geralmente fornecem um mecanismo para visualizar a documentação de um determinado método passando o mouse ou através de um atalho de teclado. Por esse motivo, os nomes dos parâmetros estão sempre à mão.

A codificação dos argumentos introduz duplicação e acoplamento

Os nomes dos parâmetros já devem documentar o que são. Ao escrever os nomes no nome do método, também estamos duplicando essas informações na assinatura do método. Também criamos um acoplamento entre o nome do método e os parâmetros. Diga expectede actualé confuso para nossos usuários. Passar de assertEquals(expected, actual)para assertEquals(planned, real)não requer alteração do código do cliente usando a função Passar de assertExpectedEqualsActual(expected, actual)para assertPlannedEqualsReal(planned, real)significa uma mudança inédita na API. Ou não alteramos o nome do método, que rapidamente se torna confuso.

Use tipos em vez de argumentos ambíguos

A verdadeira questão é que temos argumentos ambíguos que são facilmente trocados porque são do mesmo tipo. Em vez disso, podemos usar nosso sistema de tipos e nosso compilador para impor a ordem correta:

class Expected<T> {
    private T value;
    Expected(T value) { this.value = value; }
    static Expected<T> is(T value) { return new Expected<T>(value); }
}

class Actual<T> {
    private T value;
    Actual(T value) { this.value = value; }
    static Actual<T> is(T value) { return new Actual<T>(value); }
}

static assertEquals(Expected<T> expected, Actual<T> actual) { /* ... */ }

// How it is used
assertEquals(Expected.is(10), Actual.is(x));

Isso pode ser aplicado no nível do compilador e garante que você não possa recuperá-los. Aproximando-se de um ângulo diferente, é essencialmente o que a biblioteca Hamcrest faz para testes.

cbojar
fonte
5
Bem, se você usa um IDE, tem os nomes dos parâmetros na ajuda do balão; se você não usar um, lembrar o nome da função é equivalente a lembrar dos argumentos; portanto, nada será ganho de qualquer maneira.
Peter - Restabelece Monica
29
Se você se opõe a assertExpectedEqualsActual"porque é mais para digitar e mais para ler", como você pode advogar assertEquals(Expected.is(10), Actual.is(x))?
Ruakh 01/07
9
@ruakh não é comparável. assertExpectedEqualsActualainda exige que o programador se preocupe em especificar os argumentos na ordem correta. A assertEquals(Expected<T> expected, Actual<T> actual)assinatura usa o compilador para impor o uso correto, que é uma abordagem totalmente diferente. Você pode otimizar essa abordagem para a brevidade, por exemplo expect(10).equalsActual(x), mas essa não era a questão ...
Holger
6
Além disso, neste caso em particular (==), a ordem dos argumentos é realmente irrelevante para o valor final. O pedido importa apenas para um efeito colateral (relatando a falha). Ao fazer o pedido, isso pode fazer (marginalmente) mais sentido. Por exemplo, strcpy (dest, src).
21720 Kristian H #
1
Não posso concordar mais, especialmente com a parte sobre duplicação e acoplamento ... Se toda vez que um parâmetro de função muda de nome, o nome da função também precisa ser alterado, você deve rastrear todos os usos dessa função e mudá-los bem ... Isso faria uma batelada de quebrar mudanças para mim, minha equipe, e todos os outros usando o nosso código como uma dependência ...
mrsmn
20

Você pergunta sobre um longo debate em programação. Quanta verbosidade é boa? Como resposta geral, os desenvolvedores descobriram que a verbosidade extra que nomeia os argumentos não vale a pena.

A verbosidade nem sempre significa mais clareza. Considerar

copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)

versus

copy(output, source)

Ambos contêm o mesmo bug, mas realmente tornamos mais fácil encontrá-lo? Como regra geral, a coisa mais fácil de depurar é quando tudo é maximamente conciso, exceto as poucas coisas que contêm o erro, e essas são detalhadas o suficiente para dizer o que deu errado.

Há uma longa história de adição de verbosidade. Por exemplo, há a " notação húngara " geralmente impopular que nos deu nomes maravilhosos como lpszName. Isso geralmente caiu no esquecimento na população geral de programadores. No entanto, adicionar caracteres aos nomes de variáveis ​​de membros (como mNameou m_Nameou name_) continua a ter popularidade em alguns círculos. Outros abandonaram isso completamente. Por acaso, trabalho em uma base de código de simulação física cujos documentos de estilo de codificação exigem que qualquer função que retorne um vetor especifique o quadro do vetor na chamada de função ( getPositionECEF).

Você pode estar interessado em alguns dos idiomas popularizados pela Apple. O Objetivo-C inclui os nomes dos argumentos como parte da assinatura da função (a função [atm withdrawFundsFrom: account usingPin: userProvidedPin]está escrita na documentação como withdrawFundsFrom:usingPin:. Esse é o nome da função). Swift tomou um conjunto semelhante de decisões, exigindo que você coloque os nomes dos argumentos nas chamadas de função ( greet(person: "Bob", day: "Tuesday")).

Cort Ammon
fonte
13
Todos os outros pontos à parte, seria muito mais fácil ler se copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)fossem escritos copy_from_source_stream_to_destination_stream_without_blocking(file_stream_from_choose_preferred_output_dialog, heuristically_decided_source_file_handle). Veja como foi mais fácil ?! Isso ocorre porque é muito fácil perder pequenas mudanças no meio dessa enorme conversa, e leva mais tempo para descobrir onde estão os limites da palavra. Esmagar confunde.
tchrist
1
A sintaxe obj-C withdrawFundsFrom: account usingPin: userProvidedPiné realmente emprestada do SmallTalk.
joH1
14
@tchrist tenha cuidado para ter certeza de que você está certo sobre tópicos que envolvem guerras sagradas. O outro lado nem sempre está errado.
Cort Ammon
3
@tchrist Addingunderscoresnakesthingseasiertoreadnotharderasyouseeestá manipulando o argumento. A resposta aqui usou letras maiúsculas, que você está omitindo. AddingCapitalizationMakesThingsEasyEnoughToReadAsYouCanSeeHere. Em segundo lugar, 9 vezes fora de 10, um nome nunca deve crescer além [verb][adjective][noun](onde cada bloco é opcional), um formato que é bem legível usando capitalização simples:ReadSimpleName
Flater
5
@tchrist - a ciência do seu estudo ( link de texto completo gratuito ) simplesmente mostra que programadores treinados para usar o estilo sublinhado são mais rápidos em ler o estilo sublinhado do que o caso camel. Os dados também mostram que a diferença é menor para indivíduos mais experientes (e a maioria dos estudantes sugere que mesmo aqueles que provavelmente não eram especialmente experientes). Isso não significa que os programadores que passaram mais tempo usando estojo de camelo também fornecerão o mesmo resultado.
Jules
8

O autor de "Código Limpo" aponta um problema legítimo, mas sua solução sugerida é bastante deselegante. Geralmente, existem maneiras melhores de melhorar nomes de métodos pouco claros.

Ele está certo que assertEquals(das bibliotecas de teste de unidade no estilo xUnit) não deixa claro qual argumento é o esperado e qual é o real. Isso também me mordeu! Muitas bibliotecas de teste de unidade notaram o problema e introduziram sintaxes alternativas, como:

actual.Should().Be(expected);

Ou similar. O que é certamente muito mais claro do que, assertEqualsmas também muito melhor do que assertExpectedEqualsActual. E também é muito mais compostável.

JacquesB
fonte
1
Eu sou anal e sigo a ordem recomendada, mas parece-me que, se espero que o resultado fun(x)seja 5, o que poderia dar errado se reverter a ordem - assert(fun(x), 5)? Como isso te mordeu?
Emory
3
@emory Eu sei que o jUnit (pelo menos) cria uma mensagem de erro através dos valores de expectede actual, portanto, revertê-los pode resultar em uma mensagem não precisa. Mas eu concordo que parece mais natural :) :)
joH1
@ joH1 parece fraco para mim. o código com falha falhará e a passagem do código passará, seja você assert(expected, observed)ou não assert(observed, expected). Um exemplo melhor seria algo como locateLatitudeLongitude- se você inverter as coordenadas, isso será muito complicado.
Emory
1
@emory Pessoas que não se importam com mensagens de erro sensíveis em testes de unidade são o motivo pelo qual eu tenho que lidar com "Falha no Assert.IsTrue" em algumas bases de código antigas. O que é extremamente divertido de depurar. Mas sim, neste caso, o problema pode não ser tão essencial (exceto se fizermos comparações imprecisas onde a ordem dos argumentos geralmente importa). As afirmações fluentes são realmente uma ótima maneira de evitar esse problema e também tornam o código muito mais expressivo (e fornecem uma mensagem de erro muito melhor para inicializar).
Voo
@emory: A reversão do argumento fará com que as mensagens de erro sejam enganosas e o levará ao caminho errado ao depurar.
precisa saber é o seguinte
5

Você está tentando orientar seu caminho entre Scylla e Charybdis para obter clareza, tentando evitar verbosidade inútil (também conhecida como divagação sem objetivo), além de brevidade excessiva (também conhecida como concisão enigmática).

Portanto, temos que olhar para a interface que você deseja avaliar, uma maneira de fazer afirmações de depuração de que dois objetos são iguais.

  1. Existe alguma outra função que possa estar considerando aridade e nome?
    Não, então o nome em si é claro o suficiente.
  2. Os tipos têm algum significado?
    Não, então vamos ignorá-los. Você já fez isso? Boa.
  3. É simétrico em seus argumentos?
    Quase, por erro, a mensagem coloca a representação de cada argumento em seu próprio local.

Então, vamos ver se essa pequena diferença tem algum significado e não é coberta pelas convenções fortes existentes.

O público-alvo é incomodado se os argumentos forem trocados involuntariamente?
Não, os desenvolvedores também recebem um rastreamento de pilha e precisam examinar o código-fonte para corrigir o erro.
Mesmo sem um rastreamento de pilha completo, a posição das asserções resolve essa pergunta. E se até isso está faltando e não é óbvio a partir da mensagem que é qual, no máximo dobra as possibilidades.

A ordem dos argumentos segue a convenção?
Parece ser o caso. Embora pareça, na melhor das hipóteses, uma convenção fraca.

Portanto, a diferença parece bastante insignificante e a ordem dos argumentos é coberta por uma convenção forte o suficiente para que qualquer esforço para codificá-la no nome da função tenha utilidade negativa.

Desduplicador
fonte
bem a ordem pode importar com jUnit, que constrói uma mensagem de erro específica a partir dos valores de expectede actual(pelo menos com cordas)
joH1
Eu acho que cobriu a parte ...
Deduplicator
você mencionou, mas considere: assertEquals("foo", "doo")dá a mensagem de erro é ComparisonFailure: expected:<[f]oo> but was:<[d]oo>... Trocar os valores inverteria o significado da mensagem, isso me parece mais anti- simétrico. De qualquer forma, como você disse, um desenvolvedor tem outros indicadores para resolver o erro, mas pode ser um erro no IMHO e levar um pouco mais de tempo de depuração.
joH1
A idéia de que existe uma "convenção" para ordens de argumento é engraçada, considerando que os dois campos (dest, src x src, dest) discutem sobre isso há pelo menos enquanto a sintaxe AT&T x Intel existir. E mensagens de erro inúteis em testes de unidade são uma praga que deve ser erradicada e não imposta. Isso é quase tão ruim quanto "Assert.IsTrue falhou" ("ei, você tem que executar o teste de unidade de qualquer maneira para depurá-lo, então execute-o novamente e coloque um ponto de interrupção lá", "ei, você tem que olhar para o código de qualquer maneira, então basta verificar se o pedido está correto ").
Voo
@Voo: O ponto é que o "dano" para cometer erros é minúsculo (a lógica não depende disso, e o utilitário de mensagens não é prejudicado em grau significativo) e, ao escrever o IDE, mostramos o nome dos parâmetros e digite assim mesmo.
Deduplicator
3

Muitas vezes, não adiciona nenhuma clareza lógica.

Compare "Adicionar" a "AddFirstArgumentToSecondArgument".

Se você precisar de uma sobrecarga que, digamos, adicione três valores. O que faria mais sentido?

Outro "Adicionar" com três argumentos?

ou

"AddFirstAndSecondAndThirdArgument"?

O nome do método deve transmitir seu significado lógico. Deve dizer o que faz. Contar, em nível micro, quais etapas são necessárias não facilitam para o leitor. Os nomes dos argumentos fornecerão detalhes adicionais, se necessário. Se você precisar de mais detalhes ainda, o código estará lá para você.

Martin Maat
fonte
4
Addsugere uma operação comutativa. O PO está preocupado com situações em que a ordem é importante.
Rosie F
No Swift, você poderia, por exemplo, chamar add (5, to: x) ou add (5, plus: 7, to: x) ou add (5, plus: 7, fornecendo: x) se você definir a função add () adequadamente.
gnasher729
A terceira sobrecarga deve ser nomeada "Sum"
StingyJack
@StringyJack Hmm .. Sum não é uma instrução, é um substantivo que o torna menos adequado para o nome de um método. Mas se você se sente assim e se deseja ser purista, a versão de dois argumentos também deve ser chamada de Sum. Se você tivesse um método Add, ele deveria ter um argumento adicionado à própria instância do objeto (que deveria ser do tipo numérico ou vetorial). As 2 ou mais variedades de argumento (seja qual for o nome delas) seriam estáticas. Então as três ou mais versões de argumento seriam redundantes e teríamos implementado um operador positivo: - |
Martin Maat
1
@ Martin Espere o que? sumé um verbo perfeitamente cromulento . É particularmente comum na frase "resumir".
Voo
2

Gostaria de adicionar outra coisa sugerida por outras respostas, mas acho que não foi mencionada explicitamente:

@puck diz: "Ainda não há garantia de que o primeiro argumento mencionado no nome da função seja realmente o primeiro parâmetro."

@cbojar diz "Use tipos em vez de argumentos ambíguos"

A questão é que as linguagens de programação não entendem nomes: elas são tratadas apenas como símbolos atômicos e opacos. Portanto, como nos comentários de código, não há necessariamente nenhuma correlação entre o nome de uma função e como ela realmente funciona.

Compare assertExpectedEqualsActual(foo, bar)com algumas alternativas (desta página e de outros lugares), como:

# Putting the arguments in a labelled structure
assertEquals({expected: foo, actual: bar})

# Using a keyword arguments language feature
assertEquals(expected=foo, actual=bar)

# Giving the arguments different types, forcing us to wrap them
assertEquals(Expected(foo), Actual(bar))

# Breaking the symmetry and attaching the code to one of the arguments
bar.Should().Be(foo)

Todos eles têm mais estrutura do que o nome detalhado, o que dá à linguagem algo não-opaco para se olhar. A definição e o uso da função também dependem dessa estrutura, portanto, ela não pode ficar fora de sincronia com o que a implementação está fazendo (como um nome ou comentário).

Quando encontro ou prevejo um problema como esse, antes de gritar com meu computador em frustração, primeiro tomo um momento para perguntar se é 'justo' culpar a máquina. Em outras palavras, a máquina recebeu informações suficientes para distinguir o que eu queria e o que pedi?

Uma ligação como assertEqual(expected, actual)faz tanto sentido quanto assertEqual(actual, expected), portanto, é fácil misturá-las e fazer com que a máquina avance e faça a coisa errada. Se usarmos em assertExpectedEqualsActualvez disso, isso pode nos tornar menos propensos a cometer um erro, mas não fornece mais informações à máquina (ela não entende inglês, e a escolha do nome não deve afetar a semântica).

O que torna as abordagens "estruturadas" mais preferíveis, como argumentos de palavras-chave, campos rotulados, tipos distintos etc. é que as informações extras também são legíveis por máquina , para que possamos fazer com que a máquina identifique usos incorretos e nos ajude a fazer as coisas corretamente. O assertEqualcaso não é muito ruim, pois o único problema seria mensagens imprecisas. Um exemplo mais sinistro pode ser String replace(String old, String new, String content), fácil de confundir, com String replace(String content, String old, String new)um significado muito diferente. Um remédio simples seria pegar um par [old, new], o que cometeria erros que desencadeiam um erro imediatamente (mesmo sem tipos).

Observe que, mesmo com os tipos, podemos não estar "dizendo à máquina o que queremos". Por exemplo, o antipadrão chamado "programação com tipos de string" trata todos os dados como strings, o que facilita a mistura de argumentos (como este caso), o esquecimento de executar alguma etapa (por exemplo, escape) e a quebra acidental de invariantes (por exemplo, tornando JSON incomparável), etc.

Isso também está relacionado à "cegueira booleana", onde calculamos um monte de booleanos (ou números etc.) em uma parte do código, mas ao tentar usá-los em outra não fica claro o que eles estão realmente representando, seja nós os misturamos, etc. Compare isso com, por exemplo, enumerações distintas que têm nomes descritivos (por exemplo, em LOGGING_DISABLEDvez de false) e que causam uma mensagem de erro se as misturarmos.

Warbo
fonte
1

porque elimina a necessidade de lembrar para onde vão os argumentos

Realmente? Ainda não há garantia de que o primeiro argumento mencionado no nome da função seja realmente o primeiro parâmetro. Portanto, procure melhor (ou deixe seu IDE fazer isso) e fique com nomes razoáveis ​​do que confiar cegamente em um nome bastante tolo.

Se você ler o código, deverá ver facilmente o que acontece quando os parâmetros são nomeados como deveriam. copy(source, destination)é muito mais fácil de entender do que algo assim copyFromTheFirstLocationToTheSecondLocation(placeA, placeB).

Por que os codificadores não adotam o primeiro, se é, como afirma o autor, mais claro que o último?

Como existem pontos de vista diferentes em estilos diferentes, é possível encontrar x autores de outros artigos que afirmam o contrário. Você ficaria louco tentando seguir tudo o que alguém escreve em algum lugar ;-)

disco
fonte
0

Concordo que a codificação de nomes de parâmetros em nomes de funções torna a escrita e o uso de funções mais intuitivas.

copyFromSourceToDestination( // "...ahh yes, the source directory goes first"

É fácil esquecer a ordem dos argumentos nas funções e comandos do shell e muitos programadores confiam nos recursos IDE ou nas referências de funções por esse motivo. Ter os argumentos descritos no nome seria uma solução eloquente para essa dependência.

No entanto, uma vez escrita, a descrição dos argumentos se torna redundante para o próximo programador que precisar ler a declaração, pois na maioria dos casos as variáveis ​​nomeadas serão usadas.

copy(sourceDir, destinationDir); // "...makes sense"

A dispersão disso conquistará a maioria dos programadores e eu pessoalmente acho mais fácil de ler.

EDIT: Como o @Blrfl apontou, os parâmetros de codificação não são tão "intuitivos", pois você precisa lembrar o nome da função em primeiro lugar. Isso requer procurar referências de função ou obter ajuda de um IDE que provavelmente fornecerá informações sobre pedidos de parâmetros de qualquer maneira.

Josh Taylor
fonte
9
Portanto, se eu posso bancar o advogado do diabo por um minuto: é intuitivo apenas quando você sabe o nome completo da função. Se você sabe que existe uma função de cópia e não se lembra se é copyFromSourceToDestinationou não copyToDestinationFromSource, suas escolhas estão sendo encontradas por tentativa e erro ou lendo o material de referência. IDEs que podem completar nomes parciais são apenas uma versão automatizada do último.
Blrfl
@ Blrfl O objetivo de chamá-lo copyFromSourceToDestinationé que, se você acha que é copyToDestinationFromSource, o compilador encontrará seu bug, mas se foi chamado copy, não será. Obter os parâmetros de uma rotina de cópia da maneira errada é fácil, pois strcpy, strcat etc. estabelecem um precedente. E o conciso é mais fácil de ler? As listas de mesclagem (listaA, listaB, listaC) criam listaA a partir da listaB e listaC, ou lêem listaA e listaB e gravam listaC?
Rosie F
4
@ RosieF Se eu não fosse positivo sobre o significado dos argumentos, estaria lendo a documentação antes de escrever o código. Além disso, mesmo com nomes de funções mais detalhados, ainda há espaço para interpretação sobre qual é realmente a ordem. Alguém dando uma olhada fria no código não poderá intuir que você estabeleceu a convenção de que o que está no nome da função reflete a ordem dos argumentos. Eles ainda precisam saber com antecedência ou ler os documentos.
Blrfl
OTOH, destinationDir.copy (sourceDir); // "... faz mais sentido"
Kristian H
1
@KristianH Que direção dir1.copy(dir2)funciona? Nenhuma idéia. Que tal dir1.copyTo(dir2)?
maaartinus