As funções longas são aceitáveis ​​se tiverem estrutura interna?


23

Ao lidar com algoritmos complicados em linguagens com suporte para funções aninhadas (como Python e D), geralmente escrevo funções enormes (porque o algoritmo é complicado), mas atenuo isso usando funções aninhadas para estruturar o código complicado. As funções enormes (mais de 100 linhas) ainda são consideradas más, mesmo que sejam bem estruturadas internamente através do uso de funções aninhadas?

Editar: para aqueles que não estão familiarizados com Python ou D, as funções aninhadas nessas linguagens também permitem o acesso ao escopo da função externa. Em D esse acesso permite a mutação de variáveis ​​no escopo externo. No Python, ele permite apenas a leitura. Em D, você pode desabilitar explicitamente o acesso ao escopo externo em uma função aninhada declarando-o static.


5
Escrevo regularmente mais de 400 funções / métodos de linha. Tem que colocar esse 500 caso instrução switch em algum lugar :)
Callum Rogers

7
@ Callum Rogers: No Python, em vez de ter uma declaração de troca de 500 casos, você usaria um dicionário / mapeamento de pesquisa. Eu acredito que eles são superiores a ter uma declaração de caso de troca de mais ou menos 500. Você ainda precisa de 500 linhas de definições de dicionário (como alternativa, no Python, é possível usar a reflexão para enumerá-las dinamicamente), mas a definição de dicionário é dados, não código; e há muito menos dúvidas sobre ter uma definição de dados grande do que uma definição de função grande. Além disso, os dados são mais robustos que o código.
Lie Ryan

Eu me encolho toda vez que vejo funções com muito LOC, me faz pensar qual é o objetivo da modularidade. É mais fácil seguir a lógica e depurar com funções menores.
Aditya P

A antiga e familiar linguagem Pascal também permite acesso ao escopo externo, assim como a extensão da função aninhada no GNU C. (Essas linguagens permitem apenas somente funções descendentes, no entanto: ou seja, argumentos que são funções que carregam escopo, só podem ser passado e não retornado, o que exigiria suporte completo ao fechamento lexical).
Kaz

Um bom exemplo de funções que tendem a ser longas são os combinadores que você escreve ao executar a programação reativa. Eles atingem facilmente trinta linhas, mas dobrariam de tamanho se fossem separados porque você perde os fechamentos.
Craig Gidney

Respostas:


21

Lembre-se sempre da regra: uma função faz uma coisa e faz bem! Se você puder fazer isso, evite funções aninhadas.

Isso dificulta a legibilidade e o teste.


9
Eu sempre odiei essa regra porque uma função que faz "uma coisa" quando vista em um alto nível de abstração pode fazer "muitas coisas" quando vista em um nível mais baixo. Se aplicada incorretamente, a regra "uma coisa" pode levar a APIs de granularidade excessiva.
dsimcha

1
@dsimcha: Que regra não pode ser aplicada incorretamente e gerar problemas? Quando alguém está aplicando "regras" / banalidades além do ponto em que é útil, basta destacar outra: todas as generalizações são falsas.

2
@ Roger: Uma reclamação legítima, exceto que, na prática, a regra da IMHO é mal aplicada para justificar APIs excessivamente refinadas e com engenharia excessiva. Isso tem custos concretos, pois a API se torna mais difícil e detalhada para casos de uso comuns e simples.
dsimcha

2
"Sim, a regra foi aplicada incorretamente, mas a regra foi aplicada demais!"? : P

1
Minha função faz uma coisa e faz muito bem: ocupar 27 telas. :)
Kaz

12

Alguns argumentaram que funções curtas podem ser mais propensas a erros do que funções longas .

Card e Glass (1990) apontam que a complexidade do projeto envolve realmente dois aspectos: a complexidade dentro de cada componente e a complexidade dos relacionamentos entre os componentes.

Pessoalmente, descobri que o código linear bem comentado é mais fácil de seguir (especialmente quando você não o escreveu originalmente) do que quando é dividido em várias funções que nunca são usadas em outros lugares. Mas isso realmente depende da situação.

Eu acho que a principal vantagem é que, quando você divide um bloco de código, está trocando um tipo de complexidade por outro. Provavelmente existe um ponto ideal em algum lugar no meio.


Você leu "Código Limpo"? Ele discute isso longamente. amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/…
Martin Wickman

9

Idealmente, toda a função deve estar visível sem precisar rolar. Às vezes, isso não é possível. Mas se você puder dividi-lo em pedaços, facilitará a leitura do código.

Sei que, assim que pressiono Page Up / Down ou movo para uma seção diferente do código, só consigo lembrar 7 +/- 2 coisas da página anterior. E, infelizmente, alguns desses locais serão usados ​​ao ler o novo código.

Eu sempre gosto de pensar na minha memória de curto prazo como os registros de um computador (CISC, não RISC). Se você tiver a função inteira na mesma página, poderá ir ao cache para obter as informações necessárias de outra seção do programa. Se a função inteira não couber em uma página, isso seria o equivalente a sempre empurrar qualquer memória para o disco após cada operação.


3
Você só precisa imprimi-lo em uma impressora matricial - ver, a 10 metros de código de uma só vez :)

Ou obter um monitor com resolução maior
frogstarr78

E use-o na orientação vertical.
Calmarius

4

Por que usar funções aninhadas, em vez de funções externas normais?

Mesmo que as funções externas sejam usadas apenas em sua única função anteriormente grande, ela ainda facilita a leitura de toda a bagunça:

DoThing(int x){
    x += ;1
    int y = FirstThing(x);
    x = SecondThing(x, y);
    while(spoo > fleem){
        x = ThirdThing(x+y);
    }
}

FirstThing(int x){...}
SecondThing(int x, int y){...}
ThirdThing(int z){...}

2
Duas razões possíveis: bagunçar o espaço para nome e o que chamarei de "proximidade lexical". Esses podem ser motivos bons ou ruins, dependendo do seu idioma.
precisa saber é o seguinte

Porém, funções aninhadas podem ser menos eficientes porque precisam suportar funções descendentes e possivelmente fechamentos lexicais completos. Se o idioma estiver pouco otimizado, poderá ser feito um trabalho extra para criar um ambiente dinâmico para passar para as funções filho.
Kaz

3

Eu não tenho o livro na minha frente neste momento (para citar), mas, de acordo com o Code Complete, o "sweetspot" para o tamanho da função era de 25 a 50 linhas de código, de acordo com sua pesquisa.

Há momentos em que é aceitável ter funções longas:

  • Quando a complexidade ciclomática da função é baixa. Seus colegas desenvolvedores podem ficar um pouco frustrados se precisarem olhar para uma função que contém uma declaração if gigante e a declaração else para isso se não estiver na tela ao mesmo tempo.

Os horários em que não é bom ter funções longas:

  • Você tem uma função com condicionais profundamente aninhados. Faça um favor aos seus colegas leitores de código, melhore a legibilidade interrompendo a função. Uma função fornece uma indicação para o leitor que "Este é um bloco de código que faz uma coisa". Além disso, pergunte-se se o comprimento da função indica que ela está fazendo muito e precisa ser fatorada para outra classe.

O ponto principal é que a manutenção deve ser uma das maiores prioridades da sua lista. Se outro desenvolvedor não conseguir ver o seu código e obter uma "essência" do que o código está fazendo em menos de 5 segundos, o código não fornecerá "metadados" suficientes para dizer o que está fazendo. Outros desenvolvedores devem saber o que sua classe está fazendo apenas olhando para o navegador de objetos no IDE escolhido em vez de ler mais de 100 linhas de código.

Funções menores têm as seguintes vantagens:

  • Portabilidade: é muito mais fácil mudar a funcionalidade (dentro da classe, na refatoração para outra)
  • Depuração: Quando você olha para o rastreamento de pilha, é muito mais rápido identificar um erro se estiver visualizando uma função com 25 linhas de código em vez de 100.
  • Legibilidade - O nome da função informa o que está fazendo um bloco inteiro de código. Um desenvolvedor de sua equipe pode não querer ler esse bloco se não estiver trabalhando com ele. Além disso, na maioria dos IDE modernos, outro desenvolvedor pode entender melhor o que sua classe está fazendo lendo os nomes das funções em um navegador de objetos.
  • Navegação - a maioria dos IDE permite pesquisar o nome das funções. Além disso, a maioria dos IDE modernos tem a capacidade de visualizar a fonte de uma função em outra janela, o que permite que outros desenvolvedores vejam sua função longa em 2 telas (se os multi-monitores) em vez de fazê-los rolar.

A lista continua.....


2

A resposta é que depende, no entanto, você provavelmente deve transformar isso em classe.


A menos que você não está escrevendo em uma OOPL, caso em que talvez não :)
Frank Shearar

O dsimcha mencionou que eles estavam usando D e Python.
Frogstarr78

As aulas costumam ser uma muleta para pessoas que nunca ouviram falar de coisas como fechamento lexical.
Kaz

2

Não gosto da maioria das funções aninhadas. Os lambdas se enquadram nessa categoria, mas geralmente não me sinalizam, a menos que tenham mais de 30 a 40 caracteres.

A razão básica é que ela se torna uma função altamente densa localmente com recursão semântica interna, o que significa que é difícil para mim envolver meu cérebro, e é apenas mais fácil enviar algumas coisas para uma função auxiliar que não sobrecarrega o espaço de código .

Considero que uma função deve fazer sua coisa. Fazer outras coisas é o que outras funções fazem. Portanto, se você tem uma função de 200 linhas, fazendo tudo, e tudo flui, tudo bem.


Às vezes, você precisa passar tanto contexto para uma função externa (à qual ela poderia ter acesso conveniente como uma função local) que há mais complicações em como a função é invocada do que no que ela faz.
Kaz

1

Isso é aceitável? Essa é realmente uma pergunta que somente você pode responder. A função alcança o que precisa? É sustentável? É 'aceitável' para os outros membros da sua equipe? Se sim, então é isso que realmente importa.

Editar: não vi nada sobre as funções aninhadas. Pessoalmente, eu não os usaria. Eu usaria funções regulares.


1

Uma função longa "linha reta" pode ser uma maneira clara de especificar uma longa sequência de etapas que sempre ocorrem em uma sequência específica.

No entanto, como outros mencionaram, essa forma é propensa a ter extensões locais de complexidade nas quais o fluxo geral é menos evidente. A colocação dessa complexidade local em uma função aninhada (ou seja, definida em outro local na função longa, talvez na parte superior ou inferior) pode restaurar a clareza do fluxo da linha principal.

Uma segunda consideração importante é controlar o escopo das variáveis ​​que devem ser usadas apenas em um trecho local de uma função longa. A vigilância é necessária para evitar que uma variável introduzida em uma seção do código seja involuntariamente referenciada em outro lugar (por exemplo, após ciclos de edição), pois esse tipo de erro não será exibido como um erro de compilação ou tempo de execução.

Em alguns idiomas, esse problema é facilmente evitado: um trecho de código local pode ser agrupado em seu próprio bloco, como com "{...}", dentro do qual qualquer variável recém-introduzida é visível apenas para esse bloco. Algumas linguagens, como Python, não possuem esse recurso; nesse caso, funções locais podem ser úteis para impor regiões de escopo menores.


1

Não, as funções de várias páginas não são desejáveis ​​e não devem passar na revisão de código. Também costumava escrever funções longas, mas depois de ler a refatoração de Martin Fowler , parei. Funções longas são difíceis de escrever corretamente, difíceis de entender e difíceis de testar. Eu nunca vi uma função de até 50 linhas que não seriam mais facilmente compreendidas e testadas se fossem divididas em um conjunto de funções menores. Em uma função de várias páginas, há quase certamente classes inteiras que devem ser fatoradas. É difícil ser mais específico. Talvez você deva postar uma de suas longas funções no Code Review e alguém (talvez eu) possa mostrar como melhorá-lo.


0

Quando estou programando em python, gosto de voltar atrás depois de escrever uma função e me perguntar se ela adere ao "Zen of Python" (digite 'import this' no seu interpretador python):

Bonito é melhor que feio.
Explícito é melhor que implícito.
Simples é melhor que complexo.
Complexo é melhor que complicado.
Flat é melhor que aninhado.
Esparso é melhor que denso.
Legibilidade conta.
Casos especiais não são especiais o suficiente para violar as regras.
Embora praticidade supere a pureza.
Os erros nunca devem passar silenciosamente.
A menos que seja explicitamente silenciado.
Diante da ambiguidade, recuse a tentação de adivinhar.
Deve haver uma - e preferencialmente apenas uma - maneira óbvia de fazê-lo.
Embora esse caminho possa não ser óbvio a princípio, a menos que você seja holandês.
Agora é melhor do que nunca.
Embora nunca seja melhor do que certoagora.
Se a implementação é difícil de explicar, é uma má ideia.
Se a implementação é fácil de explicar, pode ser uma boa ideia.
Os espaços para nome são uma ótima idéia - vamos fazer mais!


1
Se explícito é melhor que implícito, devemos codificar em linguagem de máquina. Nem mesmo em montador; ele faz coisas implícitas desagradáveis, como preencher automaticamente os slots de atraso de ramificação com instruções, calcular compensações de ramificações relativas e resolver referências simbólicas entre globais. Homem, estes usuários de Python mudos e seu pequeno religião ...
Kaz

0

Coloque-os em um módulo separado.

Supondo que sua solução não seja mais inchada do que precisar, você não tem muitas opções. Você já dividiu as funções em diferentes sub-funções, então a questão é onde você deve colocá-lo:

  1. Coloque-os em um módulo com apenas uma função "pública".
  2. Coloque-os em uma classe com apenas uma função "pública" (estática).
  3. Aninhe-os em uma função (como você descreveu).

Agora, a segunda e a terceira alternativas estão, de certa forma, aninhadas, mas a segunda alternativa, por alguns programadores, não parece ruim. Se você não excluir a segunda alternativa, não vejo muitas razões para excluir a terceira.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.