Funções de linha única chamadas apenas uma vez

120

Considere uma função sem parâmetros ( editar: não necessariamente) que executa uma única linha de código e é chamada apenas uma vez no programa (embora não seja impossível que seja necessário novamente no futuro).

Pode realizar uma consulta, verificar alguns valores, fazer algo que envolva regex ... qualquer coisa obscura ou "hacky".

A lógica por trás disso seria evitar avaliações dificilmente legíveis:

if (getCondition()) {
    // do stuff
}

Onde getCondition()é a função de uma linha.

Minha pergunta é simples: essa é uma boa prática? Parece bom para mim, mas não sei a longo prazo ...

vemv
fonte
9
A menos que seu fragmento de código é a partir de uma linguagem OOP com receptor implícito (por exemplo, isso) isso certamente seria uma má prática, como getCondition () provavelmente se baseia em estado global ...
Ingo
2
Ele pode ter sugerido que getCondition () poderia ter argumentos?
user606723
24
@ Ingo - algumas coisas realmente têm estado global. Hora atual, nomes de host, números de porta etc. são todos "Globals" válidos. O erro de design é transformar um Global em algo que não é inerentemente global.
James Anderson
1
Por que você simplesmente não faz fila getCondition? Se é tão pequeno e pouco usado como você diz, então dar um nome a ela não está conseguindo nada.
davidk01 13/09/11
12
davidk01: Legibilidade do código.
Wjl 13/09/11

Respostas:

240

Depende dessa linha. Se a linha é legível e concisa por si só, a função pode não ser necessária. Exemplo simplista:

void printNewLine() {
  System.out.println();
}

OTOH, se a função der um bom nome a uma linha de código que contém, por exemplo, uma expressão complexa e de difícil leitura, é perfeitamente justificada (para mim). Exemplo artificial (dividido em várias linhas para facilitar a leitura aqui):

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isFemale() 
        && (taxPayer.getNumberOfChildren() > 2 
        || (taxPayer.getAge() > 50 && taxPayer.getEmployer().isNonProfit()));
}
Péter Török
fonte
99
+1. A palavra mágica aqui é "código de auto-documentação".
12137 Konamiman
8
Um bom exemplo do que o tio Bob chamaria de "condicionais encapsulantes".
Anthony Pegram
12
@ Aditya: Nada diz que taxPayeré global neste cenário. Talvez essa classe seja TaxReturne taxPayerseja um atributo.
Adam Robinson
2
Além disso, permite que você documente a função de uma maneira que possa ser captada por, por exemplo, javadoc e esteja publicamente visível.
2
@ Dallas, o Clean Code de Bob Martin mostra muitos exemplos de códigos semi-reais. Se você tem muitas funções em uma única classe, talvez sua classe seja muito grande?
Péter Török
67

Sim, isso pode ser usado para satisfazer as melhores práticas. Por exemplo, é melhor que uma função com nome claro faça algum trabalho, mesmo que tenha apenas uma linha, do que ter essa linha de código em uma função maior e precisar de um comentário em uma linha explicando o que faz. Além disso, as linhas de código vizinhas devem executar tarefas no mesmo nível de abstração. Um contraexemplo seria algo como

startIgnition();
petrolFlag |= 0x006A;
engageChoke();

Nesse caso, é definitivamente melhor mover a linha do meio para uma função com nome sensato.

Kilian Foth
fonte
15
Que 0x006A é adorável: DA bem conhecida constante de combustível carbonizado com os aditivos a, bec.
quer
2
+1 Sou a favor do código de auto-documentação :), a propósito, acho que concordo em manter um nível constante de abstração através de blocos, mas não consegui explicar o porquê. Você se importaria de expandir isso?
vemv 12/09
3
Se você está apenas dizendo que, petrolFlag |= 0x006A;sem qualquer tipo de tomada de decisão, seria melhor simplesmente dizer petrolFlag |= A_B_C; sem uma função adicional. Presumivelmente, engageChoke()só deve ser chamado se petrolFlagatender a um determinado critério e que deve dizer claramente 'Preciso de uma função aqui'. Apenas um pequeno detalhe, esta resposta é basicamente pontual :)
Tim Post
9
+1 para apontar que o código dentro de um método deve estar no mesmo nível de abstração. @vemv, é uma boa prática, pois facilita a compreensão do código, evitando a necessidade de sua mente pular para cima e para baixo entre os diferentes níveis de abstração ao ler o código. Amarrar as alternâncias de nível de abstração às chamadas / retornos de métodos (por exemplo, "saltos" estruturais) é uma boa maneira de tornar o código mais fluente e limpo.
Péter Török
21
Não, é um valor completamente inventado. Mas o fato de você se perguntar sobre isso enfatiza o ponto: se o código dissesse mixLeadedGasoline(), você não precisaria!
Kilian Foth
37

Eu acho que, em muitos casos, essa função é de bom estilo, mas você pode considerar uma variável booleana local como alternativa nos casos em que você não precisa usar essa condição em algum lugar em outros lugares, por exemplo:

bool someConditionSatisfied = [complex expression];

Isso dará uma dica para o leitor de código e evitará a introdução de uma nova função.

cybevnm
fonte
1
O bool é particularmente benéfico se o nome da função de condição for difícil ou possivelmente enganoso (por exemplo IsEngineReadyUnlessItIsOffOrBusyOrOutOfService).
dbkk 12/09
2
Eu experimentei esse conselho com uma péssima idéia: o problema e a condição desviam a atenção do negócio principal da função. Além disso, um estilo que prefere variáveis ​​locais dificulta a refatoração.
Wolf
1
@ Wolf OTOH, prefiro isso a uma chamada de função para reduzir a "profundidade de abstração" do código. Na IMO, saltar para uma função é uma opção de contexto maior do que algumas linhas de código explícitas e diretas, especialmente se ela retornar um valor booleano.
Kache
@Kache Eu acho que depende se você codifica orientação a objeto ou não, no caso OOP, o uso da função membro mantém o design muito mais flexível. Realmente depende do contexto ...
Lobo
23

Além da resposta de Peter , se essa condição precisar ser atualizada em algum momento no futuro, encapsulando a maneira como você sugere que você teria apenas um único ponto de edição.

Seguindo o exemplo de Pedro, se este

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isFemale() 
        && (taxPayer.getNumberOfChildren() > 2 
        || (taxPayer.getAge() > 50 && taxPayer.getEmployer().isNonProfit()));
}

torna-se isso

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isMutant() 
        && (taxPayer.getNumberOfThumbs() > 2 
        || (taxPayer.getAge() > 123 && taxPayer.getEmployer().isXMan()));
}

Você faz uma única edição e é atualizada universalmente. Manutenção sábio, este é um plus.

Em termos de desempenho, a maioria dos compiladores otimizadores removerá a chamada de função e alinhará o pequeno bloco de código de qualquer maneira. A otimização de algo assim pode reduzir o tamanho do bloco (excluindo as instruções necessárias para a chamada de função, retorno, etc.), portanto é normalmente seguro fazer isso mesmo em condições que poderiam impedir a inclusão.

Stephen
fonte
2
Já estou ciente dos benefícios de manter um único ponto de edição - é por isso que a pergunta diz "... ligou uma vez". No entanto, é ótimo saber sobre essas otimizações do compilador, sempre achei que eles seguiram esse tipo de instrução literalmente.
vemv 12/09/11
Dividir um método para testar uma condição simples tende a sugerir que uma condição pode ser alterada alterando o método. Essa implicação é útil quando é verdadeira, mas pode ser perigosa quando não é. Por exemplo, suponha que o código precise dividir as coisas em objetos leves, objetos verdes pesados ​​e objetos não verdes pesados. Os objetos possuem uma propriedade rápida "appearGreen", que é confiável para objetos pesados, mas pode retornar falsos positivos com objetos leves; eles também têm uma função "measureSpectralResponse" que é lenta, mas funcionará de maneira confiável para todos os objetos.
Supercat 02/09
O fato de seemsGreennão ser confiável com objetos leves será irrelevante se o código nunca se importar se objetos leves são verdes. Se a definição de "leve" for alterada, no entanto, de modo que alguns objetos não verdes que retornam true para seemsGreennão sejam relatados como "leve", essa alteração na definição de "leve" poderá quebrar o código que testa objetos sendo Verde". Em alguns casos, testar o verde e o peso juntos no código pode tornar a relação entre os testes mais clara do que se fossem métodos separados.
Supercat 02/09
5

Além da legibilidade (ou em complemento), isso permite escrever funções no nível adequado de abstração.

tylermac
fonte
1
Eu tenho medo que eu não entendo o que você quis dizer ...
vemv
1
@vemv: Eu acho que ele quer dizer com "nível adequado de abstração" que você não acaba com um código que possui diferentes níveis de abstração misturados (ou seja, o que Kilian Foth já disse). Você não deseja que seu código lide com detalhes aparentemente insignificantes (por exemplo, a formação de todas as ligações no combustível) quando o código circundante estiver considerando uma visão de mais alto nível da situação em questão (por exemplo, acionar um motor). O primeiro atrapalha o segundo e torna seu código menos fácil de ler e manter, pois você terá que considerar todos os níveis de abstração de uma vez a qualquer momento.
Egon
4

Depende. Às vezes, é melhor encapsular uma expressão em uma função / método, mesmo que seja apenas uma linha. Se é complicado ler ou você precisar dele em vários lugares, considero uma boa prática. A longo prazo, é mais fácil manter, pois você introduziu um único ponto de mudança e melhor legibilidade .

No entanto, às vezes é apenas algo que você não precisa. Quando a expressão é fácil de ler de qualquer maneira e / ou aparece em um só lugar, não a envolva.

Falcão
fonte
3

Eu acho que se você tiver apenas alguns deles, tudo bem, mas o problema surge quando há muitos deles no seu código. E quando o compilador é executado ou o interpitor (dependendo do idioma que você usa), está indo para essa função na memória. Então, digamos que você tenha três deles, acho que o computador não notará, mas se você começar a ter centenas dessas pequenas coisas, o sistema precisará registrar funções na memória que são chamadas apenas uma vez e depois não destruídas.

WojonsTech
fonte
De acordo com a resposta de Stephen isso não pode acontecer sempre (embora confiando cegamente em magia compiladores não pode ser bom de qualquer maneira)
vemv
1
Sim, isso deve ser esclarecido, dependendo de muitos fatos. Se for um idioma interpolado, o sistema cairá para as funções de linha única todas as vezes, a menos que você instale algo para o cache que ainda não funcione. Agora, quando se trata de compiladores, só importa no dia dos fracos e na colocação dos aviões se o compilador será seu amigo e limpar sua pequena bagunça ou se vai pensar que você realmente precisa. Lembro que, se você souber o número exato de vezes que um loop sempre será executado algumas vezes, é melhor copiá-lo e colá-lo várias vezes do que o loop.
WojonsTech
+1 para o alinhamento dos planetas :) mas sua última frase parece totalmente louca para mim, você realmente faz isso?
vemv 13/09/11
Realmente depende Na maioria das vezes, não, a menos que esteja recebendo o valor correto do pagamento para verificar se isso acelera o aumento e outras coisas assim. Mas em compiladores antigos, era melhor copiá-lo e colá-lo, em seguida, deixar o compilador descobrir isso em 10/09.
WojonsTech
3

Recentemente, fiz exatamente isso em um aplicativo que refatorei, para tornar explícito o significado real do código sem comentários:

protected void PaymentButton_Click(object sender, EventArgs e)
    Func<bool> HaveError = () => lblCreditCardError.Text == string.Empty && lblDisclaimer.Text == string.Empty;

    CheckInputs();

    if(HaveError())
        return;

    ...
}
joshperry
fonte
Apenas curioso, que idioma é esse?
vemv 12/09
5
@vemv: Parece C # para mim.
Scott Mitchell
Também prefiro identificadores extras a comentários. Mas isso realmente difere da introdução de uma variável local para manter o ifcurto? Essa abordagem local (lambda) torna a PaymentButton_Clickfunção (como um todo) mais difícil de ler. O lblCreditCardErrorexemplo parece ser um membro, assim também HaveErroré um predicado (privado) válido para o objeto. Eu tenderia a rebaixar isso, mas não sou programador em C #, então resisto.
Wolf
@ Wolf Ei cara, sim. Eu escrevi isso há um bom tempo atrás :) Eu definitivamente faço as coisas de maneira bem diferente agora. De fato, olhar o conteúdo do rótulo para ver se houve algum erro me faz estremecer ... Por que não retornei um bool de CheckInputs()???
joshperry
0

Mover essa linha para um método bem nomeado facilita a leitura do código. Muitos outros já mencionaram isso ("código de auto-documentação"). A outra vantagem de movê-lo para um método é que facilita o teste de unidade. Quando é isolado em seu próprio método e testado por unidade, você pode ter certeza de que se / quando um bug for encontrado, ele não estará nesse método.

Andrew
fonte
0

Já existem muitas respostas boas, mas há um caso especial que vale a pena mencionar .

Se a sua declaração de uma linha precisar de um comentário e você conseguir identificar claramente (o que significa: nome) seu objetivo, considere extrair uma função enquanto aprimora o comentário no documento da API. Dessa forma, você torna a chamada de função mais fácil e rápida de entender.

Curiosamente, o mesmo pode ser feito se atualmente não houver nada a fazer, mas um comentário lembrando as expansões necessárias (em um futuro muito próximo 1) ), então,

def sophisticatedHello():
    # todo set up
    say("hello")
    # todo tear down

poderia muito bem mudou para isso

def sophisticatedHello():
    setUp()
    say("hello")
    tearDown()

1) você deve ter certeza disso (consulte o princípio YAGNI )

Lobo
fonte
0

Se o idioma suportar, eu normalmente uso funções anônimas rotuladas para fazer isso.

someCondition = lambda p: True if [complicated expression involving p] else False
#I explicitly write the function with a ternary to make it clear this is a a predicate
if (someCondition(p)):
    #do stuff...

IMHO, esse é um bom compromisso, pois oferece o benefício de legibilidade por não ter a expressão complicada bagunçando a ifcondição, evitando bagunçar o espaço de nomes global / pacote com pequenos rótulos descartáveis. Tem o benefício adicional de que a função "definição" esteja exatamente onde está sendo usada, facilitando a modificação e a leitura da definição.

Não precisa ser apenas funções predicadas. Também gosto de envolver repetidamente a placa da caldeira em pequenas funções como esta (funciona particularmente bem na geração de listas python sem sobrecarregar a sintaxe do colchete). Por exemplo, o seguinte exemplo simplificado ao trabalhar com PIL em python

#goal - I have a list of PIL Image objects and I want them all as grayscale (uint8) numpy arrays
im_2_arr = lambda im: array(im.convert('L')) 
arr_list = [im_2_arr(image) for image in image_list]
crasic
fonte
Por que "lambda p: True se [expressão complicada envolvendo p] else False" em vez de "lambda p: [expressão complicada envolvendo p]"? 8-)
Hans-Peter Störr
@hstoerr está no comentário logo abaixo dessa linha. Eu gosto de explicitamente saber que nós, alguma condição, é um predicado. Embora seja estritamente desnecessário, escrevo muitos scripts científicos lidos por pessoas que não codificam tanto, pessoalmente, acho mais apropriado ter uma discrepância extra do que confundir meus colegas porque não sabem disso [] == Falseou de algum outro equivalência pitônica semelhante que nem sempre é intuitiva. É basicamente uma maneira de sinalizar que alguma Condição é, de fato, um predicado.
crasic
Apenas para limpar o meu erro óbvio [] != False, mas []é falso como quando convertido para um bool
crasic
@crasic: quando não espero que meus leitores saibam que [] avalia False, prefiro fazer len([complicated expression producing a list]) == 0do que usar o True if [blah] else Falseque ainda exige que o leitor saiba que [] avalia False.
Lie Ryan