Os métodos longos são sempre ruins?

64

Então, olhando em volta mais cedo, notei alguns comentários sobre métodos longos sendo uma má prática.

Não sei se concordo sempre que os métodos longos são ruins (e gostariam de opiniões de outras pessoas).

Por exemplo, eu tenho algumas views do Django que fazem um pouco de processamento dos objetos antes de enviá-los para a view, um método longo sendo 350 linhas de código. Eu tenho meu código escrito para que ele lide com os paramaters - classificando / filtrando o queryset, então pouco a pouco faz algum processamento nos objetos que minha consulta retornou.

Portanto, o processamento é principalmente agregação condicional, que tem regras complexas o suficiente para que não possam ser feitas facilmente no banco de dados, então eu tenho algumas variáveis declaradas fora do loop principal e depois alteradas durante o loop.

variable_1 = 0
variable_2 = 0
for object in queryset :
     if object.condition_condition_a and variable_2 > 0 :
     variable 1+= 1
     .....
     ...    
     . 
      more conditions to alter the variables

return queryset, and context 

Então, de acordo com a teoria, eu deveria fatorar todo o código em métodos menores, de modo que eu tenha o método view como sendo o máximo de uma página.

No entanto, tendo trabalhado em várias bases de código no passado, às vezes acho que torna o código menos legível, quando você precisa pular constantemente de um método para o próximo, descobrindo todas as partes dele, enquanto mantém o método mais externo em sua cabeça.

Eu acho que tendo um longo método que é bem formatado, você pode ver a lógica mais facilmente, já que ela não está sendo escondida em métodos internos.

Eu poderia fatorar o código em métodos menores, mas muitas vezes existe um loop interno sendo usado para duas ou três coisas, então isso resultaria em código mais complexo, ou métodos que não fazem uma coisa a não ser dois ou três (alternativamente eu poderia repetir loops internos para cada tarefa, mas depois haverá um hit de desempenho).

Então, há um caso em que métodos longos nem sempre são ruins? Há sempre um caso para escrever métodos, quando eles só serão usados em um lugar?

ATUALIZAÇÃO: Parece que fiz esta pergunta há mais de um ano.

Então eu refatorei o código após a resposta (mista) aqui, dividi-lo em métodos. É um aplicativo Django que recupera conjuntos complexos de objetos relacionados do banco de dados, então o argumento de teste está fora (provavelmente teria levado a maior parte do ano para criar objetos relevantes para os casos de teste. Eu tenho um tipo "isso precisa ser feito ontem") ambiente de trabalho antes que alguém se queixe). Corrigir bugs nessa parte do código é um pouco mais fácil agora, mas não de forma massiva.

antes:

#comment 1 
bit of (uncomplicated) code 1a  
bit of code 2a

#comment 2 
bit of code 2a
bit of code 2b
bit of code 2c

#comment 3
bit of code 3

agora:

method_call_1
method_call_2
method_call_3

def method_1 
    bit of (uncomplicated) code 1a  
    bit of code 2a

def method_2 
    bit of code 2a
    bit of code 2b
    bit of code 2c

def method_3
    bit of code 3
    
por wobbily_col 18.10.2013 / 20:37
fonte

18 respostas

78

Não, os métodos longos nem sempre são ruins.

No livro Código Completo , mede-se que os métodos longos são, às vezes, mais rápidos e fáceis de escrever, e não levam a problemas de manutenção.

Na verdade, o que é realmente importante é ficar seco e respeitar a separação de interesses. Às vezes, o cálculo é longo para escrever, mas não causará problemas no futuro.

No entanto, pela minha experiência pessoal, a maioria dos métodos longos tendem a não ter separação de interesses. Na verdade, métodos longos são uma maneira fácil de detectar que algo pode estar errado no código, e que é necessário um cuidado especial ao fazer uma revisão de código.

EDITAR: Como comentários são feitos, eu adiciono um ponto interessante para a resposta. Na verdade, também verificaria métricas de complexidade para a função (NPATH, complexidade ciclomática ou ainda melhor CRAP).

Na verdade, eu recomendo não verificar tais métricas em funções longas, mas incluir alertas sobre elas com ferramentas automatizadas (como checkstyle para java, por exemplo) SOBRE TODAS AS FUNÇÕES.

    
por 17.10.2012 / 23:33
fonte
54

A maior parte do foco aqui parece estar em torno da palavra sempre . Sim, absolutos são ruins, e engenharia de software é quase tanto arte quanto ciência, e tudo isso ... mas eu vou ter que dizer que, para o exemplo que você deu, o método seria melhor se fosse dividido acima. Estes são os argumentos que eu normalmente usaria para justificar a divisão do seu método:

Legibilidade: Não tenho certeza sobre outras pessoas, mas não consigo ler 350 linhas de código rapidamente. Sim, se é o meu código próprio , e eu posso fazer muitas suposições sobre isso, eu poderia examiná-lo rapidamente, mas isso é além do ponto. Considere o quanto esse método seria mais fácil de ler, se ele consistisse em 10 chamadas para outros métodos (cada um com um nome descritivo). Fazendo isso, você introduziu uma camada no código, e o método de alto nível dá um esboço curto e doce para o leitor sobre o que está acontecendo.

Editar - para colocar isso em uma luz diferente, pense nisso é assim: como você explicaria esse método para um novo membro da equipe? Certamente tem alguma estrutura que você pode resumir ao longo das linhas de "bem, começa fazendo A, depois B, depois C, etc". Ter um método curto de "visão geral" chamando outros métodos torna essa estrutura óbvia. É extremamente raro encontrar 350 linhas de código que não beneficiem; o cérebro humano não está destinado a lidar com listas de centenas de itens, nós os agrupamos.

Reutilização: Métodos longos tendem a ter baixa coesão - eles frequentemente fazem mais de uma coisa. A baixa coesão é inimiga da reutilização; se você combinar várias tarefas em um método, acabará sendo reutilizado em menos lugares do que deveria.

Testabilidade e coesão: mencionei complexidade ciclomática em um comentário acima - é uma bela boa medida de quão complexo é o seu método. Ele representa o limite inferior do número de caminhos únicos através de seu código, dependendo das entradas (edit: corrigido de acordo com o comentário de MichaelT). Isso também significa que, para testar adequadamente seu método, você precisaria ter pelo menos tantos casos de teste quanto o seu número de complexidade ciclomática. Infelizmente, quando você reúne partes de código que não são realmente dependentes umas das outras, não há como ter certeza dessa falta de dependência, e a complexidade tende a se multiplicar. Você pode pensar nesta medida como uma indicação do número de coisas diferentes que você está tentando fazer. Se for muito alto, é hora de dividir e conquistar.

Refatoração e estrutura: Métodos longos são frequentemente sinais de que falta alguma estrutura no código. Muitas vezes, o desenvolvedor não conseguia descobrir quais são os pontos em comum entre as diferentes partes desse método e onde uma linha poderia ser traçada entre eles. Perceber que um método longo é um problema e tentar dividi-lo em métodos menores é o primeiro passo em um caminho mais longo para realmente identificar uma estrutura melhor para a coisa toda. Talvez você precise criar uma classe ou duas; não vai necessariamente ser mais complexo no final!

Eu também acho que neste caso, a desculpa para ter um método longo é "... algumas variáveis declaradas fora do loop principal são alteradas durante o loop". Eu não sou especialista em Python, mas tenho quase certeza de que esse problema pode ser trivialmente corrigido por alguma forma de passar por referência.

    
por 15.10.2012 / 16:58
fonte
28

Os métodos longos são sempre ruins, mas ocasionalmente são melhores que as alternativas.

    
por 15.10.2012 / 13:39
fonte
8

Os métodos longos são um código de cheiro . Eles geralmente indicam que algo está errado, mas não é uma regra dura e rápida. Normalmente, casos em que eles são justificados envolvem muitas regras de negócios de estado e bastante complexas (como você descobriu).

Quanto à sua outra pergunta, é útil separar pedaços de lógica em métodos separados, mesmo que sejam chamados apenas uma vez. Isso facilita a visualização da lógica de alto nível e pode tornar o tratamento de exceções um pouco mais limpo. Contanto que você não precise passar em vinte parâmetros para representar o estado de processamento!

    
por 15.10.2012 / 14:11
fonte
7

Os métodos longos nem sempre são ruins. Eles geralmente são um sinal de que pode haver um problema.

No sistema em que estou trabalhando, temos meia dúzia de métodos com mais de 10.000 linhas. Uma delas tem atualmente 54.830 linhas. E tudo bem.

Essas funções ridiculamente longas são muito simples e são autogeradas. Aquele grande monstro de 54.830 linhas de comprimento contém os dados diários de movimento polar de 1º de janeiro de 1962 a 10 de janeiro de 2012 (nosso último lançamento). Também lançamos um procedimento pelo qual nossos usuários podem atualizar esse arquivo gerado automaticamente. Esse arquivo de origem contém os dados de movimento polar do link , traduzido automaticamente para C ++ .

A leitura rápida desse site não é possível em uma instalação segura. Não há conexão com o mundo exterior. Fazer o download do site como uma cópia e análise local em C ++ também não é uma opção; a análise é lenta , e isso tem que ser rápido. Baixando, auto-traduzindo para C ++ e compilando: Agora você tem algo que é rápido. (Só não compile isso otimizado. É incrível o tempo que um compilador otimizador leva para compilar 50.000 linhas de código de linha reta extremamente simples. Demora mais de meia hora no meu computador para compilar aquele arquivo otimizado. E a otimização não faz absolutamente nada Não há nada para otimizar. É um código simples de linha reta, uma declaração de atribuição após a outra.)

    
por 15.10.2012 / 14:32
fonte
7

Digamos que existem maneiras boas e ruins de quebrar um método longo. Ter que "[manter] o método mais externo na sua cabeça" é um sinal de que você não está se separando da maneira mais ideal, ou que seus métodos são mal nomeados. Em teoria, há casos em que um método longo é melhor. Na prática, é extremamente raro. Se você não conseguir descobrir como tornar um método mais curto legível, peça a alguém para revisar seu código e peça-lhe especificamente ideias sobre como encurtar os métodos.

Quanto a múltiplos loops causando um suposto desempenho, não há como saber isso sem medir. Vários loops menores podem ser significativamente mais rápidos se isso significar que tudo o que precisa pode permanecer no cache. Mesmo que haja um impacto no desempenho, geralmente é insignificante em favor da legibilidade.

Eu direi que frequentemente os métodos longos são mais fáceis de escrever, mesmo que sejam mais difíceis de ler. É por isso que eles proliferam mesmo que ninguém goste deles. Não há nada de errado em planejar desde o início para refatorar antes de fazer o check-in.

    
por 15.10.2012 / 15:58
fonte
4

Os métodos longos podem ser mais computacionais e eficientes em termos de espaço, pode ser mais fácil ver a lógica e mais fácil depurá-los. No entanto, essas regras só se aplicam quando apenas um programador toca nesse código. O código será difícil de extender se não for atômico, essencialmente, a próxima pessoa terá que começar do zero e, em seguida, a depuração e o teste levarão para sempre, já que não está usando nenhum código bom conhecido.

    
por 15.10.2012 / 14:20
fonte
3

Existe algo que chamamos de Decomposição Funcional , que significa dividir seus métodos mais longos em menores, sempre que possível. Como você mencionou que seu método envolve a classificação / filtragem, é melhor ter métodos ou funções separados para essas tarefas.

Precisamente, seu método deve ser focado apenas na execução de 1 tarefa.

E se precisar chamar outro método, por algum motivo, faça o mesmo com o que você já está escrevendo. Também para fins de legibilidade, você pode adicionar comentários. Convencionalmente, os programadores usam comentários de várias linhas (/ ** / em C, C ++, C # e Java) para descrições de métodos e usam comentários de linha única (// em C, C ++, C # e Java). Também há boas ferramentas de documentação disponíveis para maior legibilidade do código (por exemplo, JavaDoc ). Você também pode pesquisar em comentários baseados em XML se for um desenvolvedor de .Net.

Os loops afetam o desempenho do programa e podem causar sobrecarga do aplicativo se não forem usados adequadamente. A idéia é projetar seu algoritmo de forma que você use loops aninhados o mínimo possível.

    
por 15.10.2012 / 14:36
fonte
3

Está tudo bem escrever longas funções. Mas isso varia no contexto se você realmente precisa ou não. Por exemplo, alguns dos melhores algorthms são expressos melhor quando se trata de um pedaço. Por outro lado, uma grande porcentagem de rotinas em programas orientados a objetos será rotinas de acesso, que serão muito curtas. Algumas das longas rotinas de processamento que tem casos de switch longos, se as condições puderem ser otimizadas por meio de métodos orientados por tabela.

Existe uma excelente discussão curta no Código Completo 2 sobre a duração das rotinas.

The theoretical best 497 maximum length is often described as one or two pages of program listing, 66 to 132 lines. Modern programs tend to have volumes of extremely short routines mixed in with a few longer routines.

Decades of evidence say that routines of such length are no more error prone than shorter routines. Let issues such as depth of nesting, number of variables, and other complexity-related considerations dictate 535 the length of the routine rather than imposing a length

If you want to write routines longer than about 200 lines, be careful. None of the studies that reported decreased cost, decreased error rates, or both with larger routines distinguished among sizes larger than 200 lines, and you’re bound to run into an upper limit of understandability as you pass 200 lines of code. 536 restriction per se.

    
por 16.10.2012 / 08:12
fonte
2

Outra votação que é quase sempre errada. No entanto, encontro dois casos básicos em que é a resposta correta:

1) Um método que basicamente apenas chama um monte de outros métodos e não faz nenhum trabalho real. Você tem um processo que leva 50 etapas para realizar, você obtém um método com 50 chamadas nele. Geralmente não há nada a ser ganho ao tentar resolver isso.

2) Despachantes. O design da POO livrou-se da maioria desses métodos, mas as fontes de dados que chegam são, por natureza, apenas dados e, portanto, não podem seguir os princípios da POO. Não é exatamente incomum ter algum tipo de rotina de dispatchers no código que manipula os dados.

Eu também diria que não se deve sequer considerar a questão quando se lida com material autogerado. Ninguém está tentando entender o que o código autogerado faz, não importa nem um pouco se é fácil para um humano entender.

    
por 16.10.2012 / 00:52
fonte
2

Eu queria abordar o exemplo que você deu:

For example I have some Django views that do a bit of processing of the objects before sending them to the view, a long method being 350 lines of code. I have my code written so that it deals with the paramaters - sorting / filtering the queryset, then bit by bit does some processing on the objects my query has returned.

Na minha empresa, nosso maior projeto é baseado no Django e também temos funções de visualização longa (muitas são mais de 350 linhas). Eu diria que não precisa ser tanto tempo, e eles estão nos machucando.

Essas funções de exibição estão realizando muitos trabalhos fracamente relacionados que devem ser extraídos para o modelo, classes auxiliares ou funções auxiliares. Além disso, acabamos reutilizando as visualizações para fazer coisas diferentes, que devem ser divididas em visualizações mais coesas.

Eu suspeito que seus pontos de vista tenham características semelhantes. No meu caso, sei que isso causa problemas e estou trabalhando para fazer mudanças. Se você não concorda que está causando problemas, não precisa corrigi-lo.

    
por 17.10.2012 / 20:37
fonte
2

Eu não sei se alguém já mencionou isso, mas uma das razões pelas quais os métodos longos são ruins é porque eles geralmente envolvem vários níveis diferentes de abstração. Você tem variáveis de loop e todos os tipos de coisas acontecendo. Considere a função fictícia:

function nextSlide() {
  var content = getNextSlideContent();
  hideCurrentSlide();
  var newSlide = createSlide(content);
  setNextSlide(newSlide);
  showNextSlide();
}

Se você estivesse fazendo toda a animação, cálculo, acesso a dados etc nessa função, teria sido uma bagunça. A função nextSlide () mantém uma camada de abstração consistente (o sistema de estados do slide) e ignora outras. Isso torna o código legível.

Se você tem que constantemente entrar em métodos menores para ver o que eles fazem, então o exercício de dividir a função falhou. Só porque o código que você está lendo não está fazendo coisas óbvias em métodos filhos não significa que métodos filhos são uma má idéia, apenas que isso foi feito incorretamente.

Quando eu crio métodos, geralmente acabo dividindo-os em métodos menores, como uma espécie de estratégia de dividir e conquistar. Um método como

   if (hasMoreRecords()) { ... }

é certamente mais legível que

if (file.isOpen() && i < recordLimit && currentRecord != null) { ... } 

certo?

Eu concordo que declarações absolutas são ruins e também concordo que geralmente um método longo é ruim.

    
por 14.11.2012 / 08:28
fonte
1

História verdadeira. Certa vez, encontrei um método com mais de duas mil linhas. O método tinha regiões que descreviam o que estava fazendo nessas regiões. Depois de ler uma região, decidi fazer um método de extração automatizado, nomeando-o de acordo com o nome da região. quando terminei, o método não passava de 40 métodos de cerca de cinquenta linhas cada e tudo funcionava da mesma forma.

O que é muito grande é subjetivo. Às vezes, um método não pode ser dividido além do que é atualmente. É como escrever um livro. A maioria das pessoas concorda que parágrafos longos geralmente devem ser divididos. Mas, às vezes, há apenas uma ideia e dividi-la causa mais confusão do que seria causado pela duração desse parágrafo.

    
por 15.10.2012 / 20:12
fonte
0

O objetivo de um método é ajudar a reduzir a regurgitação de código. Um método deve ter uma função específica pela qual é responsável. Se você acabar reescrevendo o código em vários lugares, correrá o risco de o código ter resultados inesperados se as especificações para as quais o software foi projetado forem alteradas.

Para um método ter 350 linhas, sugeriria que muitas das tarefas que está sendo executada são replicadas em outro lugar, pois é incomum exigir uma quantidade tão grande de código para executar uma tarefa especializada.

    
por 15.10.2012 / 13:35
fonte
0

Não são realmente os longos métodos que são uma má prática, é mais deixá-los assim, o que é ruim.

Quero dizer que o ato real de refatorar sua amostra de:

varaible_1 = 0
variable_2 = 0
for object in queryset :
     if object.condition_condition_a and variable_2 > 0 :
     variable 1+= 1
     .....
     ...    
     . 
      more conditions to alter the variables

return queryset, and context 

para

Status status = new Status();
status.variable1 = 0;
status.variable2 = 0;
for object in queryset :
     if object.condition_condition_a and status.variable2 > 0 :
     status.variable1 += 1
     .....
     ...    
     . 
      more conditions to alter the variables (status)

return queryset, and context 

e depois para

class Status {
    variable1 = 0;
    variable2 = 0;

    void update(object) {
        if object.condition_condition_a and variable2 > 0 {
            variable1 += 1
        }
    }
};

Status status = new Status();
for object in queryset :
     status.update(object);
     .....
     ...    
     . 
      more conditions to alter the variables (status)

return queryset, and context 

você está agora a caminho de não apenas um método muito mais curto, mas muito mais utilizável e compreensível.

    
por 15.10.2012 / 16:28
fonte
0

Eu acho que o fato de que o método é muito longo é algo para verificar, mas definitivamente não é um anti-padrão instantâneo. A grande coisa para procurar em métodos enormes é muito de aninhamento. Se você tiver

foreach(var x in y)
{
    //do ALL the things
    //....
}

e o corpo do loop não é extremamente localizado (ou seja, você poderia enviar menos de 4 parâmetros), então é melhor convertê-lo para:

foreach(var x in y)
{
    DoAllTheThings(x);
}
...
void DoAllTheThings(object x)
{
    //do ALL the things
    //....
}

Por sua vez, isso pode reduzir muito o tamanho de uma função. Além disso, certifique-se de procurar código duplicado na função e movê-lo para uma função separada

Finalmente, alguns métodos são longos e complicados e não há nada que você possa fazer. Alguns problemas precisam de soluções que não são fáceis de codificar. Por exemplo, analisar uma gramática de muita complexidade pode tornar métodos realmente longos que você realmente não pode fazer muito sem piorar.

    
por 15.10.2012 / 17:59
fonte
0

A verdade é que depende. Como mencionado, se o código não separa as preocupações e tenta fazer tudo em um método, então é um problema. Separar o código em vários módulos facilita a leitura do código, bem como o código de gravação (por vários programadores). Aderir a um módulo (classe) por arquivo de origem é uma boa ideia para começar.

Em segundo lugar, quando se trata de funções / procedimentos:

void setDataValueAndCheckForRange(Data *data) {/*code*/} 

é um bom método se verificar o intervalo de apenas "dados". É um método BAD quando o mesmo intervalo se aplica a múltiplas funções (exemplo de código incorreto):

void setDataValueAndCheckForRange(Data *data){ /*code */}
void addDataValuesAndCheckForRange(Data *result, Data *d1, Data *d2){ /*code*/}
void subDataValuesAndCheckForRange(Data *result, Data *d1, Data *d2){ /*code*/}
void mulDataValuesAndCheckForRange(Data *result, Data *d1, Data *d2){ /*code*/}

Isso precisa ser refatorado para:

bool isWithinRange(Data *d){ /*code*/ }
void setDataValue(Data *d) {/*code*/ if(isWithinRange(d)){/*continue*/}else{/*warn/abort*/} 
void addDataValue(Data *d, Data *d1, Data *d2) {/*code*/ if(isWithinRange(d)){/*continue*/}else{/*warn/abort*/} 
void subDataValue(Data *d, Data *d1, Data *d2) {/*code*/ if(isWithinRange(d)){/*continue*/}else{/*warn/abort*/} 
void mulDataValue(Data *d, Data *d1, Data *d2) {/*code*/ if(isWithinRange(d)){/*continue*/}else{/*warn/abort*/} 

REUSE o código, tanto quanto possível. E isso é possível quando cada função do seu programa é SIMPLES (não necessariamente fácil) o suficiente.

CITAÇÃO: A MONTANHA é composta de minúsculos grãos de terra. O oceano é feito de minúsculas gotas de água .. (- Sivananda)

    
por 16.10.2012 / 13:41
fonte
0

Os métodos longos tendem a ser "ruins" em linguagens imperativas que favorecem instruções, efeitos colaterais e mutabilidade, precisamente porque esses recursos aumentam a complexidade e, portanto, os bugs.

Em linguagens de programação funcionais, que favorecem expressões, pureza e imutabilidade, há menos motivos para preocupação.

Em linguagens funcionais e imperativas, é sempre melhor separar partes reutilizáveis de código em rotinas comuns de nível superior, mas em linguagens funcionais que suportam escopo léxico com funções aninhadas etc., é na verdade melhor encapsulamento para ocultar sub-rotinas dentro da função de nível superior (método) do que dividi-los em outras funções de nível superior.

    
por 16.10.2012 / 19:29
fonte