Como você justifica a criação de mais código seguindo as práticas de código limpo?


106

Nota do moderador
Esta pergunta já teve dezessete respostas postadas nela. Antes de postar uma nova resposta, leia as respostas existentes e verifique se o seu ponto de vista ainda não está coberto adequadamente.

Eu tenho seguido algumas das práticas recomendadas no livro "Código Limpo" de Robert Martin, especialmente as que se aplicam ao tipo de software com o qual trabalho e as que fazem sentido para mim (não o sigo como dogma) .

Um efeito colateral que notei, no entanto, é que o código "limpo" que escrevo é mais código do que se eu não seguisse algumas práticas. As práticas específicas que levam a isso são:

  • Encapsulando condicionais

Então, ao invés de

if(contact.email != null && contact.emails.contains('@')

Eu poderia escrever um pequeno método como este

private Boolean isEmailValid(String email){...}
  • Substituindo um comentário embutido por outro método privado, para que o nome do método se descreva em vez de ter um comentário embutido sobre ele
  • Uma classe deve ter apenas um motivo para mudar

E alguns outros. O ponto é que, o que poderia ser um método de 30 linhas, acaba sendo uma classe, por causa dos métodos minúsculos que substituem comentários e encapsulam condicionais, etc. Quando você percebe que tem tantos métodos, "faz sentido" coloque toda a funcionalidade em uma classe, quando realmente deveria ter sido um método.

Estou ciente de que qualquer prática levada ao extremo pode ser prejudicial.

A pergunta concreta que eu estou procurando uma resposta é:

Esse é um subproduto aceitável da escrita de código limpo? Em caso afirmativo, quais são alguns argumentos que posso usar para justificar o fato de que mais LOC foram escritos?

A organização não está preocupada especificamente com mais LOC, mas mais LOC pode resultar em classes muito grandes (que, novamente, podem ser substituídas por um método longo sem várias funções auxiliares de uso único para facilitar a leitura).

Quando você vê uma classe que é grande o suficiente, dá a impressão de que a classe está ocupada o suficiente e que sua responsabilidade foi concluída. Você pode, portanto, acabar criando mais classes para obter outras partes da funcionalidade. O resultado são muitas classes, todas fazendo "uma coisa" com a ajuda de muitos métodos auxiliares pequenos.

Essa é a preocupação específica ... essas classes podem ser uma classe única que ainda alcança "uma coisa", sem a ajuda de muitos métodos pequenos. Pode ser uma aula única com talvez 3 ou 4 métodos e alguns comentários.


98
Se sua organização usa apenas LOC como uma métrica para suas bases de código, justificando o código limpo, é inútil começar.
Kilian Foth 18/03

24
Se a manutenção é o seu objetivo, o LOC não é a melhor métrica a julgar - é uma delas, mas há muito mais a considerar do que simplesmente mantê-la curta.
Zibbobz 18/03

29
Não é uma resposta, mas um ponto a ser destacado: existe toda uma subcomunidade sobre escrever código com o mínimo de linhas / símbolos possível. codegolf.stackexchange.com Pode-se argumentar que a maioria das respostas não são tão legíveis quanto poderiam ser.
Antitheos 18/03

14
Aprenda as razões por trás de todas as melhores práticas, não apenas as regras. Seguir regras sem motivos é o culto à carga. Cada regra tem uma razão própria.
Gherman

9
Apenas como um aparte, e usando o seu exemplo, às vezes, enviar coisas para métodos o fará pensar: "Talvez exista uma função de biblioteca que possa fazer isso". Por exemplo, para validar um endereço de email, você pode criar um System.Net.Mail.MailAddress que o validará para você. Você pode confiar (espero) no autor dessa biblioteca para fazer o que é certo. Isso significa que sua base de código terá mais abstrações e diminuirá de tamanho.
Gregory Currie

Respostas:


130

... somos uma equipe muito pequena que oferece suporte a uma base de código relativamente grande e não documentada (que herdamos), para que alguns desenvolvedores / gerentes vejam valor ao escrever menos código para fazer as coisas, para que tenhamos menos código para manter

Esse pessoal identificou algo corretamente: eles querem que o código seja mais fácil de manter. Onde eles deram errado, porém, está assumindo que quanto menos código houver, mais fácil será manter.

Para que o código seja fácil de manter, ele precisa ser fácil de alterar. De longe, a maneira mais fácil de obter código fácil de alterar é ter um conjunto completo de testes automatizados, que falharão se a alteração for uma alteração. Os testes são código, portanto, escrever esses testes aumentará sua base de códigos. E isso é uma coisa boa.

Em segundo lugar, para descobrir o que precisa ser alterado, o código precisa ser fácil de ler e fácil de raciocinar. Código muito conciso, reduzido em tamanho apenas para manter a contagem de linhas baixa é muito improvável que seja fácil de ler. Obviamente, existe um compromisso a ser alcançado, pois um código mais longo levará mais tempo para ser lido. Mas se é mais rápido entender, vale a pena. Se não oferecer esse benefício, essa verbosidade deixará de ser um benefício. Mas se um código mais longo melhorar a legibilidade, novamente isso é uma coisa boa.


27
"De longe, a maneira mais fácil de obter código fácil de alterar é ter um conjunto completo de testes automatizados para que falhem se a sua alteração for recente." Isto simplesmente não é verdade. Os testes exigem trabalho adicional para todas as mudanças comportamentais, porque os testes também precisam ser alterados , isso ocorre por design e muitos argumentam que a mudança é mais segura, mas também necessariamente torna as alterações mais difíceis.
Jack Aidley 18/03

63
Claro, mas o tempo perdido na manutenção desses testes é reduzido quando você perde o diagnóstico e a correção de erros que os testes impedem.
MetaFight 18/03

29
@JackAidley, ter que alterar os testes junto com o código pode dar mais aparência, mas apenas se ignorarmos os bugs difíceis de encontrar que as alterações no código não testado introduzirão e que muitas vezes não serão encontradas até depois do envio . Este último apenas oferece a ilusão de menos trabalho.
David Arno

31
@ JackAidley, eu discordo completamente de você. Os testes facilitam a alteração do código. Eu admito que o código mal projetado que é muito acoplado e, portanto, acoplado aos testes pode ser difícil de mudar, mas o código bem estruturado e bem testado é simples de mudar na minha experiência.
David Arno

22
@JackAidley Você pode refatorar muito sem alterar nenhuma API ou interface. Isso significa que você pode ficar louco enquanto modifica o código sem precisar alterar uma única linha em testes de unidade ou funcionais. Ou seja, se seus testes não testarem uma implementação específica.
Eric Duminil 18/03

155

Sim, é um subproduto aceitável, e a justificativa é que agora está estruturado de forma que você não precisa ler a maior parte do código na maioria das vezes. Em vez de ler uma função de 30 linhas toda vez que estiver fazendo uma alteração, você está lendo uma função de 5 linhas para obter o fluxo geral e talvez algumas das funções auxiliares se sua alteração tocar nessa área. Se sua nova classe "extra" for chamada EmailValidatore você souber que seu problema não está na validação de email, você pode pular a leitura.

Também é mais fácil reutilizar peças menores, o que tende a reduzir a contagem de linhas em todo o programa. Um EmailValidatorpode ser usado em todo o lugar. Algumas linhas de código que validam e-mails, mas são agrupadas com o código de acesso ao banco de dados, não podem ser reutilizadas.

E considere o que precisa ser feito se as regras de validação de email precisarem ser alteradas - o que você prefere: um local conhecido; ou muitos locais, possivelmente faltando alguns?


10
resposta muito melhor do que o cansativo "teste de unidade resolve todos os seus problemas"
Dirk Boer

13
Essa resposta atinge um ponto-chave que o tio Bob e seus amigos sempre sentem falta - refatorar para métodos pequenos só ajuda se você não precisar ler todos os métodos pequenos para descobrir o que seu código está fazendo. Criar uma classe separada para validar endereços de email é prudente. Puxar o código iterations < _maxIterationspara um método chamado ShouldContinueToIterateé estúpido .
BJ Myers

4
@DavidArno: "ser útil"! = "Resolve todos os seus problemas"
Christian Hackl

2
@DavidArno: Quando alguém reclama que as pessoas implicam que o teste de unidade "resolve todos os seus problemas", elas obviamente significam pessoas que implicam que o teste de unidade resolve ou pelo menos contribui para a solução de quase todos os problemas na engenharia de software. Acho que ninguém acusa ninguém de sugerir testes de unidade como uma maneira de acabar com a guerra, a pobreza e as doenças. Outra maneira de dizer isso é que a extrema substituição de testes de unidade em muitas respostas, não apenas a esta pergunta, mas também no SE em geral, está sendo criticada (com razão).
Christian Hackl

2
Olá @DavidArno, meu comentário foi claramente uma hipérbole, não um palhaço;) Para mim, é assim: estou perguntando como consertar meu carro e pessoas religiosas aparecem e me dizem que eu deveria viver uma vida menos pecaminosa. Em teoria, algo que vale a pena discutir, mas realmente não está me ajudando a melhorar a reparação de carros.
Dirk Boer

34

É famoso o fato de Bill Gates ter dito: "Medir o progresso da programação por linhas de código é como medir o progresso da construção de aeronaves em peso".

Eu humildemente concordo com esse sentimento. Isso não quer dizer que um programa deva se esforçar por mais ou menos linhas de código, mas que isso não é o que conta para criar um programa que funcione e funcione. É bom lembrar que, em última análise, a razão por trás da adição de linhas extras de código é que é teoricamente mais legível dessa maneira.

Pode haver discordâncias sobre se uma alteração específica é mais ou menos legível, mas não acho que você esteja errado em fazer uma alteração em seu programa, porque, ao fazê-lo, está tornando-a mais legível. Por exemplo, fazer um isEmailValidpode ser considerado supérfluo e desnecessário, especialmente se for chamado exatamente uma vez pela classe que o define. No entanto, eu preferiria ver uma isEmailValidcondição em vez de uma série de condições ANDed, na qual devo determinar o que cada condição individual verifica e por que está sendo verificada.

O problema é quando você cria um isEmailValidmétodo que tem efeitos colaterais ou verifica outras coisas além do email, porque isso é pior do que simplesmente escrever tudo. É pior porque é enganoso e eu posso perder um bug por causa disso.

Embora claramente você não esteja fazendo isso neste caso, gostaria de encorajá-lo a continuar como está fazendo. Você deve sempre se perguntar se, fazendo a alteração, é mais fácil de ler e, se esse for o seu caso, faça-o!


1
O peso da aeronave é uma métrica importante, no entanto. E durante o projeto, o peso esperado é monitorado de perto. Não como um sinal de progresso, mas como uma restrição. As linhas de código de monitoramento sugerem que mais é melhor, enquanto no design de aeronaves menos peso é melhor. Então, acho que o senhor Gates poderia ter escolhido uma ilustração melhor para o seu argumento.
jos

21
@jos na equipe em que o OP está trabalhando, parece que menos LOC é considerado 'melhor'. O argumento de Bill Gates é que o LOC não está relacionado ao progresso de maneira significativa, assim como no peso da construção da aeronave não está relacionado ao progresso de maneira significativa. Uma aeronave em construção pode ter 95% de seu peso final de forma relativamente rápida, mas seria apenas uma concha vazia sem sistemas de controle, não está 95% concluída. O mesmo no software, se um programa tiver 100 mil linhas de código, isso não significa que cada 1000 linhas forneça 1% da funcionalidade.
Mr.Mindor 18/03

7
O monitoramento do progresso é uma tarefa difícil, não é? Pobres gerentes.
jos 18/03

@jos: no código também é melhor ter menos linhas para a mesma funcionalidade, se tudo o resto for igual.
RemcoGerlich 20/03

@jos Leia com atenção. Gates não diz nada sobre se o peso é uma medida importante para a própria aeronave. Ele diz que o peso é uma medida terrível para o progresso da construção de um avião. Afinal, nessa medida, assim que você jogar todo o casco no chão, estará basicamente pronto, já que isso presumivelmente equivale a 9x% do peso do avião inteiro.
Voo

23

portanto, alguns desenvolvedores / gerentes veem valor ao escrever menos código para fazer as coisas, para que tenhamos menos código para manter

É uma questão de perder de vista o objetivo real.

O que importa é diminuir as horas gastas em desenvolvimento . Isso é medido no tempo (ou esforço equivalente), não nas linhas de código.
É como dizer que os fabricantes de automóveis devem fabricar seus carros com menos parafusos, porque leva um tempo diferente de zero para colocar cada parafuso. Enquanto isso é pedanticamente correto, o valor de mercado de um carro não é definido por quantos parafusos ele faz. ou não tem. Acima de tudo, um carro precisa ter desempenho, segurança e facilidade de manutenção.

O restante da resposta são exemplos de como o código limpo pode levar a ganhos de tempo.


Exploração madeireira

Pegue um aplicativo (A) que não possui registro. Agora crie o aplicativo B, que é o mesmo aplicativo A, mas com o log. B sempre terá mais linhas de código e, portanto, você precisará escrever mais código.

Mas muito tempo será dedicado à investigação de problemas e bugs e à descoberta do que deu errado.

Para o aplicativo A, os desenvolvedores ficam presos lendo o código e tendo que reproduzir continuamente o problema e percorrer o código para encontrar a origem do problema. Isso significa que o desenvolvedor deve testar desde o início da execução até o final, em todas as camadas usadas, e precisa observar todas as peças lógicas usadas.
Talvez ele tenha sorte de encontrá-lo imediatamente, mas talvez a resposta esteja no último lugar que ele pensa em procurar.

Para o aplicativo B, assumindo um log perfeito, um desenvolvedor observa os logs, pode identificar imediatamente o componente defeituoso e agora sabe onde procurar.

Isso pode ser uma questão de minutos, horas ou dias economizados; dependendo do tamanho e da complexidade da base de código.


Regressões

Tome o aplicativo A, que não é compatível com SECA.
Tome o aplicativo B, que é SECO, mas acabou precisando de mais linhas por causa das abstrações adicionais.

Uma solicitação de mudança é arquivada, o que requer uma alteração na lógica.

Para o aplicativo B, o desenvolvedor altera a lógica (exclusiva, compartilhada) de acordo com a solicitação de mudança.

Para o aplicativo A, o desenvolvedor precisa alterar todas as instâncias dessa lógica em que se lembra de ter sido usada.

  • Se ele conseguir se lembrar de todas as instâncias, ainda precisará implementar a mesma alteração várias vezes.
  • Se ele não conseguir se lembrar de todas as instâncias, agora você estará lidando com uma base de código inconsistente que se contradiz. Se o desenvolvedor esqueceu um trecho de código raramente usado, esse bug pode não se tornar aparente para os usuários finais até o futuro. Naquele momento, os usuários finais vão identificar qual é a origem do problema? Mesmo assim, o desenvolvedor pode não se lembrar do que a mudança implicava e terá que descobrir como mudar essa parte esquecida da lógica. Talvez o desenvolvedor nem sequer trabalhe na empresa até então, e então alguém agora precise descobrir tudo do zero.

Isso pode levar a um enorme desperdício de tempo. Não apenas no desenvolvimento, mas em caçar e encontrar o bug. O aplicativo pode começar a se comportar de maneira irregular, de uma maneira que os desenvolvedores não possam entender facilmente. E isso levará a longas sessões de depuração.


Intercambiabilidade do desenvolvedor

Desenvolvedor A. Aplicativo criado A. O código não é limpo nem legível, mas funciona como um encanto e está sendo executado em produção. Sem surpresa, também não há documentação.

O desenvolvedor A está ausente por um mês devido a feriados. Uma solicitação de mudança de emergência é arquivada. Não pode esperar mais três semanas para o Dev A retornar.

O desenvolvedor B precisa executar essa alteração. Ele agora precisa ler toda a base de código, entender como tudo funciona, por que funciona e o que tenta realizar. Isso leva séculos, mas digamos que ele possa fazer isso em três semanas.

Ao mesmo tempo, o aplicativo B (que o desenvolvedor B criou) tem uma emergência. O Dev B está ocupado, mas o Dev C está disponível, mesmo que ele não conheça a base de código. O que nós fazemos?

  • Se mantemos B trabalhando em A e colocamos C em B, temos dois desenvolvedores que não sabem o que estão fazendo, e o trabalho é realizado de maneira subótima.
  • Se afastarmos B de A e fazer com que ele faça B, e agora colocarmos C em A, todo o trabalho do desenvolvedor B (ou uma parte significativa dele) pode acabar sendo descartado. Isso é potencialmente dias / semanas de esforço desperdiçado.

O desenvolvedor A volta de suas férias e vê que B não entendeu o código e, portanto, o implementou mal. Não é culpa de B, porque ele usou todos os recursos disponíveis, o código fonte simplesmente não era suficientemente legível. A agora precisa gastar tempo corrigindo a legibilidade do código?


Todos esses problemas, e muitos mais, acabam perdendo tempo . Sim, a curto prazo, o código limpo exige mais esforço agora , mas acabará pagando dividendos no futuro quando erros / alterações inevitáveis ​​precisarem ser resolvidos.

A gerência precisa entender que uma tarefa curta agora economizará várias tarefas longas no futuro. Falhar no planejamento é planejar para falhar.

Em caso afirmativo, quais são alguns argumentos que posso usar para justificar o fato de que mais LOC foram escritos?

Minha explicação a seguir é perguntar à gerência o que eles prefeririam: um aplicativo com uma base de código de 100KLOC que pode ser desenvolvida em três meses ou uma base de código de 50KLOC que pode ser desenvolvida em seis meses.

Obviamente, eles escolherão o menor tempo de desenvolvimento, porque o gerenciamento não se importa com o KLOC . Os gerentes que se concentram no KLOC são microgerenciados e informados sobre o que estão tentando gerenciar.


23

Eu acho que você deve ter muito cuidado ao aplicar práticas de "código limpo", caso elas levem a mais complexidade geral. A refatoração prematura é a raiz de muitas coisas ruins.

A extração de uma condicional para uma função leva a um código mais simples no ponto em que a condicional foi extraída , mas leva a uma complexidade mais geral, porque agora você tem uma função que é visível a partir de mais pontos no programa. Você adiciona uma carga de complexidade leve a todas as outras funções em que essa nova função agora está visível.

Não estou dizendo que você não deve extrair o condicional, apenas que você deve considerar cuidadosamente se precisar.

  • Se você deseja testar especificamente a lógica de validação de email. Então você precisa extrair essa lógica para uma função separada - provavelmente até uma classe.
  • Se a mesma lógica for usada de vários lugares no código, obviamente você precisará extraí-la para uma única função. Não se repita!
  • Se a lógica é obviamente uma responsabilidade separada, por exemplo, a validação do email ocorre no meio de um algoritmo de classificação. A validação do email será alterada independentemente do algoritmo de classificação, portanto, elas devem estar em classes separadas.

Em todas as opções acima, há uma razão para a extração além de ser apenas "código limpo". Além disso, você provavelmente nem ficaria em dúvida se era a coisa certa a fazer.

Eu diria que, em caso de dúvida, sempre escolha o código mais simples e direto.


7
Eu tenho que concordar, transformar cada condicional em um método de validação pode introduzir mais complexidade indesejada quando se trata de revisões de manutenção e código. Agora você precisa alternar o código apenas para garantir que seus métodos condicionais estejam corretos. E o que acontece quando você tem condições diferentes para o mesmo valor? Agora você pode ter um pesadelo de nomes com vários métodos pequenos que são chamados apenas uma vez e têm a mesma aparência.
pboss3010 19/03

7
Facilmente a melhor resposta aqui. Especialmente a observação (no terceiro parágrafo) de que complexidade não é simplesmente uma propriedade de todo o código como um todo, mas algo que existe, e difere, em vários níveis de abstração simultaneamente.
Christian Hackl

2
Eu acho que uma maneira de colocar isso é que, em geral, a extração de uma condição deve ser feita apenas se houver um nome significativo e não ofuscado para essa condição. Esta é uma condição necessária, mas não suficiente.
JimmyJames 19/03

Re "... porque agora você tem uma função que é visível em mais pontos do programa" : em Pascal é possível ter funções locais - "... Cada procedimento ou função pode ter suas próprias declarações de goto labels, constantes , tipos, variáveis ​​e outros procedimentos e funções, ... "
Peter Mortensen

2
@ PeterMortensen: Também é possível em C # e JavaScript. E isso é ótimo! Mas o ponto permanece: uma função, mesmo uma função local, é visível em um escopo maior que um fragmento de código embutido.
JacquesB

9

Eu apontaria que não há nada inerentemente errado com isso:

if(contact.email != null && contact.email.contains('@')

Pelo menos assumindo que é usado desta vez.

Eu poderia ter problemas com isso muito facilmente:

private Boolean isEmailValid(String email){
   return email != null && email.contains('@');
}

Algumas coisas pelas quais eu observaria:

  1. Por que é privado? Parece um esboço potencialmente útil. É útil o suficiente para ser um método privado e sem chance de ser usado mais amplamente?
  2. Eu não nomearia o método IsValidEmail pessoalmente, possivelmente ContainsAtSign ou LooksVaguelyLikeEmailAddress porque quase não faz nenhuma validação real, o que talvez seja bom, talvez não seja o que é esperado.
  3. Está sendo usado mais de uma vez?

Se estiver sendo usado uma vez, é simples de analisar e leva menos de uma linha, eu adivinharia a decisão. Provavelmente não é algo que eu chamaria se não fosse um problema específico de uma equipe.

Por outro lado, vi métodos fazerem algo assim:

if (contact.email != null && contact.email.contains('@')) { ... }
else if (contact.email != null && contact.email.contains('@') && contact.email.contains("@mydomain.com")) { //headquarters email }
else if (contact.email != null && contact.email.contains('@') && (contact.email.contains("@news.mydomain.com") || contact.email.contains("@design.mydomain.com") ) { //internal contract teams }

Esse exemplo obviamente não é SECO.

Ou mesmo apenas essa última declaração pode dar outro exemplo:

if (contact.email != null && contact.email.contains('@') && (contact.email.contains("@news.mydomain.com") || contact.email.contains("@design.mydomain.com") )

O objetivo deve ser tornar o código mais legível:

if (LooksSortaLikeAnEmail(contact.Email)) { ... }
else if (LooksLikeFromHeadquarters(contact.Email)) { ... }
else if (LooksLikeInternalEmail(contact.Email)) { ... }

Outro cenário:

Você pode ter um método como:

public void SaveContact(Contact contact){
   if (contact.email != null && contact.email.contains('@'))
   {
       contacts.Add(contact);
       contacts.Save();
   }
}

Se isso se encaixa na lógica de negócios e não é reutilizado, não há problema aqui.

Mas quando alguém pergunta "Por que o '@' é salvo, porque isso não está certo!" e você decide adicionar alguma validação real e extraí-la!

Você ficará satisfeito quando tiver também que prestar contas da segunda conta de e-mail dos presidentes Pr3 $ sid3nt @ h0m3! @ Mydomain.com e decidir se empenhar e tentar apoiar a RFC 2822.

Sobre legibilidade:

// If there is an email property and it contains an @ sign then process
if (contact.email != null && contact.email.contains('@'))

Se seu código estiver claro, você não precisa de comentários aqui. Na verdade, você não precisa de comentários para dizer o que o código está fazendo na maioria das vezes, mas sim por que está fazendo:

// The UI passes '@' by default, the DBA's made this column non-nullable but 
// marketing is currently more concerned with other fields and '@' default is OK
if (contact.email != null && contact.email.contains('@'))

Se os comentários acima de uma declaração if ou dentro de um método minúsculo são para mim, pedantes. Eu poderia até argumentar o contrário de útil com bons comentários dentro de outro método, porque agora você teria que navegar para outro método para ver como e por que ele faz o que faz.

Em resumo: não meça essas coisas; Concentre-se nos princípios dos quais o texto foi criado (SECO, SÓLIDO, BEIJO).

// A valid class that does nothing
public class Nothing 
{

}

3
Whether the comments above an if statement or inside a tiny method is to me, pedantic.Esse é um problema do "canudo que quebrou as costas do camelo". Você está certo que essa coisa não é particularmente difícil de ler completamente. Mas se você tem um método grande (por exemplo, uma grande importação) que tem dezenas de estas pequenas avaliações, tendo estes encapsulado em nomes de métodos legível ( IsUserActive, GetAverageIncome, MustBeDeleted, ...) se tornará uma melhora notável na leitura do código. O problema com o exemplo é que ele observa apenas um canudo, não o pacote inteiro que quebra as costas do camelo.
Flater 19/03

@Flater e espero que esse seja o espírito que o leitor retira disso.
AthomSfere 19/03

1
Esse "encapsulamento" é um anti-padrão, e a resposta realmente demonstra isso. Voltamos a ler o código para fins de depuração e para estender o código. Nos dois casos, é essencial entender o que o código realmente faz. O início do bloco de código if (contact.email != null && contact.email.contains('@'))é incorreto. Se o if for falso, nenhuma das outras linhas poderá ser verdadeira. Isso não é visível no LooksSortaLikeAnEmailbloco. Uma função que contém uma única linha de código não é muito melhor do que um comentário explicando como a linha funciona.
Quirk

1
Na melhor das hipóteses, outra camada de indireção obscurece a mecânica real e torna a depuração mais difícil. Na pior das hipóteses, o nome da função se tornou uma mentira da mesma forma que os comentários se tornam mentiras - o conteúdo é atualizado, mas o nome não é. Este não é um ataque contra o encapsulamento em geral, mas esse idioma em particular é sintomático do grande problema moderno da engenharia de software "corporativa" - camadas e camadas de abstração e cola que enterram a lógica relevante.
Quirk 19/03

@quirk Eu acho que você está concordando com o meu argumento geral? E com a cola, você está enfrentando um problema totalmente diferente. Na verdade, eu uso mapas de código ao olhar para um novo código de equipe. É assustador o que eu já vi fazer em alguns métodos grandes que chamam uma série de métodos grandes, mesmo no nível do padrão mvc.
AthomSfere 19/03

6

O Código Limpo é um excelente livro e vale a pena ler, mas não é a autoridade final sobre tais assuntos.

Dividir o código em funções lógicas geralmente é uma boa idéia, mas poucos programadores fazem isso da mesma forma que Martin - em algum momento, você obtém retornos decrescentes ao transformar tudo em funções e pode ser difícil de seguir quando todo o código é minúsculo peças.

Uma opção quando não vale a pena criar uma função totalmente nova é simplesmente usar uma variável intermediária:

boolean isEmailValid = (contact.email != null && contact.emails.contains('@');

if (isEmailValid) {
...

Isso ajuda a manter o código fácil de seguir sem ter que pular muito o arquivo.

Outra questão é que o Clean Code está ficando velho como um livro agora. Muita engenharia de software avançou na direção da programação funcional, enquanto Martin se esforça para adicionar estado às coisas e criar objetos. Eu suspeito que ele teria escrito um livro completamente diferente se o tivesse escrito hoje.


Alguns estão preocupados com a linha extra de código próxima à condição (não estou, de jeito nenhum), mas talvez resolvam isso na sua resposta.
Peter Mortensen

5

Considerando o fato de que a condição "é válido por email", você atualmente aceita o endereço de email inválido " @", acho que você tem todos os motivos para abstrair uma classe EmailValidator. Melhor ainda, use uma biblioteca boa e bem testada para validar endereços de email.

Linhas de código como métrica não fazem sentido. As questões importantes na engenharia de software não são:

  • Você tem muito código?
  • Você tem muito pouco código?

As questões importantes são:

  • O aplicativo como um todo foi projetado corretamente?
  • O código foi implementado corretamente?
  • O código é sustentável?
  • O código é testável?
  • O código é testado adequadamente?

Nunca pensei em LoC ao escrever código para qualquer finalidade, exceto o Code Golf. Eu me perguntei "Eu poderia escrever isso de forma mais sucinta?", Mas, para fins de legibilidade, manutenção e eficiência, não apenas o comprimento.

Claro, talvez eu possa usar uma longa cadeia de operações booleanas em vez de um método utilitário, mas devo?

Sua pergunta realmente me faz pensar em algumas longas cadeias de booleanos que escrevi e percebi que provavelmente deveria ter escrito um ou mais métodos utilitários.


3

Em um nível, eles estão certos - menos código é melhor. Outra resposta citada Gate, eu prefiro:

“Se a depuração é o processo de remoção de bugs de software, a programação deve ser o processo de colocá-los.” ​​- Edsger Dijkstra

“Ao depurar, os iniciantes inserem código corretivo; especialistas removem código defeituoso. ”- Richard Pattis

Os componentes mais baratos, mais rápidos e mais confiáveis ​​são aqueles que não estão lá. - Gordon Bell

Em resumo, quanto menos código você tiver, menos poderá dar errado. Se algo não for necessário, corte-o.
Se houver código muito complicado, simplifique-o até que os elementos funcionais reais sejam tudo o que resta.

O que é importante aqui é que todos eles se referem à funcionalidade e possuem apenas o mínimo necessário para fazê-lo. Não diz nada sobre como isso é expresso.

O que você está fazendo tentando ter um código limpo não é contra o acima. Você está adicionando ao seu LOC, mas não adicionando funcionalidades não utilizadas.

O objetivo final é ter um código legível, mas sem extras supérfluos. Os dois princípios não devem agir um contra o outro.

Uma metáfora estaria construindo um carro. A parte funcional do código é o chassi, o motor, as rodas ... o que faz o carro funcionar. Como você divide isso é mais parecido com a suspensão, a direção hidráulica e assim por diante, facilita o manuseio. Você deseja que sua mecânica seja a mais simples possível, enquanto ainda executa seu trabalho, para minimizar a chance de as coisas darem errado, mas isso não impede que você tenha bons assentos.


2

Há muita sabedoria nas respostas existentes, mas eu gostaria de acrescentar mais um fator: a linguagem .

Alguns idiomas usam mais código que outros para obter o mesmo efeito. Em particular, enquanto Java (que eu suspeito ser a linguagem em questão) é extremamente conhecido e geralmente muito sólido, claro e direto, algumas linguagens mais modernas são muito mais concisas e expressivas.

Por exemplo, em Java, seria fácil levar 50 linhas para escrever uma nova classe com três propriedades, cada uma com um getter e setter e um ou mais construtores - enquanto você pode realizar exatamente o mesmo em uma única linha do Kotlin * ou Scala. (Ainda maior poupança se você também queria adequados equals(), hashCode()e toString()métodos.)

O resultado é que, em Java, o trabalho extra significa que é mais provável que você reutilize um objeto geral que realmente não se encaixa, espremer propriedades em objetos existentes ou transmitir um monte de propriedades "vazias" individualmente; enquanto estiver em uma linguagem concisa e expressiva, é mais provável que você escreva um código melhor.

(Isso destaca a diferença entre a complexidade "superficial" do código e a complexidade das idéias / modelos / processamento que ele implementa. As linhas de código não são uma medida ruim da primeira, mas têm muito menos a ver com a segunda .)

Portanto, o "custo" de fazer as coisas certas depende do idioma. Talvez um sinal de uma boa linguagem seja aquele que não faça você escolher entre fazer bem as coisas e fazê-las simplesmente!

(* Este não é realmente o lugar para um plugue, mas vale a pena conferir Kotlin IMHO.)


1

Vamos supor que você esteja trabalhando com a classe Contactatualmente. O fato de você estar escrevendo outro método para validação do endereço de email é uma prova do fato de que a classe Contactnão está lidando com uma única responsabilidade.

Ele também está lidando com alguma responsabilidade por email, que idealmente deve ser de sua própria classe.


Outra prova de que seu código é uma fusão Contacte Emailclasse é que você não poderá testar facilmente o código de validação de email. Isso exigirá muitas manobras para alcançar o código de validação de email em um grande método com os valores corretos. Veja o método viz abaixo.

private void LargeMethod() {
    //A lot of code which modifies a lot of values. You do all sorts of tricks here.
    //Code.
    //Code..
    //Code...

    //Email validation code becoming very difficult to test as it will be difficult to ensure 
    //that you have the right data till you reach here in the method
    ValidateEmail();

    //Another whole lot of code that modifies all sorts of values.
    //Extra work to preserve the result of ValidateEmail() for your asserts later.
}

Por outro lado, se você tivesse uma classe de email separada com um método para validação de email, para testar o seu código de validação, basta fazer uma chamada simples Email.Validation()com seus dados de teste.


Conteúdo bônus: o discurso do MFeather sobre a profunda sinergia entre testabilidade e bom design.


1

Verificou-se que a redução no LOC está correlacionada com defeitos reduzidos, nada mais. Supondo que, sempre que você reduz o LOC, você reduziu a chance de defeitos essencialmente caindo na armadilha de acreditar que a correlação é igual a causação. O LOC reduzido é o resultado de boas práticas de desenvolvimento e não o que torna o código bom.

Na minha experiência, as pessoas que podem resolver um problema com menos código (no nível macro) tendem a ser mais hábeis do que aquelas que escrevem mais código para fazer a mesma coisa. O que esses desenvolvedores qualificados fazem para reduzir as linhas de código é usar / criar abstrações e soluções reutilizáveis ​​para resolver problemas comuns. Eles não gastam tempo contando linhas de código e agonizando sobre se podem cortar uma linha aqui ou ali. Muitas vezes, o código que eles escrevem é mais detalhado do que o necessário, eles apenas escrevem menos.

Deixe-me lhe dar um exemplo. Eu tive que lidar com a lógica em torno de períodos de tempo e como eles se sobrepõem, se são adjacentes e que lacunas existem entre eles. Quando comecei a trabalhar nesses problemas, eu teria blocos de código fazendo os cálculos em todos os lugares. Eventualmente, construí classes para representar os períodos e operações que calculavam sobreposições, complementos etc. Isso removeu imediatamente grandes faixas de código e as transformou em algumas chamadas de método. Mas essas aulas em si não foram escritas de maneira concisa.

Declarando claramente: se você está tentando reduzir o LOC, tentando cortar uma linha de código aqui ou ali com mais concisão, está fazendo errado. É como tentar perder peso, reduzindo a quantidade de vegetais que você come. Escreva um código fácil de entender, manter, depurar e reduzir o LOC por meio da reutilização e abstração.


1

Você identificou uma compensação válida

Portanto, existe de fato uma troca aqui e é inerente à abstração como um todo. Sempre que alguém tenta colocar N linhas de código em sua própria função para nomeá-lo e isolá-lo, simultaneamente facilita a leitura do site de chamada (referindo-se a um nome e não a todos os detalhes sangrentos subjacentes a esse nome) e mais complexo (agora você tem um significado emaranhado em duas partes diferentes da base de código). "Fácil" é o oposto de "difícil", mas não é sinônimo de "simples", que é o oposto de "complexo". Os dois não são opostos, e a abstração sempre aumenta a complexidade para inserir uma forma ou outra de facilidade.

Podemos ver a complexidade adicional diretamente quando alguma mudança nos requisitos de negócios faz com que a abstração comece a vazar. Talvez alguma nova lógica tenha sido mais natural no meio do código pré-abstraído, digamos, por exemplo, se o código abstraído atravessa alguma árvore e você realmente gostaria de coletar (e talvez agir sobre) algum tipo de informação enquanto está atravessando a árvore. Enquanto isso, se você abstraiu esse código, pode haver outros sites de chamada, e a adição da lógica necessária no meio do método pode interromper esses outros sites de chamada. Veja, sempre que alteramos uma linha de código, precisamos apenas olhar para o contexto imediato dessa linha de código; quando alteramos um método, devemos Cmd-F todo o nosso código-fonte procurando por algo que possa quebrar como resultado da alteração do contrato desse método,

O algoritmo ganancioso pode falhar nesses casos

A complexidade também tornou o código, em certo sentido, menos legível em vez de mais. Em um trabalho anterior, lidei com uma API HTTP estruturada com muito cuidado e precisão em várias camadas, cada ponto de extremidade é especificado por um controlador que valida a forma da mensagem recebida e a entrega a algum gerente da "camada de lógica de negócios" , que solicitou uma "camada de dados" responsável por fazer várias consultas a uma camada de "objeto de acesso a dados", responsável pela criação de vários delegados SQL que realmente responderiam à sua pergunta. A primeira coisa que posso dizer sobre isso foi que algo como 90% do código foi copiado e colado, ou seja, não era uma operação. Portanto, em muitos casos, a leitura de qualquer passagem de código era muito "fácil", porque "oh, esse gerente apenas encaminha a solicitação para esse objeto de acesso a dados".muita troca de contexto e localização de arquivos e tentativa de rastrear informações que você nunca deveria estar rastreando ", isso é chamado X nesta camada, ele se chama X 'nessa outra camada e depois é chamado X' 'nessa outra outra camada ".

Acho que quando parei, essa simples API CRUD estava no estágio em que, se você a imprimisse a 30 linhas por página, seriam necessários 10 a 20 livros de quinhentos páginas em uma prateleira: era uma enciclopédia inteira código. Em termos de complexidade essencial, não tenho certeza de que havia metade de um livro de complexidade essencial; tivemos apenas 5-6 diagramas de banco de dados para lidar com isso. Fazer qualquer alteração leve era uma tarefa gigantesca, aprender que era uma tarefa gigantesca, a adição de novas funcionalidades era tão dolorosa que, na verdade, tínhamos arquivos de modelo padrão que usaríamos para adicionar novas funcionalidades.

Então, eu vi em primeira mão como tornar cada parte muito legível e óbvia pode tornar o todo muito ilegível e não óbvio. Isso significa que o algoritmo ganancioso pode falhar. Você conhece o algoritmo ganancioso, sim? "Eu darei qualquer passo que melhore a situação localmente e depois confiarei em uma situação globalmente melhorada". Geralmente, é uma bela primeira tentativa, mas também pode faltar em contextos complexos. Por exemplo, na fabricação, você pode tentar aumentar a eficiência de cada etapa específica de um complexo processo de fabricação - faça lotes maiores, grite com as pessoas que parecem não estar fazendo nada para ocupar as mãos com outra coisa - e isso geralmente pode destruir a eficiência global do sistema.

Prática recomendada: use DRY e comprimentos para fazer a chamada

(Nota: o título desta seção é uma piada; muitas vezes digo aos meus amigos que quando alguém está dizendo "devemos fazer X porque as práticas recomendadas o fazem", 90% do tempo não está falando sobre algo como injeção SQL ou hash de senha ou o que quer que seja - práticas recomendadas unilaterais - e, portanto, a declaração pode ser traduzida em 90% do tempo para "deveríamos fazer X porque eu o digo ". Como se eles pudessem ter algum artigo de blog de alguma empresa que fez um trabalho melhor com X em vez de X ', mas geralmente não há garantia de que sua empresa se assemelhe a essa, e geralmente há algum outro artigo de outra empresa que fez um trabalho melhor com X' em vez de X. Portanto, não tome o título também a sério.)

O que eu recomendaria é baseado em uma palestra de Jack Diederich chamada Stop Writing Classes (youtube.com) . Ele faz vários pontos importantes nessa conversa: por exemplo, que você pode saber que uma classe é realmente apenas uma função quando ela possui apenas dois métodos públicos, e um deles é o construtor / inicializador. Mas, em um caso, ele está falando sobre como uma biblioteca hipotética que ele substituiu por string como "Muffin" declarou sua própria classe "MuffinHash", que era uma subclasse do dicttipo interno que o Python possui. A implementação estava totalmente vazia - alguém tinha acabado de pensar: "podemos precisar adicionar funcionalidades personalizadas aos dicionários Python mais tarde, vamos apresentar uma abstração agora, apenas por precaução".

E sua resposta desafiadora foi simplesmente: "sempre podemos fazer isso mais tarde, se precisarmos".

Eu acho que às vezes fingimos que seremos programadores piores no futuro do que somos agora, então podemos querer inserir algum tipo de coisinha que pode nos fazer felizes no futuro. Antecipamos as necessidades do futuro. "Se o tráfego for 100 vezes maior do que pensamos, essa abordagem não será dimensionada, por isso temos de colocar o investimento inicial nessa abordagem mais difícil que será dimensionada". Muito suspeito.

Se levarmos esse conselho a sério, precisamos identificar quando "mais tarde" chegou. Provavelmente, a coisa mais óbvia seria estabelecer um limite superior na duração das coisas por razões de estilo. E acho que o melhor conselho restante seria usar DRY - não se repita - com essas heurísticas sobre os comprimentos das linhas para corrigir um buraco nos princípios do SOLID. Com base na heurística de 30 linhas, sendo uma "página" de texto e uma analogia com a prosa,

  1. Refatore um cheque em uma função / método quando desejar copiá-lo e colá-lo. Como existem ocasionalmente razões válidas para copiar e colar, mas você deve sempre se sentir sujo com isso. Autores verdadeiros não fazem você reler uma grande frase longa 50 vezes ao longo da narrativa, a menos que estejam realmente tentando destacar um tema.
  2. Uma função / método deve idealmente ser um "parágrafo". A maioria das funções deve ter cerca de meia página ou de 1 a 15 linhas de código, e apenas 10% de suas funções devem ter permissão para atingir uma página e meia, 45 linhas ou mais. Quando você tiver mais de 120 linhas de código e comentários, essa coisa precisará ser dividida em partes.
  3. Um arquivo deve ser idealmente um "capítulo". A maioria dos arquivos deve ter 12 páginas ou menos, portanto, 360 linhas de código e comentários. Apenas talvez 10% dos seus arquivos tenham permissão para atingir 50 páginas ou 1500 linhas de código e comentários.
  4. Idealmente, a maior parte do seu código deve ser recuada com a linha de base da função ou com um nível de profundidade. Com base em algumas heurísticas sobre a árvore de origem Linux, se você é religioso, apenas 10% do seu código deve ser recuado 2 níveis ou mais dentro da linha de base, menos de 5% recuado 3 níveis ou mais. Isso significa, em particular, que coisas que precisam "encobrir" alguma outra preocupação, como o tratamento de erros em uma grande tentativa / captura, devem ser retiradas da lógica real.

Como mencionei lá em cima, testei essas estatísticas com a árvore de fontes Linux atual para encontrar essas porcentagens aproximadas, mas elas também raciocinam na analogia literária.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.