Quando é apropriado fazer uma função separada quando somente haverá uma única chamada para a função mencionada? [duplicado]

43

Estamos projetando padrões de codificação e estamos tendo discordâncias quanto à possibilidade de quebrar o código em funções separadas dentro de uma classe, quando essas funções só serão chamadas uma vez.

Por exemplo:

f1()
{
   f2();  
   f4();
}


f2()
{
    f3()
    // Logic here
}


f3()
{
   // Logic here
}

f4()
{
   // Logic here
}

versus:

f1()
{
   // Logic here
   // Logic here
   // Logic here
}

Alguns argumentam que é mais simples de ler quando você divide uma função grande usando subfunções de uso único separadas. No entanto, ao ler código pela primeira vez, acho tedioso seguir as cadeias lógicas e otimizar o sistema como um todo. Há alguma regra normalmente aplicada a esse tipo de layout de função?

Por favor, note que ao contrário de outras perguntas, eu estou pedindo o melhor conjunto de condições para diferenciar usos permitidos e não permitidos de funções de chamada única, não apenas se eles são permitidos.

    
por David 23.01.2016 / 01:05
fonte

8 respostas

88

A lógica por trás da divisão de funções não é quantas vezes serão chamadas , mantendo-as pequenas e impedindo-as de fazer várias coisas diferentes.

livro de Bob Martin Código Limpo fornece boas orientações sobre quando dividir uma função:

  • Functions should be small; how small? See the bullet bellow.
  • Functions should do only one thing.

Portanto, se a função tiver várias telas, divida-a. Se a função fizer várias coisas, divida-a.

Se a função é feita de etapas seqüenciais destinadas a um resultado final, não é necessário dividi-la, mesmo que seja relativamente longa. Mas se as funções fazem uma coisa, então outra, depois outra e depois outra, com condições, blocos logicamente separados, etc., ela deve ser dividida. Como resultado dessa lógica, as funções devem ser geralmente pequenas.

Se f1() fizer autenticação, f2() analisará a entrada em partes menores, se f3() fizer cálculos e f4() logs ou persistir, eles obviamente deverão ser separados, mesmo quando cada um deles for chamado apenas uma vez.

Dessa forma, você pode refatorar e testá-los separadamente, além da vantagem de ser mais fácil de ler .

Por outro lado, se toda a função for:

a=a+1;
a=a/2;
a=a^2
b=0.0001;
c=a*b/c;
return c;

então não há necessidade de dividi-lo, mesmo quando a sequência de etapas é longa.

    
por 23.01.2016 / 01:19
fonte
43

Acho que a nomeação de funções é muito importante aqui.

Uma função strongmente dissecada pode ser muito auto-documentada. Se cada processo lógico dentro de uma função é dividido em sua própria função, com lógica interna mínima, então o comportamento de cada instrução pode ser racionalizado pelos nomes das funções e pelos parâmetros que eles tomam.

Claro, há um lado negativo: os nomes das funções. Como os comentários, essas funções altamente específicas podem frequentemente ficar fora de sincronia com o que a função está realmente fazendo. Mas , ao mesmo tempo, dando-lhe um nome de função adequado, você o torna mais difícil para justificar o uso do escopo. Torna-se mais difícil fazer uma sub-função fazer algo mais do que deveria claramente.

Então, eu sugeriria isso. Se você acredita que uma seção do código pode ser dividida, mesmo que ninguém mais a chame, pergunte a si mesmo: "Que nome você daria a ela?"

Se responder a essa pergunta leva mais de 5 segundos, ou se o nome da função escolhido for decididamente opaco, há uma boa chance de que não seja uma unidade lógica separada na função. Ou, no mínimo, você não tem certeza o suficiente sobre o que aquela unidade lógica está realmente fazendo para dividi-la corretamente.

Mas há um problema adicional que funções muito dissecadas podem encontrar: correção de bugs.

O rastreamento de erros lógicos dentro de uma função de linha 200 + é difícil. Mas acompanhá-los através de mais de 10 funções individuais, tentando lembrar as relações entre eles? Isso pode ser ainda mais difícil.

No entanto, novamente, a auto-documentação semântica através de nomes pode desempenhar um papel fundamental. Se cada função tiver um nome lógico, então tudo o que você precisa fazer para validar uma das funções folha (além do teste de unidade) é ver se ela realmente faz o que diz. As funções das folhas tendem a ser curtas e focadas. Então, se cada função folha individual faz o que diz que deveria, então o único problema possível é que alguém lhes passou as coisas erradas.

Então, nesse caso, pode ser um benefício para a correção de bugs.

Acho que realmente se trata da questão de se você pode atribuir um nome significativo a uma unidade lógica. Se sim, então provavelmente pode ser uma função.

    
por 23.01.2016 / 01:31
fonte
12

Sempre que sentir a necessidade de escrever um comentário para descrever o que um bloco de texto está fazendo, você encontrou uma oportunidade de extrair um método.

Em vez de

//find eligible contestants
var eligible = contestants.Where(c=>c.Age >= 18)
eligible = eligible.Where(c=>c.Country == US)

tente

var eligible = FindEligible(contestants)
    
por 23.01.2016 / 13:34
fonte
5

DRY - Não se repita - é apenas um dos vários princípios que precisam ser equilibrados.

Alguns outros que vêm à mente aqui estão nomeando. Se a lógica não for óbvia para o leitor casual, a extração para o método / função cujo nome melhor encapsula o que e por que ele está fazendo isso pode melhorar a legibilidade do programa.

Também com o objetivo de menos de 5-10 linhas de código método / função pode entrar em jogo, dependendo de quantas linhas o //logic se torna.

Além disso, uma função com parâmetros pode atuar como uma API e pode nomear os parâmetros apropriadamente para tornar a lógica de código mais clara.

Além disso, você pode descobrir que, com o tempo, as coleções de tais funções revelam o agrupamento útil da cúpula, por exemplo, admin e, em seguida, eles podem ser facilmente reunidos sob ele.

    
por 23.01.2016 / 01:33
fonte
4

O ponto sobre a divisão de funções é tudo sobre uma coisa: simplicidade.

Um leitor de código não pode ter mais de sete coisas em mente simultaneamente. Suas funções devem refletir isso.

  • Se você criar funções muito longas, elas ficarão ilegíveis porque você tem muito mais do que sete coisas dentro de suas funções.

  • Se você construir uma tonelada de funções de uma linha, os leitores se confundem da mesma forma no emaranhado de funções. Agora você teria que manter mais de sete funções em sua memória para entender o propósito de cada uma delas.

  • Algumas funções são simples, embora sejam longas. O exemplo clássico é quando uma função contém uma grande instrução switch com muitos casos. Contanto que o manuseio de cada caso seja simples, suas funções não são muito longas.

Ambos os extremos (o Megamoth e a sopa de pequenas funções) são igualmente ruins, e você precisa encontrar um equilíbrio no meio. Na minha experiência, uma função legal tem algo em torno de dez linhas. Algumas funções serão one-liners, algumas excederão vinte linhas. O importante é que cada função serve uma função facilmente compreensível, sendo igualmente compreensível em sua implementação.

    
por 23.01.2016 / 21:58
fonte
2

Depende muito do seu // Logic Here .

Se for um one-liner, provavelmente você não precisa de uma decomposição funcional.

Se, por outro lado, são linhas e linhas de código, então é muito melhor colocá-lo em uma função separada e nomeá-lo apropriadamente ( f1,f2,f3 não passa aqui).

Tudo isso tem a ver com cérebros humanos, em média, não são muito eficientes com o processamento de grandes quantidades de dados de relance. Em certo sentido, não importa quais dados: função multi-linha, interseção movimentada, quebra-cabeça de 1000 peças.

Seja um amigo do cérebro do seu mantenedor de código, reduza as partes no ponto de entrada. Quem sabe, esse mantenedor de código pode até ser você alguns meses depois.

    
por 23.01.2016 / 03:28
fonte
2

É tudo sobre separação de interesses . (ok, não todos sobre isso; isso é uma simplificação).

Tudo bem:

function initializeUser(name, job, bye) {
    this.username = name;
    this.occupation = job;
    this.farewell = bye;
    this.gender = Gender.unspecified;
    this.species = Species.getSpeciesFromJob(this.occupation);
    ... etc in the same vein.
}

Essa função está relacionada a uma única preocupação: define as propriedades iniciais de um usuário a partir dos argumentos, padrões, extrapolação, etc. fornecidos.

Isso não está bem:

function initializeUser(name, job, bye) {
    // Connect to internet if not already connected.
    modem.getInstance().ensureConnected();
    // Connect to user database
    userDb = connectToDb(USER_DB);
    // Validate that user does not yet exist.
    if (1 != userDb.db_exec("SELECT COUNT(*) FROM 'users' where 'name' = %d", name)) {
        throw new sadFace("User exists");
    }
    // Configure properties. Don't try to translate names.
    this.username = removeBadWords(name);
    this.occupation = removeBadWords(translate(name));
    this.farewell = removeBadWords(translate(name));
    this.gender = Gender.unspecified;
    this.species = Species.getSpeciesFromJob(this.occupation);
    userDb.db_exec("INSERT INTO 'users' set 'name' = %s", this.username);
    // Disconnect from the DB.
    userDb.disconnect();
}

A separação de interesses sugere que isso deve ser tratado como múltiplas preocupações; o manuseio do banco de dados, a validação de existência do usuário, a configuração das propriedades. Cada um deles é muito facilmente testado como uma unidade, mas testar todos eles em um único método faz com que um conjunto de testes de unidade seja complicado, o que testaria as coisas de forma diferente com a maneira como ele lidava com o banco de dados. títulos de trabalho vazios e inválidos, e como ele lida com a criação de um usuário duas vezes (resposta: mal, há um bug).

Parte do problema é que isso acontece em termos de quão alto nível é: redes de baixo nível e coisas de DB não têm lugar aqui. Isso é parte da preocupação com a separação. A outra parte é aquela coisa que deveria ser a preocupação de outra coisa, é a preocupação da função init. Por exemplo, se traduzir ou aplicar filtros de linguagem incorreta, pode fazer mais sentido como uma preocupação dos campos que estão sendo definidos.

    
por 25.01.2016 / 01:45
fonte
1

A resposta "certa", de acordo com os dogmas de codificação predominantes, é dividir grandes funções em funções pequenas, fáceis de ler, testáveis e testadas com nomes de autodocumentação.

Dito isto, definir "grande" em termos de "linhas de código" pode parecer arbitrário, dogmático e tedioso, o que pode causar desentendimentos desnecessários, escrúpulos e tensão. Mas, não tenha medo! Pois, se reconhecermos que os propósitos principais por trás do limite de linhas de código são a legibilidade e testabilidade, podemos encontrar facilmente o limite de linha apropriado para qualquer função específica! (E comece a construir as bases para um limite mínimo relevante ).

Concordar como equipe para permitir as funções megalíticas e extrair linhas para funções menores e bem nomeadas no sinal primeiro de que a função é difícil de ler como um todo. ou quando os subconjuntos são incertos quanto à exatidão.

... E se todos forem honestos durante a implementação inicial, e se nenhum deles estiver divulgando um QI acima de 200, os limites de compreensibilidade e testabilidade podem ser identificados antes que alguém veja seu código.

    
por 23.01.2016 / 06:39
fonte