5 Lições de Design Orientado a Objetos da Sandi Metz

por Alan Willms


Esta é uma tradução do artigo escrito por Jessie Young e publicado em: https://18f.gsa.gov/2016/06/24/5-lessons-in-object-oriented-design-from-sandi-metz/

A primeira vez em que ouvi Sandi Metz falando foi em um meetup em San Francisco em 2012. Uma coisa que ela disse naquela noite teve um profundo impacto sobre mim: “O código precisa funcionar só uma vez hoje, mas precisa ser fácil de mudar para sempre”.

Naquele tempo, eu já havia escrito código por alguns anos e entendia que “refatoração” era uma coisa boa. Mas, assim como tantas outras lições que aprendi como uma iniciante, eu não entendia realmente porquê refatorar. E ali estava, tão direto quanto se pode dizer: o código precisa ser refatorado para que possa ser mais fácil de alterá-lo no futuro.

Se um programa que eu estiver escrevendo nunca precisar mudar, a refatoração seria sem sentido. No minuto em que ele começar a funcionar, não importa quão maluco e bagunçado ele pareça no meu editor de texto, eu poderia sair do teclado e ficar satisfeita com o meu código. Mas na verdade, as coisas sempre mudam. No mundo do software, as pessoas constantemente querem mudar, adicionar ou remover funcionalidades. Isso que é escrever código. Portanto nós precisamos refatorar. Nós não mudamos o código para torná-lo melhor, nós o mudamos para torná-lo mais fácil de mudar no futuro.

No mês passado, eu completei o curso de design orientado a objetos da Sandi. Foram três dias intensos de trabalho através de exercícios de refatoração e da discussão de código como um grupo com minha turma de 30 estudantes. Eu tirei muita coisa da aula e voltei ao meu trabalho na 18F animada para praticar o que aprendi. Eu resumi as principais lições que aprendi no curso para você aproveitar.

1. O propósito do design é reduzir o custo da mudança

Essa lição é semelhante à ideia de refatorar o código de modo que ele possa “mudar para sempre”, mas focada no valor de negócio de tornar o código fácil de mudar. Muitas pessoas pensam na refatoração como uma etapa bônus ou algo que eles farão “depois, quando houver tempo”. Mas a refatoração constante é central para os objetivos de qualquer organização que produz software. Sem refatoração, você não consegue ter software bem-arquitetado. Software bem-arquitetado é mais fácil de mudar. Coisas que são mais fáceis de mudar consomem menos tempo. Menos tempo significa menos dinheiro gasto.

Algumas pessoas podem pensar “mas ei, a refatoração consome tempo, e quanto mais rápido eu puder entregar essa funcionalidade, melhor”. Em alguns casos, como a Sandi diz, isso é verdade. Se o seu negócio vai falir amanhã se você não entregar uma funcionalidade hoje, então certamente escreva algum código porcaria e faça o deploy em produção. Mas se você está trabalhando em um repositório por um tempo indefinido no futuro, você precisa focar nas consequências de longo prazo das suas ações de hoje. Se você está escrevendo código mal projetado para economizar tempo agora, você precisa se perguntar: você realmente está poupando o dinheiro da empresa entregando coisas mais rápido, ou está custando mais a ela no futuro por escrever código que é difícil de alterar?

2. Busque o verde mais fácil de alcançar

No nosso primeiro exercício do curso, a Sandi nos pediu para escrever um script em Ruby para fazer com que um arquivo de testes passasse. Esse script precisava cantar qualquer verso da canção “99 bottles of beer on the wall”, dado o número do verso.

Eu me considero uma pessoa que não tem problemas em fazer as coisas do jeito “fácil”. Eu sempre digo “primeiro faça funcionar e depois melhore”. Então eu comecei escrevendo o código para fazer o primeiro verso (para as 99 garrafas) funcionar. Então comecei escrevendo o código para o verso 1, onde usei interpolação para determinar o número de garrafas e notei que a palavra “bottles” precisava ficar no singular como “bottle”, então eu escrevi uma pequena condição para isso. A segunda parte do verso também é um pouquinho diferente, então escrevi outra condição. Então cheguei no verso “zero”, que precisou de outra condição. E então outra. E outra.

Não demorou muito para eu ter um método de 40 linhas incrivelmente confuso. E só era código para escrever uma canção simples! E eu achava que estava escrevendo do jeito mais simples possível!

class Bottles
  def verse(number)
    if number - 1 == 0
      next_number = "no more"
    else
      next_number = number - 1
    end
    if number - 1 == 1
      next_bottle = "bottle"
    else
      next_bottle = "bottles"
    end
    if number == 1
      bottle = "#{number} bottle"
    elsif number == 0
      bottle = "no more bottles"
    else
      bottle = "#{number} bottles"
    end
    if number == 1
      pronoun = "it"
    else
      pronoun = "one"
    end
    if number == 0
      second_verse = "Go to the store and buy some more, " +
      "99 bottles of beer on the wall.\n"
    else
      second_verse = "Take #{pronoun} down and pass it around, " +
      "#{next_number} #{next_bottle} of beer on the wall.\n"
    end
    "#{bottle.capitalize} of beer on the wall, " +
      "#{bottle} of beer.\n" +
      second_verse
  end
end

É hora do verde desavergonhado

Quando nós terminamos este exercício, Sandi nos ensinou sobre o conceito do “verde desavergonhado”: faça a coisa mais simples possível para fazer os testes passarem (ficarem verdes). Esse estado é chamado de verde “desavergonhado” porque o seu objetivo é só fazer os testes passarem, e nada mais. Você pode estar com vergonha do código que você escreveu para fazer isso, porque ele parece simples demais para ter sido escrito por alguém do seu nível intelectual. Então, e somente então, você pode abstrair a lógica.

No caso das 99 garrafas, o verde desavergonhado significa não se preocupar com lógicas extravagantes para os versos que não se parecem com os outros. Ao invés disso, ele usa uma condição para imprimir as diferentes strings:

class Bottles
  def verse(number)
    case number
    when 0
      "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"
    when 1
      "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"
    when 2
      "2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"
    else
      "#{number} bottles of beer on the wall, #{number} bottles of beer.\nTake one down and pass it around, #{number-1} bottles of beer on the wall.\n"
    end
  end
end

Os programadores amam escrever código esperto, e no caso do exercício das 99 garrafas, eu não fui uma exceção. Ao invés de pensar comigo mesma “OK, versos 1 e 0 são diferentes, então vou imprimir uma string diferente nesses casos”, eu imediatamente tentei usar a mesma string com interpolação para funcionar em todos os versos. O código resultante, embora seja um esboço, é uma implementação mal projetada e quase impossível de entender.

Quando você começa pelo verde desavergonhado, você começa com algo que pode ter uma tonelada de duplicação, mas é muito fácil de entender. Neste momento, você pode considerar a funcionalidade pronta. Isso parece contradizer o primeiro tópico sobre código bem arquitetado, mas não contradiz. O jeito pelo qual eu abordei o problema foi uma solução inelegante, mas difícil de entender. O verde desavergonhado é uma solução inelegante que é fácil de entender. Sempre comece pelo verde desavergonhado, e então extraia os métodos e os objetos assim que houver requisitos de produto que serão difíceis de implementar com sua solução de verde desavergonhado. Esse é o caminho para o software bem projetado.

3. A duplicação é muito mais barata do que a abstração errada

Um motivo pelo qual é tão difícil começar pelo verde desavergonhado é que geralmente ele inclui uma grande quantidade de duplicação. Pense da seguinte maneira: um jeito de resolver o problema das 99 garrafas seria escrever um método de 202 linhas que retorna uma string completamente diferente para cada número recebido. Na primeira vez que tentei resolver o problema, eu fui na direção completamente oposta: zero duplicação. E meu código imediatamente se tornou difícil de entender.

Quando se trata de duplicação, tornar o código DRY muito cedo frequentemente leva a código que é mais difícil de projetar. E eis o porquê: quando quer que você remova a duplicação, você está criando uma abstração. Como na minha primeira resposta para o exercício das garrafas, eu criei várias condições que retornavam valores baseados nos números recebidos. De modo a usar esses valores na string, eu tive que dar um nome a eles. Então eu estava dando nome a valores sem nem entender o que eles eram.

Por exemplo: na canção das 99 garrafas, os versos começam com um número e esse número diminui em um na segunda parte do verso. Então quando eu escrevi a condição para esse valor “menos um”, eu chamei ela de next_number. No entanto, quando você chega no verso “1”, o next_number se torna a string “no more”. “no more” não é um número! Por inventar um nome antes de implementar o verde desavergonhado, eu dei um nome impreciso a um conceito.

Você pode argumentar que dar um nome errado não é grande coisa. É só renomear, certo? Sim e não. Sim, o método pode ser renomeado. Não, dar o nome a ele cedo demais trás sim consequências. Mover de uma abstração para outra abstração diferente é muito mais difícil do que mover de uma concreção (tal como uma string duplicada) para uma abstração. Quando vemos todos os elementos duplicados na nossa frente, temos uma chance muito melhor de inventar um nome mais apropriado para essa duplicação. Ao invés disso, nós temos a pré-disposição de tentar enfiar o novo caso de uso em nossa abstração já existente. E ao fazer isso, estamos tornando o nosso código mais difícil de entender e assim mais difícil de mudar no futuro.

Remover a duplicação não é sempre a escolha errada. Por exemplo, no exemplo de verde desavergonhado das garrafas, 97 dos 100 versos possíveis funcionam na condição else do case (nós não digitamos todos os 100 versos). Mas remover a duplicação não é sempre algo bom. DRY é um dos primeiros conceitos que você aprende quando começa a programar. É um conceito bom de aprender no começo porque seres humanos são pattern matchers naturais, e até mesmo novatos podem identificar rapidamente lógica duplicada em seu código. Mas o que eu aprendi no workshop da Sandi é que em muitos casos, ter alguma duplicação é melhor do que ter zero duplicação com código que é difícil de entender.

4. Refatorar código deveria ser seguro e chato

Todos os métodos da Sandi dependem de ter uma suíte de testes que te diz se o seu código está funcionando conforme o esperado. Muitos programadores conhecem o adágio “vermelho, verde, refatorar”. Isso significa que você começa com um teste que falha (vermelho), escreve código para fazê-lo passar (verde), então muda o código para torná-lo melhor (refatorar).

Tipicamente, quando chego na etapa de refatorar, eu faço um punhado de pequenas alterações e rodo meus testes novamente. Nesse ponto, é comum perceber que minhas mudanças quebraram alguma coisa que antes funcionava. Então eu me torno uma detetive em uma missão para descobrir o que eu fiz que causou a quebra. Muitas vezes esse processo é arriscado e empolgante. Arriscado, porque descobrir o que você fez para quebrar a funcionalidade do código frequentemente demora mais tempo do que fazer a mudança que quebrou o código levou em primeiro lugar. Empolgante, porque estou escrevendo um monte de código, o que é divertido, já que rodar testes não é nem um pouco. Colocando as palavras de outra maneira: meu método anterior de refatoração é o equivalente de “mover-se rapidamente e quebrar as coisas”. Refatorar com muita afobação é excitante porque você faz um monte de coisas ao mesmo tempo e, pelo menos para os programadores, escrever um monte de código é super divertido.

“Vermelho, verde, verde infinito”

Na aula de design orientado a objetos da Sandi, seguimos um padrão estrito ao refatorar, que eu gosto de chamar de “vermelho, verde, verde infinito”. Nesse processo, começamos com testes que falham, fazemos com que eles passem (com o verde desavergonhado), e então refatoramos. Mas durante nossa refatoração, nós rodamos os testes depois de cada . uma . das . mudanças. Isso significa que você não pode mover o código para uma nova pasta e excluir ele no lugar anterior e então rodar os testes. Primeiro você copia o código e cola ele na nova localização. Então você roda os testes. Se eles ainda estiverem passando, então você avança para a próxima mudança. Se a qualquer momento os testes falharem, você desfaz a última mudança, assegura-se de que os testes estão verdes novamente, e então faz uma alteração diferente que permite que a suíte de testes permaneça verde. Se essa mudança diferente passar, então você pode partir para a próxima. E assim por diante.

Se isso parecer chato, é porque é mesmo. É o que a Sandi chama de “seguro e chato”. Mas há imenso valor no seguro e chato. Primeiramente, você sempre sabe exatamente o que fez com que os testes falhassem. Quando você roda os testes de tempos em tempos, você pode acabar com uma suíte quebrada e sem ideia alguma do porquê ela quebrou. É comum perder mais tempo debugando uma mudança feita durante a refatoração do que na refatoração em si. Embora esse trabalho de detetive possa ser divertido e estimulante, ele também consome bastante tempo, o que significa que é mais caro. Com o método “seguro e chato” de rodar os testes após cada alteração, você sempre sabe exatamente qual mudança causou a quebra. Está a apenas a um desfazer de distância.

Além disso, o ritmo de rodar os testes após todas as mudanças te força a fazer alterações pequenas e fáceis de entender. Um erro que eu sempre cometo ao refatorar é ver um “objeto piscando” no código que eu quero mudar, e então eu me distraio da minha refatoração original e acabo mudando coisas que não têm nada a ver com o meu trabalho atual. Isso não é problemático apenas no contexto de code review (onde é muito melhor enviar trechos de código coesos), mas também leva a situações onde coisas demais mudaram e eu não sei mais o que eu estou fazendo. Focando em fazer uma única coisa pequena de cada vez, há zero necessidade de trabalho de detetive e um risco muito menor de mudar código que não está relacionado ao assunto em questão.

Nem toda a programação precisa ser segura e chata, mas fazendo a refatoração em um estado de “verde infinito”, você assegura-se de que terá mais tempo e energia sobrando para as tarefas verdadeiramente empolgantes. Permita que a refatoração seja um lugar para cair no ritmo suave de fazer uma alteração, rodar os testes, fazer uma alteração, rodar os testes.

5. Escreva o melhor código possível hoje, seja completamente desapegado dele e esteja disposto a apagá-lo amanhã

É fato: os programadores gastam muito mais tempo mudando código que já existe do que escrevendo código novo. Enquanto gastamos um tempão pensando sobre novas funcionalidades excitantes, a realidade é que a maior parte do nosso trabalho envolve mudar coisas que já estão aí. Até mesmo aqueles que estão em repositórios relativamente novos raramente se veem escrevendo código que não requer mudanças no código existente. Muitas vezes, o código existente é algo que nós mesmos escrevemos. Talvez há meses. Talvez há dias. Talvez até há algumas horas.

Para se tornar um pró em design orientado a objetos como a Sandi Metz, você precisa ter completo desapego pelo código que você escreveu. Devido à falácia dos custos irrecuperáveis, muitos de nós temos problemas com isso. Nós pensamos: “eu gastei tanto tempo escrevendo aquilo! Funciona! É bonito! Eu não posso excluir!” Mas requisitos de mudança significam que o seu código pode precisar de uma arquitetura diferente, o que significa desapegar de implementações existentes para torná-las abertas a novos requisitos.

Não existe código perfeito, só código que funciona para as necessidades de hoje. Você nunca poderá prever as necessidades de amanhã. Nem deveria, pois isso leva a abstração prematura. Mas quando chega um novo requisito que requer que você repense completamente uma solução que você criou ontem, o único curso de ação é praticar o completo desapego à implementação existente e repensar o seu design para que o código continue a ser fácil de mudar para sempre.