Os métodos privados com uma única referência são ruins?

139

Geralmente, uso métodos privados para encapsular a funcionalidade que é reutilizada em vários locais da classe. Mas, às vezes, tenho um método público grande que pode ser dividido em etapas menores, cada uma em seu próprio método privado. Isso tornaria o método público mais curto, mas estou preocupado que forçar quem lê o método a pular para diferentes métodos privados prejudicará a legibilidade.

Existe um consenso sobre isso? É melhor ter métodos públicos longos ou dividi-los em pedaços menores, mesmo que cada peça não seja reutilizável?

Jordak
fonte
7
Todas as respostas são baseadas em opiniões. Os autores originais sempre pensam que seu código ravioli é mais legível; editores subsequentes chamam de ravioli.
precisa
5
Eu sugiro que você leia o código limpo. Ele recomenda esse estilo e tem muitos exemplos. Ele também tem uma solução para o problema de "pular".
Kat
1
Acho que isso depende da sua equipe e de quais linhas de código estão contidas.
que você
1
A estrutura inteira do STRUTS não se baseia nos métodos Getter / Setter de uso único, todos métodos de uso único?
Zibbobz

Respostas:

203

Não, este não é um estilo ruim. Na verdade, é um estilo muito bom.

Funções privadas não precisam existir simplesmente por causa da reutilização. Esse é certamente um bom motivo para criá-los, mas existe outro: decomposição.

Considere uma função que faz muito. Tem cem linhas e é impossível argumentar.

Se você dividir essa função em partes menores, ela ainda "funcionará" tanto quanto antes, mas em partes menores. Ele chama outras funções que devem ter nomes descritivos. A função principal é quase como um livro: faça A, depois faça B, depois faça C, etc. As funções que ele chama podem ser chamadas apenas em um local, mas agora são menores. Qualquer função específica é necessariamente protegida por sandbox das outras funções: elas têm escopos diferentes.

Quando você decompõe um problema grande em problemas menores, mesmo que esses problemas (funções) menores sejam usados ​​/ resolvidos apenas uma vez, você obtém vários benefícios:

  • Legibilidade. Ninguém pode ler uma função monolítica e entender o que faz completamente. Você pode continuar mentindo para si mesmo ou dividi-lo em pedaços pequenos que fazem sentido.

  • Localidade de referência. Agora, torna-se impossível declarar e usar uma variável, depois manter-se por aí e usar novamente 100 linhas depois. Essas funções têm escopos diferentes.

  • Testando. Embora seja necessário testar unicamente os membros públicos de uma classe, pode ser desejável testar também certos membros particulares. Se houver uma seção crítica de uma função longa que possa se beneficiar do teste, é impossível testá-la independentemente, sem extraí-la para uma função separada.

  • Modularidade. Agora que você possui funções privadas, pode encontrar um ou mais deles que possam ser extraídos em uma classe separada, seja usada apenas aqui ou reutilizável. Até o ponto anterior, é provável que essa classe separada também seja mais fácil de testar, pois precisará de uma interface pública.

A idéia de dividir o código grande em pedaços menores, mais fáceis de entender e testar, é um ponto-chave do livro Clean Code, do tio Bob . No momento em que escrevi essa resposta, o livro tinha nove anos, mas hoje é tão relevante quanto era naquela época.


fonte
7
Não se esqueça de manter níveis consistentes de abstração e bons nomes que garantam uma espiada dentro dos métodos que não o surpreenderão. Não se surpreenda se um objeto inteiro estiver escondido lá.
candied_orange
14
Às vezes, os métodos de divisão podem ser bons, mas manter as coisas alinhadas também pode trazer vantagens. Se uma verificação de limite puder ser feita de maneira sensata dentro ou fora de um bloco de código, por exemplo, manter esse bloco de código em linha facilitará a verificação se a verificação de limite está sendo feita uma vez, mais de uma vez ou não . Colocar o bloco de código em sua própria sub-rotina tornaria essas coisas mais difíceis de determinar.
Supercat
7
O marcador "legibilidade" pode ser rotulado como "Abstração". É sobre abstração. Os "pedaços pequenos" fazem uma coisa , uma coisa de baixo nível com a qual você realmente não se importa com os detalhes detalhados quando está lendo ou percorrendo o método público. Ao abstraí-lo para uma função, você passa por cima dela e mantém o foco no grande esquema das coisas / no que o método público está fazendo em um nível superior.
Mathieu Guindon
7
A bala "Teste" é IMO questionável. Se seus membros particulares quiserem ser testados, provavelmente pertencem à sua própria classe, como membros públicos. Concedido, o primeiro passo para extrair uma classe / interface é extrair um método.
Mathieu Guindon
6
Dividir uma função exclusivamente por números de linha, blocos de código e escopo variável sem se preocupar com a funcionalidade real é uma péssima idéia, e eu diria que a melhor alternativa é deixá-la em uma única função. Você deve dividir as funções de acordo com a funcionalidade. Veja a resposta de DaveGauer . Você está sugerindo isso na sua resposta (com menções de nomes e problemas), mas não posso deixar de sentir que esse aspecto precisa de muito mais foco, dada a sua importância e relevância em relação à pergunta.
Dukeling
36

Provavelmente é uma ótima ideia!

Eu discordo de dividir longas seqüências lineares de ação em funções separadas, exclusivamente para reduzir o comprimento médio da função na sua base de código:

function step1(){
  // ...
  step2(zarb, foo, biz);
}

function step2(zarb, foo, biz){
  // ...
  step3(zarb, foo, biz, gleep);
}

function step3(zarb, foo, biz, gleep){
  // ...
}

Agora você adicionou linhas de origem e reduziu consideravelmente a legibilidade total. Especialmente se você estiver passando muitos parâmetros entre cada função para acompanhar o estado. Caramba!

No entanto , se você conseguiu extrair uma ou mais linhas em uma função pura que serve a um propósito único e claro ( mesmo que seja chamado apenas uma vez ), melhorou a legibilidade:

function foo(){
  f = getFrambulation();
  g = deglorbFramb(f);
  r = reglorbulate(g);
}

Isso provavelmente não será fácil em situações do mundo real, mas peças de pura funcionalidade muitas vezes podem ser destacadas se você pensar nisso por tempo suficiente.

Você saberá que está no caminho certo quando tiver funções com bons nomes de verbos e quando a função dos pais as chamar e tudo parecer praticamente como um parágrafo da prosa.

Então, quando você voltar semanas mais tarde para adicionar mais funcionalidades e descobrir que pode realmente reutilizar uma dessas funções, então , alegria arrebatadora! Que maravilha radiante e maravilhosa!

DaveGauer
fonte
2
Eu ouvi esse argumento por muitos anos, e ele nunca se desenrola no mundo real. Estou falando especificamente de uma ou duas funções de linha, chamadas de apenas um lugar. Enquanto o autor original sempre pensa que é "mais legível", os editores subsequentes costumam chamá-lo de código ravioli. Isso depende da profundidade da chamada, de um modo geral.
Frank Hileman
34
@FrankHileman Para ser justo, nunca vi nenhum desenvolvedor elogiar o código de seu antecessor.
T. Sar
4
@TSar o desenvolvedor anterior é a fonte de todos os problemas!
precisa
18
@TSar Eu nunca elogiei o desenvolvedor anterior quando era desenvolvedor anterior.
precisa
3
A coisa agradável sobre decomposição é que você pode, então, compor: foo = compose(reglorbulate, deglorbFramb, getFrambulation);;)
Warbo
19

A resposta é que depende muito da situação. O @Snowman cobre os aspectos positivos de interromper uma grande função pública, mas é importante lembrar que também pode haver efeitos negativos, como você está certo.

  • Muitas funções privadas com efeitos colaterais podem tornar o código difícil de ler e bastante frágil. Isto é especialmente verdade quando essas funções privadas dependem dos efeitos colaterais uma da outra. Evite ter funções fortemente acopladas.

  • Abstrações estão vazando . Embora seja bom fingir como uma operação é executada ou como os dados são armazenados não importa, há casos em que ocorre e é importante identificá-los.

  • Semântica e matéria de contexto. Se você não conseguir capturar claramente o que uma função faz em seu nome, novamente poderá reduzir a legibilidade e aumentar a fragilidade da função pública. Isto é especialmente verdade quando você começa a criar funções privadas com um grande número de parâmetros de entrada e saída. E enquanto a partir da função pública, você vê a que funções privadas ela chama, a partir da função privada, não vê como as funções públicas a chamam. Isso pode levar a uma "correção de bug" em uma função privada que interrompe a função pública.

  • Código fortemente decomposto é sempre mais claro para o autor do que para outros. Isso não significa que ainda não esteja claro para os outros, mas é fácil dizer que faz todo o sentido que bar()deve ser chamado antes foo()no momento da redação.

  • Reutilização perigosa. Sua função pública provavelmente restringiu as entradas possíveis para cada função privada. Se essas suposições de entrada não forem capturadas adequadamente (documentar TODAS as suposições é difícil), alguém poderá reutilizar indevidamente uma de suas funções privadas, introduzindo erros na base de código.

Decomponha funções com base em sua coesão e acoplamento internos, e não em comprimento absoluto.

dlasalle
fonte
6
Para ignorar a legibilidade, vejamos o aspecto do relatório de erros: se o usuário relatar que ocorreu um erro MainMethod(fácil de obter, geralmente são incluídos símbolos e você deve ter um arquivo de desreferência de símbolos), que é de 2k linhas (os números de linhas nunca são incluídos em relatórios de bugs) e é um NRE que pode acontecer em 1k dessas linhas, bem, eu serei amaldiçoado se eu estiver investigando isso. Mas se eles me disserem que aconteceu SomeSubMethodAe são 8 linhas e a exceção foi uma NRE, provavelmente vou encontrar e corrigir isso com bastante facilidade.
410_Gone
2

O IMHO, o valor de retirar blocos de código puramente como um meio de quebrar a complexidade, estaria frequentemente relacionado à diferença de complexidade entre:

  1. Uma descrição completa e precisa em linguagem humana do que o código faz, incluindo como ele lida com casos de canto e

  2. O próprio código.

Se o código for muito mais complicado que a descrição, a substituição do código em linha por uma chamada de função poderá facilitar a compreensão do código ao redor. Por outro lado, alguns conceitos podem ser expressos mais facilmente em uma linguagem de computador do que na linguagem humana. Eu consideraria w=x+y+z;, por exemplo, mais legível que w=addThreeNumbersAssumingSumOfFirstTwoDoesntOverflow(x,y,z);.

À medida que uma função grande é dividida, haverá cada vez menos diferença entre a complexidade das sub-funções e suas descrições, e a vantagem de outras subdivisões diminuirá. Se as coisas forem divididas a ponto de as descrições serem mais complicadas que o código, novas divisões tornarão o código pior.

supercat
fonte
2
O nome da sua função é enganoso. Não há nada no w = x + y + z que indique qualquer tipo de controle de estouro, enquanto o nome do método por si só parece implicar o contrário. Um nome melhor para sua função seria "addAll (x, y, z)", por exemplo, ou mesmo apenas "Sum (x, y, z)".
T. Sar
6
Como estamos falando de métodos privados , essa questão não está se referindo a C. De qualquer forma, esse não é o meu ponto - você poderia chamar a mesma função de "soma" sem a necessidade de abordar a suposição sobre a assinatura do método. Pela sua lógica, qualquer método recursivo deve ser chamado "DoThisAssumingStackDoesntOverflow", por exemplo. O mesmo vale para "FindSubstringAssumingStartingPointIsInsideBounds".
T. Sar
1
Em outras palavras, suposições básicas, quaisquer que sejam (dois números não transbordam, pilha não transborda, índice foi passado corretamente) não devem estar presentes no nome do método. Essas coisas devem ser documentadas, é claro, se necessário, mas não na assinatura do método.
T. Sar
1
@TSar: Eu estava sendo um pouco ridículo com o nome, mas o ponto principal é que (mudando para a média porque é um exemplo melhor), um programador que vê "(x + y + z + 1) / 3" no contexto será muito melhor posicionado para saber se essa é uma maneira apropriada de calcular a média, conforme o código de chamada, do que alguém que esteja olhando para a definição ou para o site de chamada int average3(int n1, int n2, int n3). Dividir a função "média" significaria que alguém que quisesse ver se era adequado aos requisitos teria que procurar em dois lugares.
precisa
1
Se usarmos o C #, por exemplo, você teria apenas um único método "Médio", que pode ser feito para aceitar qualquer número de parâmetros. De fato, em c #, ele funciona sobre qualquer coleção de números - e tenho certeza de que é muito mais fácil entender do que abordar a matemática bruta no código.
T. Sar
2

É um desafio de equilíbrio.

Melhor é melhor

O método privado efetivamente fornece um nome para o código contido e, às vezes, uma assinatura significativa (a menos que metade de seus parâmetros sejam dados de rastreamento completamente ad-hoc com interdependências pouco claras e não documentadas).

Dar nomes às construções de código é geralmente bom, desde que os nomes sugiram um contrato significativo para o chamador, e o contrato do método privado corresponda exatamente ao que o nome sugere.

Ao se forçar a pensar em contratos significativos para partes menores do código, o desenvolvedor inicial pode detectar alguns erros e evitá-los sem esforço, mesmo antes de qualquer teste de desenvolvimento. Isso só funciona se o desenvolvedor estiver se esforçando para nomear concisa (som simples, mas preciso) e estiver disposto a adaptar os limites do método privado para que a nomeação concisa seja possível.

A manutenção subsequente também é ajudada, porque nomes adicionais ajudam a tornar o código auto-documentado .

  • Contratos sobre pequenos pedaços de código
  • Os métodos de nível superior às vezes se transformam em uma curta sequência de chamadas, cada uma das quais faz algo significativo e com nome distinto - acompanhada da fina camada de manipulação geral e externa de erros. Esse método pode se tornar um recurso de treinamento valioso para quem precisa se tornar rapidamente um especialista na estrutura geral do módulo.

Até ficar muito bom

É possível exagerar na atribuição de nomes a pequenos pedaços de código e acabar com muitos métodos privados pequenos demais? Certo. Um ou mais dos seguintes sintomas indicam que os métodos estão ficando muito pequenos para serem úteis:

  • Muitas sobrecargas que realmente não representam assinaturas alternativas para a mesma lógica essencial, mas sim uma única pilha de chamadas fixas.
  • Sinônimos usados ​​apenas para se referir ao mesmo conceito repetidamente ("nomeação divertida")
  • Muitas decorações nominais superficiais, como XxxInternalou DoXxx, especialmente se não houver um esquema unificado para introduzi-las.
  • Nomes desajeitados quase mais que a própria implementação, como LogDiskSpaceConsumptionUnlessNoUpdateNeeded
Jirka Hanika
fonte
1

Ao contrário do que os outros disseram, eu argumentaria que um método público longo é um cheiro de design que não é retificado pela decomposição em métodos privados.

Mas às vezes eu tenho um método público grande que pode ser dividido em etapas menores

Se esse for o caso, eu argumentaria que cada passo deve ser seu próprio cidadão de primeira classe, com uma única responsabilidade cada. Em um paradigma orientado a objetos, eu sugeriria a criação de uma interface e uma implementação para cada etapa, de modo que cada uma tenha uma responsabilidade única e facilmente identificável e possa ser nomeada de uma maneira que a responsabilidade seja clara. Isso permite testar o método público grande (anteriormente) grande, bem como cada etapa individual, independentemente uma da outra. Todos devem ser documentados também.

Por que não se decompor em métodos particulares? Aqui estão algumas razões:

  • Acoplamento apertado e testabilidade. Ao reduzir o tamanho do seu método público, você melhorou sua legibilidade, mas todo o código ainda está fortemente associado. Você pode testar os métodos particulares individuais (usando recursos avançados de uma estrutura de teste), mas não pode testar facilmente o método público independentemente dos métodos privados. Isso vai contra os princípios do teste de unidade.
  • Tamanho e complexidade da classe. Você reduziu a complexidade de um método, mas aumentou a complexidade da classe. O método público é mais fácil de ler, mas a classe agora é mais difícil de ler porque possui mais funções que definem seu comportamento. Minha preferência é por pequenas classes de responsabilidade única; portanto, um método longo é um sinal de que a classe está fazendo muito.
  • Não pode ser reutilizado facilmente. Geralmente, como um corpo de código amadurece, a reutilização é útil. Se suas etapas estiverem em métodos particulares, elas não poderão ser reutilizadas em nenhum outro lugar sem primeiro extraí-las de alguma forma. Além disso, pode incentivar copiar e colar quando uma etapa é necessária em outro lugar.
  • A divisão dessa maneira provavelmente será arbitrária. Eu argumentaria que a divisão de um método público longo não leva em consideração tanto o pensamento ou o design como se você dividisse as responsabilidades em classes. Cada classe deve ser justificada com um nome, documentação e testes adequados, enquanto um método privado não recebe tanta consideração.
  • Esconde o problema. Então você decidiu dividir seu método público em pequenos métodos privados. Agora não há problema! Você pode continuar adicionando mais e mais etapas adicionando mais e mais métodos particulares! Pelo contrário, acho que este é um grande problema. Ele estabelece um padrão para adicionar complexidade a uma classe que será seguida pelas correções de erros subsequentes e implementações de recursos. Logo seus métodos privados vai crescer e eles terão de ser dividida.

mas estou preocupado que forçar quem lê o método a pular para diferentes métodos privados prejudicará a legibilidade

Este é um argumento que tive recentemente com um de meus colegas. Ele argumenta que ter todo o comportamento de um módulo no mesmo arquivo / método melhora a legibilidade. Concordo que o código é mais fácil de seguir quando todos juntos, mas o código é menos fácil de raciocinar à medida que a complexidade aumenta. À medida que o sistema cresce, torna-se intratável pensar sobre todo o módulo como um todo. Quando você decompõe a lógica complexa em várias classes, cada uma com uma única responsabilidade, fica muito mais fácil argumentar sobre cada parte.

Samuel
fonte
1
Eu teria que discordar. Abstração excessiva ou prematura leva a códigos desnecessariamente complexos. Um método privado pode ser o primeiro passo em um caminho que pode precisar ser interconectado e abstraído, mas sua abordagem aqui sugere vaguear por lugares que talvez nunca precisem ser visitados.
WillC
1
Entendo de onde você é, mas acho que o código cheira como o que o OP está perguntando são sinais de que está pronto para um design melhor. Abstração prematura seria projetar essas interfaces antes mesmo de você se deparar com esse problema.
Samuel
Eu concordo com esta abordagem. De fato, quando você segue o TDD, é a progressão natural. A outra pessoa diz que é muita abstração prematura, mas acho muito mais fácil zombar rapidamente de uma funcionalidade necessária em uma classe separada (atrás de uma interface), do que ter que implementá-la de fato em um método privado para que o teste seja aprovado. .
Eternal21