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 assertExpectedEqualsActual
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 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_DISABLED
vez de false
) e que causam uma mensagem de erro se as misturarmos.