É sempre uma boa prática escrever uma função para qualquer coisa que precise ser repetida duas vezes?

130

Eu mesmo, mal posso esperar para escrever uma função quando preciso fazer algo mais do que duas vezes. Mas quando se trata de coisas que aparecem apenas duas vezes, é um pouco mais complicado.

Para código que precisa de mais de duas linhas, vou escrever uma função. Mas ao enfrentar coisas como:

print "Hi, Tom"
print "Hi, Mary"

Eu hesito em escrever:

def greeting(name):
    print "Hi, " + name

greeting('Tom')
greeting('Mary')

O segundo parece demais, não parece?

Mas e se tivermos:

for name in vip_list:
    print name
for name in guest_list:
    print name

E aqui está a alternativa:

def print_name(name_list):
    for name in name_list:
        print name

print_name(vip_list)
print_name(guest_list)

As coisas ficam complicadas, não? É difícil decidir agora.

Qual sua opinião sobre isso?

    
por Zen 13.01.2015 / 05:15
fonte

15 respostas

199

Embora seja um fator ao decidir dividir uma função, o número de vezes que algo é repetido não deve ser o único fator. Geralmente, faz sentido criar uma função para algo que é executado apenas uma vez . Geralmente, você quer dividir uma função quando:

  • Simplifica cada camada de abstração individual.
  • Você tem nomes bons e significativos para as funções de divisão, portanto, normalmente não é necessário alternar entre as camadas de abstração para entender o que está acontecendo.

Seus exemplos não atendem a esse critério. Você está indo de uma linha de uma para outra, e os nomes realmente não lhe dão nada em termos de clareza. Dito isto, funções simples são raras fora de tutoriais e tarefas escolares. A maioria dos programadores tendem a errar demais para o outro lado.

    
por 13.01.2015 / 06:47
fonte
104

Apenas se a duplicação for intencional em vez de acidental .

Ou, colocando de outra forma:

Somente se você esperasse que evoluíssem no futuro.

Veja por que:

Às vezes, duas partes do código apenas acontecem tornam-se as mesmas, embora não tenham relação entre si. Nesse caso, você deve resistir ao desejo de combiná-los, porque da próxima vez que alguém executar manutenção em um deles, essa pessoa não esperará as alterações para propagar para um chamador anteriormente inexistente, e essa função pode quebrar como resultado. Portanto, você só precisa calcular o código quando fizer sentido, não sempre que parecer reduzir o tamanho do código.

Regra geral:

Se o código só retorna novos dados e não modifica os dados existentes ou tem outros efeitos colaterais, é muito provável que seja seguro fatorar como uma função separada. (Não consigo imaginar nenhum cenário em que isso causasse uma quebra sem alterar completamente a semântica pretendida da função, ponto no qual o nome ou a assinatura da função também deve mudar e, nesse caso, seria necessário ter cuidado. )

    
por 14.01.2015 / 09:38
fonte
60

Não, nem sempre é uma prática recomendada.

Todas as outras coisas sendo iguais, linear, o código linha por linha é mais fácil de ler do que alternar entre as chamadas de função. Uma chamada de função não-trivial sempre pega parâmetros, então você tem que ordenar tudo isso e fazer saltos contextuais mentais de chamada de função para chamada de função. Sempre a favor de uma clareza de código melhor, a menos que você tenha uma boa razão para ser obscuro (como obter melhorias necessárias no desempenho).

Então, por que o código é refatorado em métodos separados? Para melhorar a modularidade. Para coletar funcionalidade significativa por trás de um método individual e dar a ele um nome significativo. Se você não está realizando isso, não precisa desses métodos separados.

    
por 13.01.2015 / 05:41
fonte
25

No seu exemplo particular, fazer uma função pode parecer um exagero; em vez disso, faço a pergunta: é possível que essa saudação específica mude no futuro? Como assim?

As funções não são usadas simplesmente para encerrar a funcionalidade e facilitar a reutilização, mas também para facilitar a modificação. Se os requisitos mudarem, o código copiado e colado precisará ser rastreado e modificado manualmente, enquanto que com uma função o código precisa ser modificado apenas uma vez.

Seu exemplo se beneficiaria disso não por meio de uma função - já que a lógica é quase inexistente - mas possivelmente uma GREET_OPENING constante. Essa constante poderia então ser carregada do arquivo para que seu programa fosse facilmente adaptável a diferentes idiomas, por exemplo. Note que esta é uma solução grosseira, um programa mais complexo provavelmente precisaria de uma maneira mais refinada de rastrear o i18n, mas novamente depende dos requisitos em relação ao trabalho a ser feito.

É tudo sobre possíveis requisitos no final, e planejar as coisas com antecedência para facilitar o trabalho do seu futuro eu.

    
por 13.01.2015 / 11:31
fonte
22

Pessoalmente, adotei a regra de três - o que justificarei chamando YAGNI . Se eu precisar fazer algo assim que eu escrever esse código, duas vezes eu posso apenas copiar / colar (sim, eu acabei de admitir copiar / colar!) Porque eu não precisarei dele novamente, mas se eu precisar fazer o mesmo coisa de novo, então eu vou refatorar e extrair esse pedaço em seu próprio método, eu demonstrei, para mim mesmo, que eu irá precisar dele novamente.

Concordo com o que Karl Bielefeldt e Robert Harvey disseram e minha interpretação do que eles estão dizendo é que a regra primordial é a legibilidade. Se isso torna meu código mais fácil de ler, considere a criação de uma função. Lembre-se de coisas como DRY e SLAP . Se eu posso evitar mudar o nível de abstração na minha função, então acho mais fácil gerenciar na minha cabeça, então não saltar entre as funções (se eu não conseguir entender o que a função faz simplesmente lendo o nome) significa menos interrupções na minha função. processos mentais.
Da mesma forma, não ter que alternar o contexto entre funções e código embutido, como print "Hi, Tom" funciona para mim, neste caso, eu poderia extrair uma função PrintNames() iff o resto do meu função geral eram principalmente chamadas de função.

    
por 13.01.2015 / 11:24
fonte
13

Ele raramente é claro, então você precisa pesar as opções:

  • Prazo (corrija a sala do servidor de gravação o mais rápido possível)
  • Legibilidade do código (pode influenciar a escolha de qualquer forma)
  • Nível de abstração da lógica compartilhada (relacionado acima)
  • Requisito de reutilização (isto é, ter exatamente a mesma lógica importante ou conveniente agora)
  • Dificuldade de compartilhar (aqui é onde o python brilha, veja abaixo)

Em C ++, eu costumo seguir a regra de três (ou seja, na terceira vez que eu preciso da mesma coisa, refatorei-a em uma peça propriamente compartilhável), mas com experiência é mais fácil fazer essa escolha inicialmente conforme você sabe mais sobre o escopo, software & domínio com o qual você está trabalhando.

No entanto, em Python, é relativamente leve reutilizar, em vez de repetir, a lógica. Mais do que em muitas outras línguas (pelo menos historicamente), IMO.

Portanto, considere reutilizar a lógica localmente, por exemplo, criando uma lista de argumentos locais:

def foo():
    for name_list in (vip_list, guest_list): # can be list of tuples, for many args
        for name in name_list:
            print name

Você pode usar uma tupla de tuplas e dividi-la diretamente no loop, se precisar de vários argumentos:

def foo2():
    for header, name_list in (('vips': vip_list), ('people': guest_list)): 
        print header + ": "
        for name in name_list:
            print name

ou fazer uma função local (talvez o seu segundo exemplo seja isso), o que torna a lógica reutilizar explícita, mas também mostra claramente que print_name () não é usado fora da função:

def foo():
    def print_name(name_list):
        for name in name_list:
            print name

    print_name(vip_list)
    print_name(guest_list)

As funções são preferíveis, especialmente quando você precisa anular a lógica no meio (ou seja, usar retorno), pois interrupções ou exceções podem sobrecarregar as coisas desnecessariamente.

Qualquer um é superior à repetição da mesma lógica, IMO, e também menos confuso do que declarar uma função global / de classe que é usada apenas por um chamador (embora duas vezes).

    
por 13.01.2015 / 07:16
fonte
9

Todas as práticas recomendadas têm um motivo específico, ao qual você pode se referir para responder a essa pergunta. Você deve sempre evitar ondulações, quando uma mudança futura potencial implica mudanças em outros componentes também.

Então, no seu exemplo: Se você assumir que tem uma saudação padrão e quiser garantir que ela seja a mesma para todas as pessoas, escreva

def std_greeting(name):
    print "Hi, " + name

for name in ["Tom", "Mary"]:
    std_greeting(name)   # even the function call should be written only once

Caso contrário, você teria que prestar atenção e alterar a saudação padrão em dois lugares, caso isso mude.

Se, no entanto, o "Hi" for o mesmo apenas acidentalmente e a alteração de uma das saudações não resultar necessariamente em alterações para a outra, mantenha-as separadas. Portanto, mantenha-os separados, se houver razões lógicas para acreditar, que a seguinte alteração é mais provável:

print "Hi, Tom"
print "Hello, Mary"

Para sua segunda parte, decidir quanto "empacotar" em funções provavelmente depende de dois fatores:

  • mantenha os blocos pequenos para facilitar a leitura
  • mantenha blocos grandes o suficiente para que as alterações ocorram em apenas alguns blocos. Não é necessário agrupar muito, pois é difícil rastrear o padrão de chamadas.

Idealmente, quando ocorrer uma mudança, você pensará "Eu tenho que mudar a maioria do bloco seguinte" e não "Eu tenho que alterar o código em algum lugar dentro de um bloco grande".

    
por 13.01.2015 / 14:53
fonte
5

O aspecto mais importante de constantes e funções nomeadas não é tanto que elas reduzem a quantidade de digitação, mas sim que elas "anexam" os diferentes lugares onde elas são usadas. Em relação ao seu exemplo, considere quatro cenários:

  1. É necessário alterar as duas saudações da mesma maneira, por exemplo, para Bonjour, Tom e Bonjour, Mary ].

  2. É necessário alterar uma saudação, mas deixar a outra como é [por exemplo, Hi, Tom e Guten Tag, Mary ].

  3. É necessário alterar as duas saudações de maneira diferente [por exemplo, Hello, Tom e Howdy, Mary ].

  4. Nenhuma saudação precisa ser alterada.

Se nunca é necessário alterar uma das saudações, não importa qual abordagem é adotada. Usar uma função compartilhada terá o efeito natural de que a alteração de qualquer saudação as alterará da mesma maneira. Se todas as saudações devem mudar da mesma maneira, isso seria uma coisa boa. Se não devem, no entanto, a saudação de cada pessoa terá que ser codificada ou especificada separadamente; qualquer trabalho feito, fazendo-os usar uma função ou especificação comum, terá que ser desfeito (e, em primeiro lugar, teria sido melhor não ter sido feito).

Para ter certeza, nem sempre é possível prever o futuro, mas se alguém tiver motivos para acreditar que é mais provável que ambas as saudações precisem mudar juntas, isso seria um fator a favor do uso de código comum (ou um constante nomeada); se alguém tiver motivos para acreditar que é mais provável que um ou ambos precisem mudar para que sejam diferentes, isso seria um fator contra a tentativa de usar código comum.

    
por 13.01.2015 / 18:58
fonte
4

Nenhum dos casos que você dá parece valer a pena refatorar.

Em ambos os casos, você está executando uma operação que é expressa de forma clara e sucinta, e não há perigo de uma mudança inconsistente sendo feita.

Eu recomendo que você sempre procure maneiras de isolar o código onde:

  1. Existe uma tarefa identificável e significativa que você pode separar, e

  2. Ou

    a. a tarefa é complexa de expressar (levando em consideração as funções disponíveis para você) ou

    b. a tarefa é executada mais de uma vez e você precisa ter uma maneira de garantir que ela seja alterada da mesma maneira em ambos os lugares,

Se a condição 1 não for satisfeita, você não encontrará a interface certa para o código: se tentar forçá-la, você terá muitos parâmetros, várias coisas que deseja retornar, muita lógica contextual e possivelmente não conseguir encontrar um bom nome. Tente documentar a interface que você está pensando em primeiro lugar, é uma boa maneira de testar a hipótese de que é o conjunto certo de funcionalidade e vem com a vantagem adicional de que quando você escreve seu código, ele já está especificado e documentado!

2a é possível sem o 2b: às vezes eu retirei o fluxo mesmo quando sei que ele só é usado uma vez, simplesmente porque movê-lo para outro lugar e substituí-lo por uma única linha significa que o contexto no qual ele é chamado é subitamente muito mais legível (especialmente se for um conceito fácil que a linguagem dificulta a implementação). Também fica claro quando se lê a função extraída, onde a lógica começa e termina e o que faz.

2b é possível sem 2a: O truque é ter uma ideia de quais mudanças são mais prováveis. É mais provável que a política da sua empresa mude sempre de "Olá" para "Olá" o tempo todo ou que sua saudação mude para um tom mais formal se você estiver enviando uma solicitação de pagamento ou um pedido de desculpas por uma interrupção do serviço? Às vezes, pode ser porque você está usando uma biblioteca externa da qual não tem certeza e quer poder trocá-la por outra rapidamente: a implementação pode mudar, mesmo que a funcionalidade não funcione.

Na maioria das vezes, você terá uma mistura de 2a e 2b. E você terá que usar seu julgamento, levando em conta o valor comercial do código, a frequência com que é usado, se está bem documentado e entendido, o estado da sua suíte de testes, etc.

Se você notar o mesmo bit de lógica usado mais de uma vez, por todos os meios você deve aproveitar a oportunidade para considerar a refatoração. Se você está iniciando novas instruções com mais de n níveis de indentação na maioria dos idiomas, esse é outro gatilho um pouco menos significativo (escolha um valor de n para seu idioma: Python pode ser 6, por exemplo).

Contanto que você esteja pensando nessas coisas e derrubando os grandes monstros espaguete, você deve estar bem - não fique tanto tempo preocupado com a refinação precisa de sua refração. Não tenho tempo para coisas como testes ou documentação trabalhada. Se precisar, o tempo dirá.

    
por 13.01.2015 / 23:08
fonte
4

Acho que você deveria ter um motivo para criar um procedimento. Há várias razões para criar um procedimento. Os que eu acho mais importantes são:

  1. abstração
  2. modularidade
  3. navegação por código
  4. atualização de consistência
  5. recursão
  6. testando

Abstração

Um procedimento pode ser usado para abstrair um algoritmo geral dos detalhes de etapas particulares no algoritmo. Se eu puder encontrar uma abstração adequadamente coerente, isso me ajudará a estruturar a maneira como penso sobre o que o algoritmo faz. Por exemplo, posso projetar um algoritmo que funciona em listas sem necessariamente considerar a representação da lista.

Modularidade

Os procedimentos podem ser usados para organizar o código em módulos. Os módulos podem frequentemente ser tratados separadamente. Por exemplo, construído e testado separadamente.

O módulo bem projetado geralmente captura alguma unidade coerente e significativa de funcionalidade. Por isso, muitas vezes podem pertencer a equipes diferentes, substituídas completamente por uma alternativa ou reutilizadas em um contexto diferente.

Navegação por código

Em sistemas grandes, encontrar o código associado a um determinado comportamento do sistema pode ser desafiador. A organização hierárquica do código dentro de bibliotecas, módulos e procedimentos pode ajudar nesse desafio. Particularmente, se você tentar a) organizar a funcionalidade sob nomes significativos e previsíveis, b) colocar os procedimentos relativos a uma funcionalidade semelhante, próximos uns dos outros.

Atualizar consistência

Em sistemas grandes, pode ser um desafio encontrar todo o código que precisa ser alterado para obter uma mudança específica no comportamento. Usar procedimentos para organizar a funcionalidade do programa pode facilitar isso. Em particular, se cada bit de funcionalidade do seu programa aparecer apenas uma vez e em um grupo coeso de procedimentos, é menos provável (em minha experiência) que você sentirá falta de algum lugar que você deveria ter atualizado ou fazer uma atualização inconsistente.

Observe que você deve organizar procedimentos com base na funcionalidade e nas abstrações do programa. Não é baseado em se dois bits de código são iguais no momento.

Recursão

O uso da recursão requer que você crie procedimentos

Teste

Normalmente, você pode testar os procedimentos independentemente um do outro. Testar diferentes partes do corpo de um procedimento de forma independente é mais difícil, porque você normalmente tem que executar a primeira parte do procedimento antes da segunda parte. Esta observação também se aplica frequentemente a outras abordagens para especificar / verificar o comportamento dos programas.

Conclusão

Muitos desses pontos estão relacionados à compreensão do programa. Você poderia dizer que os procedimentos são uma maneira de criar um novo idioma, específico para o seu domínio, com o qual organizar, escrever e ler, sobre os problemas e processos em seu domínio.

    
por 15.01.2015 / 16:10
fonte
3

Em geral, concordo com Robert Harvey, mas queria adicionar um caso para dividir uma funcionalidade em funções. Para melhorar a legibilidade. Considere um caso:

def doIt(smth,smthElse)
    for x in getDataFromSomething(smth,smthElse):
        if not check(x,smth):
            continue
        process(x,smthElse)
        store(x) 

Mesmo que essas chamadas de função não sejam usadas em nenhum outro lugar, há um ganho significativo na legibilidade se todas as 3 funções forem consideravelmente longas e possuírem loops aninhados etc.

    
por 13.01.2015 / 09:12
fonte
2

Não há regra rígida e rápida que deva ser aplicada, dependerá da função real que seria usada. Para impressão de nome simples eu não usaria uma função, mas se é uma soma de matemática que seria uma história diferente. Você criaria uma função mesmo se ela fosse chamada duas vezes, para garantir que a soma da matemática seja sempre a mesma, se alguma vez for alterada. Em outro exemplo, se você estiver fazendo alguma forma de validação, você usaria uma função, portanto, no seu exemplo, se você precisasse verificar o nome com mais de 5 caracteres, usaria uma função para garantir que as mesmas validações sejam sempre feitas.

Então, eu acho que você respondeu sua própria pergunta quando disse "Para códigos que precisam de mais de duas linhas, eu tenho que escrever um func". Em geral, você estaria usando uma função, mas também deve usar sua própria lógica para determinar se há algum tipo de valor adicionado usando uma função.

    
por 13.01.2015 / 05:52
fonte
2

Eu passei a acreditar em algo sobre a refatoração que eu não vi mencionado aqui, eu sei que há muitas respostas aqui, mas eu acho que isso é novo.

Eu tenho sido um refactorer implacável e um strong crente em DRY desde antes dos termos surgiram. Principalmente porque eu tenho dificuldade em manter uma grande base de código na minha cabeça e em parte porque gosto de codificação DRY e não gosto de nada sobre codificação C & P, na verdade é doloroso e terrivelmente lento para mim.

A questão é que insistir em DRY me deu muita prática em algumas técnicas que raramente vejo outras pessoas usarem. Muitas pessoas insistem que o Java é difícil ou impossível de fazer o DRY, mas na verdade eles simplesmente não tentam.

Um exemplo de muito tempo atrás é semelhante ao seu exemplo. As pessoas tendem a pensar que a criação de GUIs Java é difícil. Claro que é se você codifica assim:

Menu m=new Menu("File");
MenuItem save=new MenuItem("Save")
save.addAction(saveAction); // I forget the syntax, but you get the idea
m.add(save);
MenuItem load=new MenuItem("Load")
load.addAction(loadAction)

Qualquer um que pense que isso é uma loucura está absolutamente certo, mas não é culpa do Java - o código nunca deve ser escrito dessa forma. Essas chamadas de método são funções destinadas a serem agrupadas em outros sistemas. Se você não consegue encontrar tal sistema, construa-o!

Você obviamente não pode codificar assim, então você precisa dar um passo atrás e ver o problema, o que esse código repetido realmente está fazendo? Ele está especificando algumas strings e seu relacionamento (uma árvore) e juntando as folhas dessa árvore às ações. Então, o que você realmente quer é dizer:

class Menu {
    @MenuItem("File|Load")
    public void fileLoad(){...}
    @MenuItem("File|Save")
    public void fileSave(){...}
    @MenuItem("Edit|Copy")
    public void editCopy(){...}...

Uma vez que você tenha definido seu relacionamento de forma sucinta e descritiva, então você escreve um método para lidar com ele - nesse caso, você faz uma iteração sobre os métodos da classe passada e constrói uma árvore e usa isso para construa seus menus e ações, bem como (obviamente) exibir um menu. Você não terá nenhuma duplicação, e seu método é reutilizável ... e provavelmente mais fácil de escrever do que um grande número de menus teria sido, e se você realmente gosta de programar, você se divertiu muito mais. Isso não é difícil - o método que você precisa escrever é, provavelmente, menos linhas do que criar seu menu à mão!

A coisa é que, para fazer isso bem, você precisa praticar muito. Você precisa ser bom em analisar exatamente quais informações exclusivas estão nas partes repetidas, extrair essas informações e descobrir como expressá-las bem. Aprender a usar ferramentas como a análise de strings e anotações ajuda muito. Aprender a ser muito claro sobre relatórios de erros e documentação também é muito importante.

Você adquire prática "Free" simplesmente codificando bem - as chances são de que, uma vez que você fique bom nisso, você verá que codificar algo DRY (incluindo escrever uma ferramenta reutilizável) é mais rápido do que copiar e colar e todos os erros. , erros duplicados e mudanças difíceis que o tipo de codificação causa.

Eu não acho que seria capaz de aproveitar meu trabalho se não praticasse técnicas de DRY e construísse ferramentas o máximo que pudesse. Se eu tivesse que fazer um corte no pagamento para não ter que copiar e colar programação, eu aceitaria isso.

Então, meus pontos são:

  • Copiar & Colar custa mais tempo, a menos que você não saiba como refatorar bem.
  • Você aprende a refatorar bem fazendo isso - insistindo no DRY mesmo nos casos mais difíceis e triviais.
  • Você é um programador, se precisar de uma pequena ferramenta para criar seu código DRY, crie-o.
por 15.01.2015 / 05:41
fonte
1

Geralmente, é uma boa ideia dividir algo em uma função que melhora a legibilidade do seu código, ou, se a parte que está sendo repetida é de alguma forma essencial para o sistema. No exemplo acima, se você precisasse cumprimentar seus usuários dependendo da localidade, então faria sentido ter uma função de saudação separada.

    
por 04.03.2015 / 14:26
fonte
1

Como corolário do comentário de @DocBrown ao @RobertHarvey sobre o uso de funções para criar abstrações: se você não conseguir criar um nome de função adequadamente informativo que não seja significativamente mais conciso ou mais claro que o código por trás dele, você está não abstraindo muito. Ausente de qualquer outra boa razão para fazer disso uma função, não há necessidade de fazê-lo.

Além disso, um nome de função raramente captura sua semântica completa, então você pode ter que verificar sua definição, se não for comumente usado o suficiente para ser familiar. Particularmente em outra linguagem que não seja funcional, você pode precisar saber se tem efeitos colaterais, quais erros podem ocorrer e como eles são respondidos, sua complexidade de tempo, se aloca e / ou libera recursos, e se é threadado. seguro.

É claro que, por definição, consideramos apenas funções simples aqui, mas isso ocorre nos dois sentidos - nesses casos, não é provável que o inline acrescente complexidade. Além disso, o leitor provavelmente não perceberá que é uma função simples até que ele apareça. Mesmo com um IDE que o hiperlink para a definição, o visual pulando ao redor é um impedimento para o entendimento.

Em geral, recursivamente factoring código em funções mais pequenas, acabará por levá-lo ao ponto em que as funções não têm mais significado independente, porque como os fragmentos de código são feitos de ficar menor, esses fragmentos obter mais do seu significado o contexto criado pelo código circundante. Como uma analogia, pense nisso como uma imagem: se você aproximar demais, não consegue entender o que está vendo.

    
por 14.01.2015 / 17:53
fonte