Provavelmente o suficiente para recomendar o Clean Code

Talvez nunca possamos chegar a uma definição empírica de "bom código" ou "código limpo". Isso significa que a opinião de uma pessoa sobre a opinião de outra pessoa sobre "código limpo" é necessariamente muito subjetiva. Não consigo ver o livro de Robert Martin , Clean Code, de 2008, do ponto de vista de outra pessoa, apenas do meu.



No entanto, para mim, o principal problema deste livro é que muitos dos exemplos de código nele são simplesmente terríveis .



No terceiro capítulo, "Funções", Martin dá várias dicas para escrever bons recursos. Provavelmente, a dica mais forte deste capítulo é que as funções não devem misturar níveis de abstração; eles não devem estar executando tarefas de alto ou baixo nível, porque isso confunde e confunde a responsabilidade da função. Há outras coisas importantes neste capítulo: Martin diz que os nomes das funções devem ser descritivos e consistentes, devem ser frases verbais e devem ser cuidadosamente selecionados. Ele diz que as funções devem fazer apenas uma coisa e fazê-lo bem. Ele diz que as funções não devem ter efeitos colaterais (e ele fornece um ótimo exemplo) e que os argumentos de saída devem ser evitados em favor dos valores de retorno. Ele diz que as funções geralmente devem ser comandos que fazem algo,ou pedidos que são para algoresposta , mas não as duas ao mesmo tempo. Ele explica para DRY . Todos esses são bons conselhos, embora um pouco superficiais e básicos.



Mas há declarações mais duvidosas neste capítulo. Martin diz que argumentos de bandeira booleana são uma prática ruim, com a qual concordo porque os códigos não adornados trueou falseno código-fonte são opacos e obscuros em comparação com os explícitos, IS_SUITEou IS_NOT_SUITE... mas o raciocínio de Martin é mais como argumento booleano significa que a função faz mais, do que uma coisa que ela não deveria fazer.



Martin diz que deve ser possível ler um único arquivo de origem de cima para baixo como uma narrativa, com o nível de abstração em cada função diminuindo à medida que lê e cada função se referindo às outras mais abaixo. Isso está longe de ser universal. Muitos arquivos de origem, eu diria mesmo que a maioria dos arquivos de origem, não podem ser hierarquicamente organizados dessa maneira. E mesmo para aqueles que são possíveis, o IDE nos permite pular trivialmente de uma chamada de função para uma implementação de função e vice-versa, assim como navegamos em sites. Além disso, ainda estamos lendo o código de cima para baixo? Bem, talvez alguns de nós façam isso.



E então fica estranho. Martin diz que as funções não precisam ser grandes o suficiente para conter estruturas aninhadas (convenções e loops); eles não devem ser recuados em mais de dois níveis. Ele diz que os blocos devem ter uma linha, provavelmente consistindo em uma chamada de função. Ele diz que uma função ideal não possui argumentos (mas ainda não possui efeitos colaterais?), E que uma função com três argumentos é confusa e difícil de testar. O mais estranho é que Martin afirma que a função ideal são duas ou quatro linhas de código . Este conselho é realmente colocado no início do capítulo. Esta é a primeira e mais importante regra:



: . : . . , , . , . 3000 . 100 300 . 20 30 . ( ), .



[...]



Quando Kent me mostrou o código, percebi como todos os recursos eram compactos. Muitas das minhas funções nos programas Swing foram esticadas verticalmente por quase quilômetros. No entanto, cada função no programa Kent ocupava apenas duas, três ou quatro linhas. Todos os recursos eram bastante óbvios. Cada função contava sua própria história, e cada história naturalmente o levava ao início da próxima história. É assim que as funções devem ser curtas!


Esta dica inteira termina com uma lista de códigos-fonte no final do capítulo 3. Este exemplo de código é a refatoração preferida de Martin da classe Java que vem da ferramenta de teste de código aberto FitNesse.



package fitnesse.html;

import fitnesse.responders.run.SuiteResponder;
import fitnesse.wiki.*;

public class SetupTeardownIncluder {
  private PageData pageData;
  private boolean isSuite;
  private WikiPage testPage;
  private StringBuffer newPageContent;
  private PageCrawler pageCrawler;


  public static String render(PageData pageData) throws Exception {
    return render(pageData, false);
  }

  public static String render(PageData pageData, boolean isSuite)
    throws Exception {
    return new SetupTeardownIncluder(pageData).render(isSuite);
  }

  private SetupTeardownIncluder(PageData pageData) {
    this.pageData = pageData;
    testPage = pageData.getWikiPage();
    pageCrawler = testPage.getPageCrawler();
    newPageContent = new StringBuffer();
  }

  private String render(boolean isSuite) throws Exception {
     this.isSuite = isSuite;
    if (isTestPage())
      includeSetupAndTeardownPages();
    return pageData.getHtml();
  }

  private boolean isTestPage() throws Exception {
    return pageData.hasAttribute("Test");
  }

  private void includeSetupAndTeardownPages() throws Exception {
    includeSetupPages();
    includePageContent();
    includeTeardownPages();
    updatePageContent();
  }


  private void includeSetupPages() throws Exception {
    if (isSuite)
      includeSuiteSetupPage();
    includeSetupPage();
  }

  private void includeSuiteSetupPage() throws Exception {
    include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
  }

  private void includeSetupPage() throws Exception {
    include("SetUp", "-setup");
  }

  private void includePageContent() throws Exception {
    newPageContent.append(pageData.getContent());
  }

  private void includeTeardownPages() throws Exception {
    includeTeardownPage();
    if (isSuite)
      includeSuiteTeardownPage();
  }

  private void includeTeardownPage() throws Exception {
    include("TearDown", "-teardown");
  }

  private void includeSuiteTeardownPage() throws Exception {
    include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
  }

  private void updatePageContent() throws Exception {
    pageData.setContent(newPageContent.toString());
  }

  private void include(String pageName, String arg) throws Exception {
    WikiPage inheritedPage = findInheritedPage(pageName);
    if (inheritedPage != null) {
      String pagePathName = getPathNameForPage(inheritedPage);
      buildIncludeDirective(pagePathName, arg);
    }
  }

  private WikiPage findInheritedPage(String pageName) throws Exception {
    return PageCrawlerImpl.getInheritedPage(pageName, testPage);
  }

  private String getPathNameForPage(WikiPage page) throws Exception {
    WikiPagePath pagePath = pageCrawler.getFullPath(page);
    return PathParser.render(pagePath);
  }

  private void buildIncludeDirective(String pagePathName, String arg) {
    newPageContent
      .append("\n!include ")
      .append(arg)
      .append(" .")
      .append(pagePathName)
      .append("\n");
  }
}


Novamente, esse é o próprio código de Martin, escrito de acordo com seus padrões pessoais. Esse é o ideal que nos é apresentado como exemplo de treinamento.



Neste ponto, confesso que minhas habilidades em Java estão desatualizadas e enferrujadas, quase tão desatualizadas e enferrujadas quanto este livro, lançado em 2008. Mas mesmo em 2008, esse código era lixo ilegível?



Vamos ignorar importos curingas.



Temos dois métodos estáticos públicos, um construtor privado e quinze métodos privados. Dos quinze métodos privados, até treze têm efeitos colaterais (eles alteram variáveis ​​que não foram passadas para eles como argumentos, por exemplo buildIncludeDirective, que têm efeitos colaterais emnewPageContent) ou chame outros métodos que tenham efeitos colaterais (por exemplo include, quais chamadas buildIncludeDirective). Apenas isTestPagee findInheritedPageparece sem os efeitos colaterais. Eles ainda usam variáveis ​​que não são passadas para eles ( pageDatae, portanto testPage), mas parecem fazê-lo sem efeitos colaterais.



Nesse ponto, você pode concluir que talvez a definição de Martin de "efeito colateral" não inclua as variáveis-membro do objeto cujo método acabamos de chamar. Se aceitarmos esta definição, em seguida, cinco dessas variáveis, pageData, isSuite, testPage, newPageContentepageCrawlersão passados ​​implicitamente para cada chamada de método privada, e isso é considerado normal; qualquer método privado é livre para fazer o que quiser com qualquer uma dessas variáveis.



Mas esta é uma suposição errada! Aqui está a própria definição de Martin de uma parte anterior deste capítulo:



Os efeitos colaterais são uma mentira. Sua função promete fazer uma coisa, mas faz algo diferente, oculta ao usuário . Às vezes, faz alterações inesperadas nas variáveis ​​de sua classe - por exemplo, atribui a elas os valores dos parâmetros passados ​​para uma função ou variáveis ​​globais do sistema. De qualquer forma, essa função é uma mentira insidiosa e maliciosa, que geralmente leva a tempo não natural e outras dependências.


Eu amo essa definição! Eu concordo com esta definição! Esta é uma definição muito útil! Concordo que é ruim para uma função fazer alterações inesperadas nas variáveis ​​de sua própria classe.



Então, por que o próprio código de Martin, o código "limpo", não faz nada além disso? É incrivelmente difícil entender o que qualquer um desses códigos faz, porque todos esses métodos incrivelmente pequenos não fazem quase nada e funcionam exclusivamente com efeitos colaterais. Vamos apenas olhar para um método particular.



private String render(boolean isSuite) throws Exception {
   this.isSuite = isSuite;
  if (isTestPage())
    includeSetupAndTeardownPages();
  return pageData.getHtml();
}


Por que esse método tem o efeito colateral de definir o valor this.isSuite? Por que não passá-lo isSuitecomo booleano para chamadas de método posteriores? Por que estamos retornando pageData.getHtml()depois de gastar três linhas de código sem fazer nada pageData? Poderíamos fazer um palpite sobre o que includeSetupAndTeardownPagestem efeitos colaterais nos dados da página, mas e daí? Não podemos conhecer um ou outro até olharmos. E que outros efeitos colaterais isso tem sobre outras variáveis-membro? A incerteza aumenta tanto que de repente nos perguntamos se ela isTestPagetambém pode ter efeitos colaterais. (O que é essa borda? Onde estão as chaves?)



Martin argumenta neste mesmo capítulo que faz sentido dividir uma função em funções menores "se você puder extrair dela outra função com um nome que não seja apenas uma repetição de sua implementação". Mas então ele nos dá:



private WikiPage findInheritedPage(String pageName) throws Exception {
  return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}


Nota: alguns dos aspectos ruins deste código não são culpa de Martin. Esta é uma refatoração de um pedaço de código existente que, aparentemente, não foi originalmente escrito por ele. Esse código já tinha uma API questionável e um comportamento questionável, os quais persistem na refatoração. Primeiro, o nome da classe é SetupTeardownIncluderterrível. É pelo menos uma frase nominal, como todos os nomes de classe, mas é uma frase verbal sufocada clássica. Esse é o nome da classe que você sempre obtém quando trabalha com código estritamente orientado a objetos, onde tudo deve ser uma classe, mas às vezes você realmente precisa apenas de uma função simples.



Segundo, o conteúdo é pageDatadestruído. Ao contrário das variáveis de membro ( isSuite, testPage, newPageContente pageCrawler), nós realmente não possuopageDatapara mudar. Ele é passado inicialmente para os métodos públicos de visualização de nível superior pelo chamador externo. O método de renderização faz um ótimo trabalho e, finalmente, retorna uma string HTML. No entanto, durante este trabalho, como efeito colateral, ele é pageDatamodificado destrutivamente (ver updatePageContent). Certamente seria preferível criar um objeto completamente novo PageDatacom as modificações desejadas e deixar o original intacto? Se o chamador tentar usá-lo pageDatapara outra coisa, ele poderá se surpreender com o que aconteceu com seu conteúdo. Mas é exatamente assim que o código original se comportava antes da refatoração de Martin. Ele manteve esse comportamento, embora o tenha enterrado de maneira muito eficaz.



*

O livro inteiro é realmente assim?



Basicamente sim. O Clean Code combina uma combinação desarmante de dicas e conselhos fortes e atemporais, altamente questionáveis ​​ou desatualizados. O livro foca quase exclusivamente no código orientado a objetos e apela às virtudes do SOLID, excluindo outros paradigmas de programação. Ele se concentra no código Java, excluindo outras linguagens de programação, mesmo outras linguagens orientadas a objetos. Há um capítulo sobre Smells and Heuristics, que nada mais é do que uma lista de pistas razoáveis ​​para procurar em seu código. Mas existem vários capítulos de cabeça vazia onde o foco está nos exemplos demorados e elaborados de refatoração do código Java. Há um capítulo inteiro estudando os componentes internos do JUnit (o livro foi escrito em 2008, para que você possa imaginar o quanto isso é relevante agora). O uso geral de Java no livro está muito desatualizado. Esses tipos de coisas são inevitáveis ​​- os livros de programação tradicionalmente se tornam obsoletos rapidamente - mas, mesmo durante esse período, o código fornecido é ruim.



Há um capítulo sobre teste de unidade. Este capítulo tem muito de bom - ainda que básico - sobre como os testes de unidade devem ser rápidos, independentes e reproduzíveis, como os testes de unidade permitem refatorar com mais confiança seu código-fonte e como os testes de unidade devem ter o mesmo tamanho. como o código em teste, mas muito mais fácil de ler e entender. O autor então mostra um teste de unidade em que ele diz que há muitos detalhes:



@Test
  public void turnOnLoTempAlarmAtThreashold() throws Exception {
    hw.setTemp(WAY_TOO_COLD);
    controller.tic();
    assertTrue(hw.heaterState());
    assertTrue(hw.blowerState());
    assertFalse(hw.coolerState());
    assertFalse(hw.hiTempAlarm());
    assertTrue(hw.loTempAlarm());
  }


e orgulhosamente refaz:



@Test
  public void turnOnLoTempAlarmAtThreshold() throws Exception {
    wayTooCold();
    assertEquals(“HBchL”, hw.getState());
  }


Isso é feito como parte de uma lição geral sobre o valor de inventar uma nova linguagem de teste específica de domínio para seus testes . Fiquei tão confuso com esta afirmação. Eu usaria exatamente o mesmo código para demonstrar conselhos completamente opostos! Não faça isso!



*

O autor apresenta três leis do TDD:



. , .



. , . .



. , .



, , , 30 . , .


... mas Martin não presta atenção ao fato de que a divisão de tarefas de programação em minúsculas partes de trigésimo segundo consome insanamente tempo na maioria dos casos, geralmente obviamente inútil e impossível.



*

Existe um capítulo "Objetos e estruturas de dados", em que o autor fornece um exemplo de estrutura de dados:



public class Point {
  public double x;
  public double y;
}


e este exemplo de um objeto (bem, uma interface para um objeto):



public interface Point {
  double getX();
  double getY();
  void setCartesian(double x, double y);
  double getR();
  double getTheta();
  void setPolar(double r, double theta);
}


Ele está escrevendo:



, . , . . . , , . , .


E é tudo?



Sim, você acertou. A definição de Martin de "estrutura de dados" está em desacordo com a definição que todos os outros usam! O livro não diz nada em tudo sobre codificação pura usando o que a maioria de nós considera estruturas de dados. Este capítulo é muito mais curto do que você poderia esperar e contém muito pouca informação útil.



*

Não vou reescrever todas as minhas outras anotações. Eu tenho muitos deles, e levaria muito tempo para listar tudo o que considero incorreto neste livro. Vou me concentrar em mais um exemplo de código flagrante. Este é o gerador de números primos do capítulo 8:



package literatePrimes;

import java.util.ArrayList;

public class PrimeGenerator {
  private static int[] primes;
  private static ArrayList<Integer> multiplesOfPrimeFactors;

  protected static int[] generate(int n) {
    primes = new int[n];
    multiplesOfPrimeFactors = new ArrayList<Integer>();
    set2AsFirstPrime();
    checkOddNumbersForSubsequentPrimes();
    return primes;
  }

  private static void set2AsFirstPrime() {
    primes[0] = 2;
    multiplesOfPrimeFactors.add(2);
  }

  private static void checkOddNumbersForSubsequentPrimes() {
    int primeIndex = 1;
    for (int candidate = 3;
         primeIndex < primes.length;
         candidate += 2) {
      if (isPrime(candidate))
        primes[primeIndex++] = candidate;
    }
  }

  private static boolean isPrime(int candidate) {
    if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) {
      multiplesOfPrimeFactors.add(candidate);
      return false;
    }
    return isNotMultipleOfAnyPreviousPrimeFactor(candidate);
  }

  private static boolean
  isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) {
    int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()];
    int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor;
    return candidate == leastRelevantMultiple;
  }

  private static boolean
  isNotMultipleOfAnyPreviousPrimeFactor(int candidate) {
    for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) {
      if (isMultipleOfNthPrimeFactor(candidate, n))
        return false;
    }
    return true;
  }

  private static boolean
  isMultipleOfNthPrimeFactor(int candidate, int n) {
   return
     candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n);
  }

  private static int
  smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) {
    int multiple = multiplesOfPrimeFactors.get(n);
    while (multiple < candidate)
      multiple += 2 * primes[n];
    multiplesOfPrimeFactors.set(n, multiple);
    return multiple;
  }
}


O que é esse código? Quais são os nomes dos métodos? set2AsFirstPrime? smallestOddNthMultipleNotLessThanCandidate? Deve ser um código limpo? Uma maneira clara e inteligente de filtrar primos?



Se essa é a qualidade do código que esse programador cria - em seu lazer, em condições ideais, sem a pressão do desenvolvimento real do software de produção -, por que prestar atenção ao restante de seu livro? Ou os outros livros dele?



*

Escrevi este ensaio porque vejo constantemente pessoas recomendando "Código Limpo". Senti a necessidade de oferecer anti-recomendação.



Eu originalmente li o Código Limpo em um grupo no trabalho. Lemos um capítulo por semana durante treze semanas.



Portanto, você não deseja que o grupo ao final de cada sessão expresse apenas um acordo unânime. Você quer que o livro evoque algum tipo de reação dos leitores, alguns comentários adicionais. E suponho que, até certo ponto, isso significa que o livro deve dizer algo com o qual você não concorda ou não divulgar o tópico completamente, como acha que deveria. Com base nisso, o "Código Limpo" mostrou-se adequado. Tivemos boas discussões. Conseguimos usar os capítulos individuais como ponto de partida para uma discussão mais profunda das práticas contemporâneas atuais. Conversamos sobre muitas coisas que não foram abordadas no livro. Discordamos sobre muitas coisas.



Eu recomendaria este livro para você? Não. Mesmo como um texto para iniciantes, mesmo com todas as advertências acima? Não. Talvez em 2008 eu recomendo este livro para você? Posso recomendá-lo agora como um artefato histórico, um instantâneo educacional de como eram as melhores práticas de programação em 2008? Não, eu não faria.



*

Então, a questão principal é que livro (s) eu recomendaria? Eu não sei. Sugira nos comentários, a menos que eu os feche.



All Articles