Existem métodos privados com um único estilo ruim de referência?

139

Geralmente, uso métodos privados para encapsular funcionalidades que são reutilizadas em vários locais da classe. Mas às vezes eu tenho um grande método público 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 qualquer um que leia o método a usar métodos privados diferentes prejudique a legibilidade.

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

    
por Jordak 05.06.2017 / 16:46
fonte

6 respostas

202

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

As funções privadas não precisam existir simplesmente por causa da capacidade de reutilização. Essa é certamente uma boa razão para criá-las, mas existe outra: a decomposição.

Considere uma função que faz muito. É uma centena de linhas e é impossível raciocinar.

Se você dividir essa função em partes menores, ela "fará" tanto trabalho quanto antes, mas em partes menores. Chama outras funções que devem ter nomes descritivos. A função principal lê quase como um livro: faça A, então faça B, então faça C, etc. As funções que chama só podem ser chamadas em um lugar, mas agora elas são menores. Qualquer função específica é necessariamente protegida das outras funções: elas têm escopos diferentes.

Quando você decompõe um problema grande em problemas menores, mesmo que esses problemas menores (funções) 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 ela faz completamente. Você pode continuar mentindo para si mesmo ou dividi-lo em pedaços pequenos que façam sentido.

  • Localidade de referência. Agora, torna-se impossível declarar e usar uma variável, então ela deve ser usada e usada novamente 100 linhas depois. Essas funções têm escopos diferentes.

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

  • Modularidade. Agora que você tem funções privadas, você pode encontrar uma ou mais delas que podem ser extraídas em uma classe separada, seja ela usada apenas aqui ou seja reutilizável. Para o ponto anterior, é provável que essa classe separada seja mais fácil de testar, já que precisará de uma interface pública.

A ideia de dividir o código grande em partes menores que são mais fáceis de entender e testar é um ponto fundamental do livro do Uncle Bob Clean Código . No momento de escrever esta resposta, o livro tem nove anos, mas é tão relevante hoje como era naquela época.

    
por 05.06.2017 / 17:04
fonte
36

É provavelmente uma ótima ideia!

Eu me preocupo com dividindo sequências lineares longas de ação em funções separadas puramente para reduzir o comprimento médio da função em 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 fonte e reduziu a legibilidade total consideravelmente. Especialmente se você está 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 se chamado apenas uma vez ), você 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 funcionalidade pura podem ser usadas se você pensar nisso por tempo suficiente.

Você saberá que está no caminho certo quando tem funções com bons nomes de verbos e quando sua função de pai as chama e a coisa toda praticamente se lê como um parágrafo de prosa.

Então, quando você retorna semanas depois para adicionar mais funcionalidades e descobre que você pode realmente reutilizar uma dessas funções, então oh, alegria arrebatadora! Que incrível deleite radiante!

    
por 05.06.2017 / 18:41
fonte
19

A resposta é que depende muito da situação. O @Snowman aborda os aspectos positivos da divisão de grandes funções públicas, mas é importante lembrar que pode haver efeitos negativos também, como você está corretamente preocupado.

  • Muitas funções privadas com efeitos colaterais podem tornar o código difícil de ler e bastante frágil. Isso é especialmente verdadeiro quando essas funções privadas dependem dos efeitos colaterais umas das outras. Evite ter funções strongmente 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 isso acontece, e é importante identificá-los.

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

  • O código altamente 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() deva ser chamado antes de foo() no momento da escrita.

  • Reutilização perigosa. Sua função pública provavelmente restringia a entrada que era possível para cada função privada. Se essas suposições de entrada não forem apropriadamente capturadas (documentar TODAS as suposições é difícil), alguém pode reutilizar indevidamente uma de suas funções privadas, introduzindo bugs na base de código.

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

    
por 05.06.2017 / 19:05
fonte
2

É um desafio de equilíbrio.

O Finer é melhor

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

Dar nomes a construções de código geralmente é bom, contanto 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 forçar-se a pensar em contratos significativos para partes menores do código, o desenvolvedor inicial pode identificar alguns bugs e evitá-los sem problemas, mesmo antes de qualquer teste de desenvolvedor. Isso só funciona se o desenvolvedor está se esforçando para nomear conciso (simples, mas preciso) e está disposto a adaptar os limites do método privado para que a nomenclatura concisa seja possível.

A manutenção subsequente também é ajudada, porque a nomenclatura adicional ajuda a tornar o código autodocumentado .

  • Contratos sobre pequenos pedaços de código
  • Métodos de nível mais alto às vezes se transformam em uma pequena sequência de chamadas, cada uma das quais faz algo significativo e distintamente nomeado - acompanhada da fina camada de tratamento geral de erros mais externo. Esse método pode se tornar um recurso de treinamento valioso para qualquer pessoa que precise se tornar rapidamente um especialista na estrutura geral do módulo.

Até ficar muito bom

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

  • Muitas sobrecargas que na verdade 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 de nomenclatura superficiais, como XxxInternal ou DoXxx , especialmente se não houver um esquema unificado na introdução dessas.
  • Nomes desajeitados quase mais longos que a própria implementação, como LogDiskSpaceConsumptionUnlessNoUpdateNeeded
por 08.06.2017 / 09:26
fonte
2

IMHO, o valor de retirar blocos de código apenas como um meio de romper a complexidade, estaria freqüentemente relacionado à diferença de complexidade entre:

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

  2. O código em si.

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 pode facilitar a compreensão do código ao redor. Por outro lado, alguns conceitos podem ser expressos de forma mais legível em uma linguagem de computador do que em linguagem humana. Eu consideraria w=x+y+z; , por exemplo, mais legível que w=addThreeNumbersAssumingSumOfFirstTwoDoesntOverflow(x,y,z); .

Quando uma função grande é dividida, haverá menos e menos diferença entre a complexidade das subfunções e suas descrições, e a vantagem de outras subdivisões diminuirá. Se as coisas estiverem divididas ao ponto de as descrições serem mais complicadas do que o código, mais divisões tornarão o código pior.

    
por 05.06.2017 / 18:45
fonte
1
Ao contrário do que os outros disseram, eu diria que um método público longo é um cheiro de design que não é retificado pela decomposição em métodos privados.

>

But sometimes I have a large public method that could be broken up into smaller steps

Se este for o caso, então eu diria que cada passo deve ser seu próprio cidadão de primeira classe com uma única responsabilidade cada. Em um paradigma orientado a objetos, sugiro criar 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 que você teste de unidade o (antigo) método público grande, bem como cada etapa individual independentemente um do outro. Todos devem ser documentados também.

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

  • Acoplamento e testabilidade apertados. Ao reduzir o tamanho do seu método público, você melhorou sua legibilidade, mas todo o código ainda está bem unido. Você pode testar a unidade dos métodos privados individuais (usando recursos avançados de uma estrutura de teste), mas não é possível testar facilmente o método público independentemente dos métodos privados. Isso vai contra os princípios do teste unitário.
  • Tamanho e complexidade da turma. 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 turma está fazendo muito.
  • Não pode ser reutilizado com facilidade. Muitas vezes é o caso que, como um corpo de código amadurece reusabilidade é útil. Se seus passos estiverem em métodos privados, eles não poderão ser reutilizados em nenhum outro lugar sem primeiro extraí-los de alguma forma. Além disso, pode encorajar copiar e colar quando uma etapa for necessária em outro lugar.
  • A divisão dessa maneira provavelmente será arbitrária. Eu diria que dividir um método público longo não requer tanto pensamento ou consideração quanto se você fosse dividir 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.
  • Oculta 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 privados! Pelo contrário, acho que isso é um grande problema. Ele estabelece um padrão para adicionar complexidade a uma classe que será seguida por correções de erros e implementações de recursos subseqüentes. Em breve, seus métodos privados crescerão e eles terão que ser divididos.

but I'm worried that forcing anyone who reads the method to jump around to different private methods will damage readability

Este é um argumento que tive com um dos meus colegas recentemente. 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 seguir quando todos juntos, mas o código é menos fácil de raciocinar sobre conforme a complexidade aumenta. À medida que um sistema cresce, torna-se intratável raciocinar sobre o módulo inteiro como um todo. Quando você decompõe a lógica complexa em várias classes, cada uma com uma única responsabilidade, fica mais fácil raciocinar sobre cada parte.

    
por 07.06.2017 / 05:40
fonte