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?
clean-code
EternalStudent
fonte
fonte
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.assert(a).toEqual(b)
(mesmo que a IMO ainda seja desnecessariamente detalhada) onde você pode encadear algumas afirmações relacionadas.assertExpectedValueEqualsActualValue
? Mas espere, como lembramos se ele usa==
ou.equals
ouObject.equals
? Deveria serassertExpectedValueEqualsMethodReturnsTrueWithActualValueParameter
?Respostas:
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
expected
eactual
é confuso para nossos usuários. Passar deassertEquals(expected, actual)
paraassertEquals(planned, real)
não requer alteração do código do cliente usando a função Passar deassertExpectedEqualsActual(expected, actual)
paraassertPlannedEqualsReal(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:
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.
fonte
assertExpectedEqualsActual
"porque é mais para digitar e mais para ler", como você pode advogarassertEquals(Expected.is(10), Actual.is(x))
?assertExpectedEqualsActual
ainda exige que o programador se preocupe em especificar os argumentos na ordem correta. AassertEquals(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 exemploexpect(10).equalsActual(x)
, mas essa não era a questão ...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 (comomName
oum_Name
ouname_
) 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 comowithdrawFundsFrom: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")
).fonte
copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)
fossem escritoscopy_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.withdrawFundsFrom: account usingPin: userProvidedPin
é realmente emprestada do SmallTalk.Addingunderscoresnakesthingseasiertoreadnotharderasyousee
está 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
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:Ou similar. O que é certamente muito mais claro do que,
assertEquals
mas também muito melhor do queassertExpectedEqualsActual
. E também é muito mais compostável.fonte
fun(x)
seja 5, o que poderia dar errado se reverter a ordem -assert(fun(x), 5)
? Como isso te mordeu?expected
eactual
, portanto, revertê-los pode resultar em uma mensagem não precisa. Mas eu concordo que parece mais natural :) :)assert(expected, observed)
ou nãoassert(observed, expected)
. Um exemplo melhor seria algo comolocateLatitudeLongitude
- se você inverter as coordenadas, isso será muito complicado.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.
Não, então o nome em si é claro o suficiente.
Não, então vamos ignorá-los. Você já fez isso? Boa.
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.
fonte
expected
eactual
(pelo menos com cordas)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.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ê.
fonte
Add
sugere uma operação comutativa. O PO está preocupado com situações em que a ordem é importante.sum
é um verbo perfeitamente cromulento . É particularmente comum na frase "resumir".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: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 quantoassertEqual(actual, expected)
, portanto, é fácil misturá-las e fazer com que a máquina avance e faça a coisa errada. Se usarmos emassertExpectedEqualsActual
vez 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
assertEqual
caso não é muito ruim, pois o único problema seria mensagens imprecisas. Um exemplo mais sinistro pode serString replace(String old, String new, String content)
, fácil de confundir, comString 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_DISABLED
vez defalse
) e que causam uma mensagem de erro se as misturarmos.fonte
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 assimcopyFromTheFirstLocationToTheSecondLocation(placeA, placeB)
.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 ;-)
fonte
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.
É 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.
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.
fonte
copyFromSourceToDestination
ou nãocopyToDestinationFromSource
, 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.copyFromSourceToDestination
é que, se você acha que écopyToDestinationFromSource
, o compilador encontrará seu bug, mas se foi chamadocopy
, 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?dir1.copy(dir2)
funciona? Nenhuma idéia. Que taldir1.copyTo(dir2)
?