Devo extrair funcionalidades específicas em uma função e por quê?

29

Eu tenho um método grande que faz 3 tarefas, cada uma delas pode ser extraída em uma função separada. Se eu criar funções adicionais para cada uma dessas tarefas, ele tornará meu código melhor ou pior e por quê?

Obviamente, ele criará menos linhas de código na função principal, mas haverá declarações de funções adicionais, portanto minha classe terá métodos adicionais, que eu acredito que não são bons, porque tornarão a classe mais complexa.

Devo fazer isso antes de escrever todo o código ou devo deixá-lo até que tudo esteja pronto e depois extrair funções?

dhblah
fonte
19
"Deixo até que tudo esteja pronto" é geralmente sinônimo de "Isso nunca será feito".
Euphoric
2
Isso geralmente é verdade, mas lembre-se também do princípio oposto do YAGNI (que não se aplica neste caso, pois você já precisa dele).
Jhocking 01/10/12
Só queria enfatizar não se concentrar tanto na redução de linhas de código. Em vez disso, tente pensar em termos de abstrações. Cada função deve ter apenas um trabalho. Se você achar que suas funções estão realizando mais de um trabalho, geralmente refatorar o método. Se você seguir estas diretrizes, será quase impossível ter funções excessivamente longas.
Adrian

Respostas:

35

Este é um livro com o qual frequentemente ligo, mas aqui vou novamente: Clean Code , de Robert C. Martin , capítulo 3, "Funções".

Obviamente, ele criará menos linhas de código na função principal, mas haverá declarações de funções adicionais, portanto minha classe terá métodos adicionais, que eu acredito que não são bons, porque tornarão a classe mais complexa.

Você prefere ler uma função com +150 linhas ou uma função que chama 3 +50 linhas? Eu acho que prefiro a segunda opção.

Sim , ele tornará seu código melhor no sentido de que será mais "legível". Crie funções que executem uma e apenas uma coisa; elas serão mais fáceis de manter e produzir um caso de teste.

Além disso, uma coisa muito importante que aprendi com o livro mencionado: escolha nomes bons e precisos para suas funções. Quanto mais importante for a função, mais preciso será o nome. Não se preocupe com o tamanho do nome, se for necessário chamá-lo FunctionThatDoesThisOneParticularThingOnly, nomeie-o dessa maneira.

Antes de executar seu refator, escreva um ou mais casos de teste. Verifique se eles funcionam. Depois de concluir sua refatoração, você poderá iniciar esses casos de teste para garantir que o novo código funcione corretamente. Você pode escrever testes "menores" adicionais para garantir que suas novas funções funcionem bem separadamente.

Finalmente, e isso não é contrário ao que acabei de escrever, pergunte a si mesmo se você realmente precisa fazer essa refatoração. Confira as respostas para " Quando refatorar ?" (também, pesquise perguntas SO sobre "refatoração", há mais e respostas interessantes para ler)

Devo fazer isso antes de escrever todo o código ou devo deixá-lo até que tudo esteja pronto e depois extrair funções?

Se o código já estiver lá e funcionar e você tiver pouco tempo para a próxima versão, não toque nele. Caso contrário, acho que devemos fazer pequenas funções sempre que possível e, como tal, refatorar sempre que houver algum tempo disponível, garantindo que tudo funcione como antes (casos de teste).

Jalayn
fonte
10
Na verdade, Bob Martin mostrou várias vezes que prefere 7 funções com 2 a 3 linhas em vez de uma função com 15 linhas (veja aqui sites.google.com/site/unclebobconsultingllc/… ). E é aí que muitos desenvolvedores experientes vão resistir. Pessoalmente, acho que muitos desses "desenvolvedores experientes" apenas têm problemas para aceitar que ainda podem melhorar algo básico, como criar abstrações com funções após mais de 10 anos de codificação.
Doc Brown
+1 apenas para fazer referência a um livro que, para minha opinião modesta, deveria estar nas prateleiras de qualquer empresa de software.
Fabio Marcolini
3
Talvez eu esteja parafraseando aqui, mas uma frase daquele livro que ecoa na minha cabeça quase todos os dias é "cada função deve fazer apenas uma coisa e fazê-la bem". Parece particularmente relevante aqui desde o OP disse "minha principal função faz três coisas"
wakjah
Você está absolutamente correto!
Jalayn
Depende de quanto as três funções separadas estão interligadas. Pode ser mais fácil seguir um bloco de código em um único local do que três blocos de código que dependem repetidamente um do outro.
user253751
13

Sim, obviamente. Se é fácil ver e separar as diferentes "tarefas" de função única.

  1. Legibilidade - Funções com bons nomes tornam explícito o que o código faz sem a necessidade de ler esse código.
  2. Reutilização - É mais fácil usar a função que faz uma coisa em vários lugares, do que ter uma função que faz coisas que você não precisa.
  3. Testabilidade - É mais fácil testar a função, que possui uma "função" definida, aquela que possui muitas delas

Mas pode haver problemas com isso:

  • Não é fácil ver como separar a função. Isso pode exigir a refatoração do interior da função primeiro, antes de você avançar para a separação.
  • A função possui um enorme estado interno, que é repassado. Isso geralmente requer algum tipo de solução OOP.
  • É difícil dizer qual função deveria estar fazendo. Unidade de teste e refatorar até que você saiba.
Eufórico
fonte
5

O problema que você está colocando não é um problema de codificação, convenções ou prática de codificação, mas um problema de legibilidade e de como os editores de texto mostram o código que você escreve. Este mesmo problema está aparecendo também no post:

É correto dividir funções e métodos longos em outros menores, mesmo que eles não sejam chamados por mais nada?

Dividir uma função em subfunções faz sentido ao implementar um grande sistema com a intenção de encapsular as diferentes funcionalidades das quais ele será composto. No entanto, mais cedo ou mais tarde, você se encontrará com várias funções importantes. Alguns deles são ilegíveis e difíceis de manter, se você os mantém como funções longas únicas ou se os dividem em funções menores. Isto é particularmente verdade para as funções em que as operações que você realiza não são necessárias em nenhum outro local do seu sistema. Permite pegar uma de uma função tão longa e considerá-la em uma visão mais ampla.

Pró:

  • Depois de ler, você tem uma idéia completa de todas as operações que a função faz (você pode ler como um livro);
  • Se você deseja depurá-lo, é possível executá-lo passo a passo sem nenhum salto para qualquer outro arquivo / parte do arquivo;
  • Você tem a liberdade de acessar / usar qualquer variável declarada em qualquer estágio da função;
  • O algoritmo em que a função implementa está totalmente contido na função (encapsulado);

Contra:

  • Leva muitas páginas da sua tela;
  • Leva muito tempo para lê-lo;
  • Não é fácil memorizar todos os diferentes passos;

Agora vamos imaginar dividir a função longa em várias subfunções e examiná-las com uma perspectiva mais ampla.

Pró:

  • Exceto as funções de sair, cada função descreve com palavras (nomes de subfunções) os diferentes passos realizados;
  • Leva muito pouco tempo para ler cada função / subfunção;
  • Está claro quais parâmetros e variáveis ​​são afetados em cada subfunção (separação de preocupações);

Contra:

  • É fácil imaginar o que uma função como "sin ()" faz, mas não é tão fácil imaginar o que nossas subfunções fazem;
  • O algoritmo está desaparecido, agora está distribuído em sub-funções may (sem visão geral);
  • Ao depurar passo a passo, é fácil esquecer a chamada de função do nível de profundidade de onde você está (pulando aqui e ali nos arquivos do projeto);
  • Você pode facilmente perder o contexto ao ler as diferentes sub-funções;

Ambas as soluções têm pró e contra. A melhor solução real seria ter editores que permitissem expandir, em linha e para toda a profundidade, cada chamada de função em seu conteúdo. O que tornaria a divisão de funções em subfunções a única melhor solução.

Antonello Ceravola
fonte
2

Para mim, existem quatro razões para extrair blocos de código em funções:

  • Você está reutilizando : você acabou de copiar um bloco de código na área de transferência. Em vez de colá-lo, coloque-o em uma função e substitua o bloco por uma chamada de função nos dois lados. Portanto, sempre que você precisar alterar esse bloco de código, precisará alterar apenas essa função em vez de alterar o código em vários locais. Portanto, sempre que você copiar um bloco de código, você deve criar uma função.

  • É um retorno de chamada : é um manipulador de eventos ou algum tipo de código de usuário que uma biblioteca ou uma estrutura chama. (Mal posso imaginar isso sem criar funções.)

  • Você acredita que será reutilizado , no projeto atual ou em outro lugar: você acabou de escrever um bloco que calcula a subsequência comum mais longa de duas matrizes. Mesmo que seu programa chame essa função apenas uma vez, eu acreditaria que também precisarei dessa função em outros projetos também.

  • Você deseja um código de autodocumentação : portanto, em vez de escrever uma linha de comentário em um bloco de código resumindo o que faz, você extrai a coisa toda em uma função e nomeia o que você escreveria em um comentário. Embora eu não seja fã disso, porque gosto de escrever o nome do algoritmo usado, a razão pela qual escolhi esse algoritmo etc. Os nomes das funções seriam muito longos ...

Calmarius
fonte
1

Tenho certeza de que você ouviu o conselho de que as variáveis ​​devem ter um escopo tão rígido quanto possível e espero que você concorde com isso. Bem, funções são contêineres de escopo e, em funções menores, o escopo das variáveis ​​locais é menor. É muito mais claro como e quando eles devem ser usados ​​e é mais difícil usá-los na ordem errada ou antes de serem inicializados.

Além disso, funções são contêineres de fluxo lógico. Há apenas uma maneira de entrar, as saídas são claramente marcadas e, se a função for curta o suficiente, os fluxos internos deverão ser óbvios. Isso tem o efeito de reduzir a complexidade ciclomática, que é uma maneira confiável de reduzir a taxa de defeitos.

John Wu
fonte
0

Além: escrevi isso em resposta à pergunta de dallin (agora fechada), mas ainda acho que poderia ser útil para alguém, então aqui vai


Penso que o motivo das funções de atomização é 2 vezes e, como o @jozefg menciona, depende da linguagem usada.

Separação de preocupações

O principal motivo para fazer isso é manter diferentes partes do código separadas, para que qualquer bloco de código que não contribua diretamente para o resultado / intenção da função seja uma preocupação separada e possa ser extraído.

Digamos que você tenha uma tarefa em segundo plano que também atualiza uma barra de progresso, a atualização da barra de progresso não está diretamente relacionada à tarefa de longa execução, portanto, deve ser extraída, mesmo que seja o único trecho de código que usa a barra de progresso.

Digamos que em JavaScript você tenha uma função getMyData (), que 1) cria uma mensagem de sabão a partir de parâmetros, 2) inicializa uma referência de serviço, 3) chama o serviço com a mensagem de sabão, 4) analisa o resultado, 5) retorna o resultado. Parece razoável, eu escrevi essa função exata várias vezes - mas realmente isso pode ser dividido em 3 funções privadas, incluindo apenas o código 3 e 5 (se houver), pois nenhum outro código é diretamente responsável por obter dados do serviço .

Experiência de depuração aprimorada

Se você possui funções completamente atômicas, seu rastreamento de pilha se torna uma lista de tarefas, listando todo o código executado com êxito, ou seja:

  • Obter meus dados
    • Mensagem Build Soap
    • Inicializar referência de serviço
    • Resposta do Serviço Analisado - ERRO

seria muito mais interessante do que descobrir que houve um erro ao obter dados. Porém, algumas ferramentas são ainda mais úteis para depurar árvores de chamadas detalhadas do que, por exemplo, o Microsofts Debugger Canvas .

Também entendo suas preocupações de que pode ser difícil seguir o código escrito dessa maneira, porque no final do dia, você precisa escolher uma ordem de funções em um único arquivo, onde sua árvore de chamadas seria muito mais complexa do que aquela . Mas se as funções tiverem um bom nome (o intellisense me permite usar 3-4 palavras maiúsculas em qualquer função que eu queira, sem abrandar nenhuma) e estruturadas com interface pública na parte superior do arquivo, seu código será como pseudo-código que é de longe a maneira mais fácil de obter um entendimento de alto nível de uma base de código.

Para sua informação, este é um daqueles "faça o que eu digo, não o que faço", manter o código atômico é inútil, a menos que você seja cruelmente consistente com ele no IMHO, o que eu não sou.

Dead.Rabit
fonte