Provavelmente é hora de parar de recomendar o Clean Code
Talvez não seja possível chegarmos a definições empíricas de “código bom” ou “código limpo”, o que significa que as opiniões de qualquer pessoa sobre as opiniões de outra pessoa sobre “código limpo” são necessariamente altamente subjetivas. Não posso rever o livro Clean Code de Robert C. Martin de 2008 da sua perspectiva, apenas a minha.
Dito isso, o grande problema que tenho com o Clean Code é que muito do código de exemplo no livro é simplesmente terrível.
* No capítulo 3, “Funções”, Martin dá uma variedade de conselhos para escrever bem as funções. Provavelmente, o conselho mais forte neste capítulo é que as funções não devem misturar níveis de abstração; Eles não devem executar tarefas de alto e baixo nível, porque isso é confuso e confunde a responsabilidade da função. Há outras coisas válidas neste capítulo: Martin diz que os nomes das funções devem ser descritivos e consistentes, e devem ser frases verbais, e devem ser escolhidos com cuidado. Ele diz que as funções devem fazer exatamente uma coisa, e fazê-lo bem, o que eu concordo… desde que não sejamos muito dogmáticos sobre como definimos “uma coisa”, e entendamos que, em muitos casos, isso pode ser altamente impraticável. 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 consultas, que respondem a algo, mas não ambos. Tudo isso é um conselho básico razoável.
Mas misturadas ao capítulo há afirmações mais questionáveis. Martin diz que os argumentos de bandeira booleana são má prática, com a qual concordo, porque um não adornado ou em código-fonte é opaco e pouco claro versus um explícito ou … mas o raciocínio de Martin é que um argumento booleano significa que uma função faz mais de uma coisa, o que não deveria. truefalse
IS_SUITE
IS_NOT_SUITE
Martin diz que deve ser possível ler um único arquivo fonte de cima para baixo como narrativa, com o nível de abstração em cada função descendo à medida que lemos, cada função chamando outras mais abaixo. Isso está longe de ser universalmente relevante. Muitos arquivos de origem, eu diria até a maioria dos arquivos de origem, não podem ser hierarquizados dessa maneira. E mesmo para aqueles que podem, um IDE nos permite trivialmente pular de chamada de função para implementação de função e vice-versa, da mesma forma que navegamos em sites.
Ele diz que a duplicação de código “pode ser a raiz de todo o mal no software” e defende ferozmente o DRY. Na época, esse era um conselho bastante comum. Em tempos mais recentes, no entanto, geralmente entendemos que uma pequena duplicação não é a pior coisa do mundo; pode ser mais claro, e pode ser mais barato do que a abstração errada.
E aí fica estranho. Martin diz que as funções não devem ser grandes o suficiente para manter estruturas de controle aninhadas (condicionais e loops); equivalentemente, eles não devem ser recuados para mais de dois níveis. Ele diz que os blocos devem ter uma linha de comprimento, consistindo provavelmente de uma única chamada de função. Ele diz que uma função ideal tem zero argumentos (mas ainda sem efeitos colaterais?), e que uma função com apenas três argumentos é confusa e difícil de testar. Mais bizarramente, Martin afirma que uma função ideal é de duas a quatro linhas de código. Este conselho é realmente colocado no início do capítulo. É a primeira e mais importante regra:
A primeira regra das funções é que elas devem ser pequenas. A segunda regra das funções é que elas devem ser menores do que isso. Esta não é uma afirmação que eu possa justificar. Não posso fornecer referências a pesquisas que mostrem que funções muito pequenas são melhores. O que posso dizer é que há quase quatro décadas escrevo funções de todos os tamanhos. Já escrevi várias abominações desagradáveis de 3.000 linhas. Eu escrevi scads de funções na faixa de 100 a 300 linhas. E escrevi funções que tinham de 20 a 30 linhas. O que essa experiência me ensinou, através de longas tentativas e erros, é que as funções devem ser muito pequenas.
[…]
Quando Kent [Beck] me mostrou o código, fiquei impressionado com o quão pequenas eram todas as funções. Eu estava acostumado a funções em programas de Swing que ocupavam quilômetros de espaço vertical. Cada função neste programa tinha apenas duas, ou três, ou quatro linhas. Cada um era transparentemente óbvio. Cada um contava uma história. E cada um o levou para o próximo em uma ordem convincente. É assim que suas funções devem ser curtas!
Tudo isso soa como hipérbole. Um caso para funções curtas em vez de longas certamente pode ser feito, mas assumimos que Martin não significa literalmente que cada função em todo o nosso aplicativo deve ter quatro linhas de comprimento ou menos.
Mas o livro está sendo absolutamente sério sobre isso. Todos esses conselhos culminam na seguinte listagem do código-fonte no final do capítulo 3. Este código de exemplo é a refatoração preferida de Martin de um par de métodos Java originados em uma 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");
}
}
Volto a dizer: este é o próprio código de Martin, escrito de acordo com seus padrões pessoais. Esse é o ideal, que nos é apresentado como exemplo de aprendizado.
Confesso nesta fase que minhas habilidades em Java são datadas e enferrujadas, quase tão datadas e enferrujadas quanto este livro, que é de 2008. Mas será que, mesmo em 2008, esse código era lixo ilegível?
Vamos ignorar o curinga.import
Primeiro, o nome da classe, , é terrível. É, pelo menos, uma frase nominal, como todos os nomes de classe deveriam ser. Mas é uma frase verbal nominal, o tipo estrangulado de nome de classe que você invariavelmente obtém quando está trabalhando em código estritamente orientado a objetos, onde tudo tem que ser uma classe, mas às vezes o que você realmente precisa é apenas uma simples função gosh-danted.SetupTeardownIncluder
Dentro da turma, temos:
- dois métodos públicos e estáticos, como antes, mais
- um novo construtor privado e
- quinze novos métodos privados.
Dos quinze métodos privados, treze deles ou têm efeitos colaterais (como , que tem efeitos colaterais sobre ) ou chamam para outros métodos que têm efeitos colaterais (como , que chama ). Apenas e parece ser livre de efeitos colaterais. Eles ainda fazem uso de variáveis que não são passadas para eles ( e respectivamente), mas parecem fazê-lo de maneiras livres de efeitos colaterais. buildIncludeDirective
newPageContent
include
buildIncludeDirective
isTestPage
findInherited
PagepageData
testPage
Neste ponto, você pode raciocinar que talvez a definição de Martin de “efeito colateral” não inclua variáveis de membro do objeto cujo método acabamos de chamar. Se tomarmos essa definição, então as cinco variáveis de membro, , , e , são implicitamente passadas para cada chamada de método privado, e elas são consideradas jogo limpo; Qualquer método privado é livre para fazer o que quiser com qualquer uma dessas variáveis. pageData
isSuite
testPage
newPageContent
pageCrawler
Mas a própria definição de Martin contradiz isso. Isso é do início deste exato capítulo, com ênfase acrescentada:
Os efeitos colaterais são mentiras. Sua função promete fazer uma coisa, mas também faz outras coisas ocultas. Às vezes, ele fará alterações inesperadas nas variáveis de sua própria classe. Às vezes, ele vai fazê-los para os parâmetros passados para a função ou para os globais do sistema. Em ambos os casos, são inverdades tortuosas e prejudiciais que muitas vezes resultam em estranhos acoplamentos temporais e dependências de ordem.
Gosto dessa definição. Concordo com essa definição. É uma definição útil, porque nos permite raciocinar sobre o que uma função faz, com algum grau de confiança, referindo-se apenas às suas entradas e saídas. 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? Em vez de fazer com que um método passe argumentos para outro método, Martin cria um hábito angustiante de ter o primeiro método definido uma variável membro que o segundo método, ou algum outro método, então lê de volta. Isso torna incrivelmente difícil descobrir o que qualquer um desses códigos faz, porque todos esses métodos incrivelmente minúsculos não fazem quase nada e funcionam exclusivamente através de efeitos colaterais.
Vejamos apenas um método privado.
private String render(boolean isSuite) throws Exception {
this.isSuite = isSuite;
if (isTestPage())
includeSetupAndTeardownPages();
return pageData.getHtml();
}
Então… Imagine que alguém entra em uma cozinha, porque quer te mostrar como fazer uma xícara de café. Enquanto você assiste com atenção, eles apertam um interruptor na parede. O interruptor parece um interruptor de luz, mas nenhuma das luzes da cozinha acende ou apaga. Em seguida, eles abrem um armário e tiram uma caneca, colocam-na na bancada e, em seguida, batem duas vezes com uma colher de chá. Eles esperam por trinta segundos e, finalmente, chegam atrás da geladeira, onde você não pode ver, e puxam uma caneca diferente, esta cheia de café fresco.
… O que aconteceu? Para que estava mexendo no interruptor? Bater na caneca vazia fazia parte do procedimento? De onde veio o café?
É assim que esse código é. Por que tem um efeito colateral de definir o valor de ? Quando é lido de volta, em , em , em ambos, em nenhum dos dois? Se ele é lido de volta, por que não apenas passar como um booleano? Ou talvez o interlocutor o leia de volta? render
this.isSuite
this.isSuite
isTestPage
includeSetupAndTear
downPagesisSuite
Por que voltamos quando nunca tocamos? Como o HTML chegou lá? Já estava lá? Podemos fazer um palpite educado que tem efeitos colaterais sobre , mas então, o quê? Não podemos saber de qualquer maneira até que olhemos. E que outros efeitos colaterais isso tem em outras variáveis de membros? A incerteza se torna tão grande que, de repente, temos que nos perguntar se ligar também pode ter efeitos colaterais. pageData.getHtml()
pageDataincludeSetupAndTeardownPages
pageDataisTestPage()
Como você testaria esse método? Bem, você não pode. Não é uma unidade. Ele não pode ser separado dos efeitos colaterais que tem em outras partes do código. (E o que acontece com o recuo? E onde estão os aparelhos danados?)
Martin afirma, neste mesmo capítulo, que faz sentido dividir uma função em funções menores “se você pode extrair outra função dela com um nome que não seja meramente uma reafirmação de sua implementação”. Mas então ele nos dá:
private boolean isTestPage() throws Exception {
return pageData.hasAttribute("Test");
}
e:
private WikiPage findInheritedPage(String pageName) throws Exception {
return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}
e meia dúzia de outros, nenhum dos quais sequer é chamado de mais de um local.
Há pelo menos um aspecto questionável desse código que não é culpa de Martin: o fato de que o conteúdo é destruído. Ao contrário das variáveis de membro (, , e ), não é realmente nosso para modificar. Ele é originalmente passado para os métodos públicos de nível superior por um chamador externo. O método faz muito trabalho e, em última análise, retorna um de HTML. No entanto, durante este trabalho, como efeito colateral, é destrutivamente modificado (ver ). Certamente seria preferível criar um novo objeto com as modificações desejadas, e deixar o original intocado? Se o chamador tentar usar para outra coisa depois, ele pode ficar muito surpreso com o que aconteceu com seu conteúdo. Mas foi assim que o código original se comportou antes da refatoração, e o comportamento pode ser intencional. Martin preservou o comportamento, embora o tenha enterrado de forma muito eficaz.pageDataisSuitetestPagenewPageContentpageCrawlerpageDatarenderrenderStringpageDataupdatePageContentPageDatapageData
Alguns outros quebra-cabeças leves: por que usamos para descobrir se esta é uma página de teste, mas temos que consultar um booleano separado para descobrir se esta é ou não uma página de conjunto de testes? E qual é exatamente a separação de preocupações entre e , ambas estão em uso aqui?pageData.hasAttribute("Test")PageCrawlerPageCrawlerImpl
* O livro inteiro é assim?
Praticamente sim. Clean Code mistura uma combinação desarmante de conselhos e conselhos fortes e atemporais que são altamente questionáveis ou datados ou ambos.
Grande parte do livro já não tem muita utilidade. Há vários capítulos do que são basicamente filler, com foco em exemplos trabalhosos de refatoração de código Java; há um capítulo inteiro examinando os aspectos internos da JUnit. Este livro é de 2008, então você pode imaginar o quão relevante isso é agora. Há um capítulo inteiro sobre formatação, que hoje em dia se reduz a uma única frase: “Escolha uma formatação padrão sensata, configure ferramentas automatizadas para impô-la e, em seguida, nunca mais pense sobre este tópico”.
O conteúdo se concentra quase exclusivamente em código orientado a objetos, com a exclusão de outros paradigmas de programação. A programação orientada a objetos estava muito na moda na época da publicação. Martin é um grande defensor do OO, tendo inventado três dos cinco princípios que compõem o SOLID, e tendo popularizado o termo. Mas a total ausência de técnicas de programação funcional ou mesmo de simples código processual era lamentável mesmo naquela época, e só se tornou mais evidente nos anos seguintes.
O livro se concentra no código Java, com a exclusão de outras linguagens de programação, até mesmo outras linguagens de programação orientadas a objetos. Java era popular na época, e se você está escrevendo um livro como este, faz sentido escolher uma única linguagem conhecida e ficar com ela, e Java ainda é muito popular e ainda pode ser uma escolha forte para esse propósito. Mas o uso geral do Java pelo livro é muito datado.
Esse tipo de coisa é inevitável – os livros de programação datam legendariamente mal. Isso é parte da razão pela qual Clean Code foi uma leitura recomendada em uma época, e agora acho que o pêndulo está balançando de volta na direção oposta.
Mas mesmo para a época, mesmo para Java da era 2008, grande parte do código fornecido é ruim.
Há um capítulo sobre testes de unidade. Há muitas coisas boas, básicas, neste capítulo, sobre como os testes de unidade devem ser rápidos, independentes e repetíveis, sobre como os testes de unidade permitem uma refatoração mais confiante do código-fonte, sobre como os testes de unidade devem ser tão volumosos quanto o código em teste, mas estritamente mais simples de ler e compreender. Mas então ele nos mostra um teste de unidade com o que ele diz ser muito detalhe:
@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 ele orgulhosamente a refatora para:
@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
wayTooCold();
assertEquals(“HBchL”, hw.getState());
}
Isso é feito como parte de uma lição geral sobre a virtude de inventar uma nova linguagem de teste específica de domínio para seus testes. Fiquei tão confuso com essa sugestão. Eu usaria exatamente o mesmo código para demonstrar exatamente a lição oposta. Não faça isso!
O que não quer dizer do método nomeado. Esta é uma frase adjetiva. Não está totalmente claro o que esse método faz. Isso define o estado do mundo como muito frio? Reage ao estado do mundo se tornar demasiado frio? Ou é uma afirmação de que o estado do mundo atualmente deve ser muito frio?wayTooCold
Os métodos devem ter nomes de verbos ou frases verbais como , ou . Não sou eu que estou dizendo isso. Essa é uma citação direta deste livro. Capítulo 2, “Nomes significativos” :postPaymentdeletePagesave
Os métodos devem ter nomes de verbos ou frases verbais como , ou .postPaymentdeletePagesave
Este é um conselho perfeitamente correto. E era uma linha de código perfeitamente inequívoca. O que dá? hw.setTemp(WAY_TOO_COLD);
E mesmo que você adivinhe corretamente que chamar define a temperatura para ser muito frio… não tem como você adivinhar que ele também liga internamente. Anteriormente, fomos aconselhados a evitar que o código tenha efeitos colaterais. Este, também, foi um bom conselho. E está, novamente, sendo ignorado no exemplo de código escrito real.wayTooCold()controller.tic()
(E já que estamos aqui, este, o código original não refatorado, é uma bela demonstração das desvantagens dos booleanos não adornados. O que significa quando, digamos, retorna? Isso significa que o estado atual do cooler é bom, ou seja, frio o suficiente, ou seja, desligado? Ou isso significa que ele está ligado e esfriando ativamente? Um com alguns valores, e , poderia ser menos ambíguo.) coolerStatetrueenumONOFF
* O livro nos apresenta o loop TDD:
- Primeira Lei Você não pode escrever código de produção até que tenha escrito um teste de unidade com falha.
- Segunda Lei Você não pode escrever mais de um teste de unidade do que é suficiente para falhar, e não compilar está falhando.
- Terceira Lei Você não pode escrever mais código de produção do que é suficiente para passar no teste com falha no momento.
Essas três leis prendem você em um ciclo que talvez tenha trinta segundos de duração. Os testes e o código de produção são gravados juntos, com os testes apenas alguns segundos antes do código de produção.
Mas o livro não reconhece o passo zero que falta no processo: descobrir como dividir a tarefa de programação à sua frente, para que você possa dar uma minúscula mordida de trinta segundos nela. Isso, em muitos casos, é extremamente demorado e, muitas vezes, obviamente inútil e, muitas vezes, impossível.
* Há um capítulo inteiro sobre “Objetos e Estruturas de Dados”. Nele, recebemos este exemplo de uma estrutura de dados:
public class Point {
public double x;
public double y;
}
e este exemplo de um objeto (bem, a interface de um):
public interface Point {
double getX();
double getY();
void setCartesian(double x, double y);
double getR();
double getTheta();
void setPolar(double r, double theta);
}
Martin escreve:
Esses dois exemplos mostram a diferença entre objetos e estruturas de dados. Os objetos ocultam seus dados por trás de abstrações e expõem funções que operam nesses dados. A estrutura de dados expõe seus dados e não tem funções significativas. Volte e leia isso novamente. Observe a natureza complementar das duas definições. São opostos virtuais. Essa diferença pode parecer trivial, mas tem implicações de longo alcance.
E… é isso?
Sim, você está entendendo isso corretamente. A definição de Martin de “estrutura de dados” discorda da definição que todos os outros usam. Esta é uma escolha de definição muito estranha, embora Martin pelo menos defina seu termo claramente. Traçar uma distinção clara entre objetos como dados burros e objetos como abstrações sofisticadas com métodos é legítimo e útil. Mas é bastante gritante que não haja nenhum conteúdo no livro sobre codificação limpa usando o que a maioria de nós considera como estruturas de dados reais: matrizes, listas vinculadas, mapas de hash, árvores binárias, gráficos, pilhas, filas e assim por diante. Este capítulo é muito mais curto do que eu esperava, e contém muito pouca informação de valor.
* Não vou repetir todo o resto das minhas anotações. Peguei muitos deles, e chamar tudo o que percebo de errado com este livro seria contraproducente. Vou parar com mais um trecho flagrante de código de exemplo. Isso é do capítulo 8, um gerador de números primos:
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;
}
}
Que diabos é esse código? O que é esse algoritmo? Quais são esses nomes de método? ? Por que o código quebra com uma exceção fora dos limites se substituímos o por um segundo? Anteriormente, fomos avisados de que um método deveria ser um comando, que faz algo, ou uma consulta, que responde algo, mas não ambos. Este foi um bom conselho, então por que quase todos esses métodos o ignoram? E a segurança dos fios? set2AsFirstPrimesmallestOddNthMultipleNotLessThanCandidateint[]ArrayList<Integer>
Isso é para ser código limpo? Essa deve ser uma maneira legível e inteligente de pesquisar números primos? Devemos escrever código assim? E se não, por que esse exemplo está no livro? E onde está a resposta “real”?
Se esta é a qualidade do código que este programador produz — a seu bel-prazer, em circunstâncias ideais, sem nenhuma das pressões do desenvolvimento real de software de produção, como exemplo de ensino —, então por que você deveria prestar alguma atenção ao resto de seu livro? Ou aos seus outros livros?
* Escrevi este ensaio porque continuo vendo pessoas recomendando o Clean Code. Senti a necessidade de oferecer uma anti-recomendação.
Li originalmente Código Limpo como parte de um grupo de leitura que tinha sido organizado no trabalho. Lemos cerca de um capítulo por semana durante treze semanas. (O livro tem dezessete capítulos, pulamos alguns por motivos já mencionados.)
Agora, você não quer que um grupo de leitura chegue ao final de cada sessão com nada além de unanimidade. Você quer que o livro extraia algum tipo de reação dos leitores, algo adicional a dizer em resposta. E eu acho que, até certo ponto, isso significa que o livro tem que dizer algo com o qual você discorda, ou não dizer tudo o que você acha que deve dizer. Com base nisso, a Clean Code estava bem. Tivemos boas discussões. Pudemos usar os capítulos individuais como pontos de partida para discussões mais profundas sobre as práticas modernas reais. Conversamos sobre muita coisa que não foi abordada no livro. Discordamos de muita coisa no livro.
Eu recomendaria este livro? Não. Eu recomendaria como um texto para iniciantes, com todas as ressalvas acima? Não. Eu recomendaria isso 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 pergunta matadora é: que livro(s) eu recomendaria? Eu não sei. Sugestões nos comentários, a não ser que eu as tenha fechado.
Atualização, 2020-12-19
Depois de sugestões dos comentários abaixo, li A Philosophy of Software Design (2018) de John Ousterhout e achei uma experiência muito mais positiva. Eu ficaria feliz em recomendá-lo sobre Clean Code.
Uma Filosofia de Design de Software não é um substituto para o Código Limpo. Como o título sugere, ele se concentra mais na prática do design de software em um nível mais alto do que na escrita ou crítica de linhas de código individuais reais. (Como tal, ele contém relativamente poucos exemplos de código.) Por ser voltado para esse nível mais alto, acho que possivelmente não é uma leitura adequada para programadores iniciantes. Muito dessa teoria de alto nível é difícil de compreender ou colocar em prática até que você tenha alguma experiência real para compará-la. Acho que realmente há uma lacuna no mercado para um texto introdutório de programação de nível básico agora.
Dito isso, achei A Filosofia do Design de Software informativo, convincente, conciso e muito mais atualizado. Descobri que concordava com quase todas as afirmações e sugestões de Ousterhout para um bom design de software, muitas das quais são diametralmente opostas às encontradas no Clean Code. Em virtude de fornecer conselhos de nível relativamente alto, sinto que o livro provavelmente envelhecerá um pouco melhor também.
É claro que o desenvolvimento de software ainda está avançando enquanto escrevo isso. Quem sabe como será um bom livro de programação daqui a dez anos?
Posfácio, 2022-03-21
Provavelmente, a defesa mais comum do Código Limpo que vi vindo dos leitores deste ensaio é que o livro ainda vale a pena recomendar, apesar das objeções acima, porque os conselhos no livro não devem ser tomados inteiramente ao pé da letra, ou aplicados dogmaticamente. O ônus é do leitor pensar criticamente, tirar suas próprias conclusões e ignorar seletivamente os conselhos do livro quando esses conselhos são ruins. Até o próprio livro diz:
Qualquer uma das recomendações deste livro é controversa. Você provavelmente não concordará com todos eles. Você pode discordar violentamente de alguns deles. Tudo bem. Não podemos reivindicar a autoridade final. Não acho que esta seja uma defesa particularmente convincente do livro que contém maus conselhos e código de mau exemplo em primeiro lugar.
É verdade que devemos sempre nos envolver criticamente com o material em vez de deixá-lo passivamente nos lavar. Isso é universalmente compreendido; a leitura é isso; É o que diz o ensaio acima. Isso não precisa ser declarado, e é redundante que o próprio livro mencione o ponto. Isso não é algo que possa ser usado para blindar um livro de críticas.
O mais importante é o que o livro realmente diz. E, especialmente em um texto instrucional como este, o quão cuidadosamente o livro tem que ser lido para obter uma experiência positiva dele, e o que acontece se o livro não for lido de forma suficientemente crítica.
Programadores experientes não obterão quase nada com a leitura do Clean Code. Eles serão capazes de pesar os conselhos dados contra suas próprias experiências e tomar uma decisão informada – e o livro lhes dirá quase nada que eles não aprenderam anos atrás.
Programadores inexperientes, por sua vez – e Clean Code é um texto de programação de nível básico, então este é o público-alvo, cujas experiências são mais importantes – não serão capazes de distinguir os bons conselhos dos maus, e não serão capazes de ver que o código de exemplo é ruim e não deve ser imitado. Programadores inexperientes tomarão essas lições pelo valor de face, e pode levar anos até que eles descubram o quão mal foram enganados.