Em 2001, um grupo de pessoas descontentes com a maneira como os projetos de software eram executados se juntaram para discutir como melhorar, de forma geral, esse quadro. Depois de muita conversa, eles criaram o Manifesto para o Desenvolvimento Ágil de Software, também conhecido como Manifesto Ágil. Um dos signatários, o David Thomas, odeia esse nome mais curto, mas essa é uma história que deixo para contar um outro dia.
Hoje, não vamos falar do Pragmatic Thomas e sim de outra pessoa presente naquela sala em que o manifesto nasceu: Robert C. Martin, aka Uncle Bob ou, mais precisamente, de algumas ideias de seu excelente livro Clean Architecture, onde ele discute o que é arquitetura de software, onde vive e do que se alimenta. Nas páginas ele também aborda o que faz de uma arquitetura uma boa escolha para um determinado contexto de software e quais os dilemas que programadores enfrentam durante o desenvolvimento que podem ser atacados diretamente, ou contornados, quando pensamos em uma Clean Architecture.
O livro é extremamente interessante e tem conteúdo vasto que pode ser explorado em diversos posts, neste artigo vou explorar duas ideias:
O que faz de uma arquitetura de software uma escolha boa? Porque você deveria postergar ao máximo a tomada de decisões arquiteturais até que elas sejam realmente necessárias?
Vamos lá!
Então, o que faz de uma arquitetura de software uma boa arquitetura? O que é uma boa arquitetura de software?
Uma boa arquitetura de software é aquela em que é fácil de fazer mudanças.
Para validar essa afirmação podemos fazer um breve exercício. Suponha que você pudesse escolher entre dois sistemas para assumir o desenvolvimento. Qual deles você escolheria?
O primeiro está funcionando perfeitamente, mas é complexo e praticamente impossível de alterar pela forma que foi construído. Uma alteração em um módulo desse sistema geralmente resulta em quebrar outras partes que, até então, estavam funcionando. O tempo que um novo programador leva para entender e dominar o sistema é contado em meses.
O segundo sistema não está funcionando atualmente, mas foi construído de forma simples e clara, fazer mudanças é fácil e não impacta quebrando outras partes do sistema. Um novo programador leva apenas algumas semanas para dominar como o sistema funciona.
Qual você escolheria?
Existe uma grande possibilidade que sua empresa opte pela primeira opção pois, afinal, ele está funcionando e suportando o negócio. Mas esse pensamento é de curto prazo. Todo software irá invariavelmente mudar em algum momento, seja por regras de negócio que não fazem mais sentido, por novas regras que surgiram, ou por alterações de leis que precisam ser cumpridas, etc. E quando esse momento chegar, mudar o primeiro software será caro, muito caro. Às vezes tão caro que o custo da mudança é muito maior que o benefício que ela traria.
A segunda opção parece contra-intuitiva. O software não está funcionando, então porque eu deveria escolhê-la? Porque é simples de fazer mudanças, a correção será, provavelmente, rápida e não irá interferir em outras partes do sistema. Quando o momento de mudar o software para suportar um novo modelo de negócios surgir, por exemplo, o custo dessa mudança será pequeno frente ao benefício que ela trará.
Então, quando nós, programadores e engenheiros de software, formos definir como será um determinado sistema, as melhores escolhas serão, invariavelmente, as que forem mais fáceis de alterar no futuro. Essa é a essência de uma boa arquitetura de software segundo o clean architecture. E faz todo o sentido, responder à mudanças é um dos preceitos do manifesto para o desenvolvimento ágil de software - e você sempre terá mudanças, é apenas uma questão de tempo.
O que nos leva a segunda parte deste artigo: quais decisões eu tenho que tomar, ou deixar de tomar, para que meu software tenha uma Clean Architecture?
O Uncle Bob sugere que uma boa arquitetura adia a tomada de decisão o máximo possível. Até quando? Até que não seja possível mais adiar. E a razão para esse recomendação é simples: eu costumo entender ela como “não resolva problemas imaginários” porque eles podem nunca existir.
Todo programador com algum tempo de vivência na área já se deparou com alguns “sistemas elefantes”, que são pesados e tem muito mais complexidade que o problema que ele tenta resolver exige. Um dos exemplos do livro é de um sistema que durante a sua concepção foi levantada a necessidade dele ser executado em um ambiente de computação distribuída e um grande esforço de desenvolvimento foi investido para que isso fosse alcançado.
Depois de anos de desenvolvimento o sistema ainda continuava rodando em apenas um servidor dedicado, nada de computação distribuída ou coisas que o valha. Mas, todo programador que fosse implementar uma nova funcionalidade tinha que implementar todo o suporte para processamento distribuído na sua atividade, mesmo que ela nunca fosse usada na prática, o que causava uma complexidade muito maior que o necessário, impactando diretamente no tempo de desenvolvimento, gerando custo e frustração.
A decisão de que o sistema utilizasse computação distribuída poderia ter sido adiada até que essa necessidade fosse realmente imprescindível. Uma opção seria utilizar o Open-Closed Principle (OCP) que diz que um artefato de software deve ser fechado para mudanças mas aberto para extensões. Essa regra é discutida no livro e poderia ser usada aqui para que o suporte à computação distribuída fosse uma opção de extensão futura. Podemos também pensar na regra de que sempre procuramos criar sistemas com baixo acoplamento entre seus componentes facilitando mudança. Investindo em um desenho de arquitetura assim deixaria extensões futuras mais simples de implementar.
Outro exemplo é a escolha do banco de dados que sua aplicação vai utilizar. Quando pensamos na arquitetura inicial de um sistema deveríamos nos preocupar em persistir os dados e não em qual banco e paradigma usar. Em alguns casos essa persistência pode ser feita até mesmo em arquivos texto até que tenhamos certeza de que precisamos de um banco de dados NoSQL ou relacional porque a natureza dos dados favorece esse ou aquele tipo de armazenamento.
É o que eu gosto de chamar de resolver problemas imaginários. Por exemplo: analisando um novo produto de software eu posso imaginar que um dia, quem sabe, talvez, o sistema precise usar um banco de dados NoSQL para persistir dados, então vou deixar isso previsto na arquitetura do sistema e forçar o programador, por meio de interfaces em Java ou algum outro tipo de contrato via código em outras linguagens, a implementar esse suporte. Dessa forma, mesmo que o sistema nunca use NoSQL, quem for fazer uma nova funcionalidade terá que se preocupar com isso e codificar o necessário para respeitar os contratos de software que o arquiteto de software determinou.
A decisão de qual banco de dados usar deveria ser adiada o máximo possível. Os dados são o que importam em um sistema, o banco de dados usado é apenas um detalhe na arquitetura e não a razão para ela existir.
Boas arquiteturas são fáceis de mudar, mas isso não significa que sejam fáceis de conceber. É necessário investir um certo tempo para acertar a mão, principalmente no que tange adiar decisões e não resolver problemas imaginários. Aqui, como em praticamente tudo no mundo do desenvolvimento, é por a mão na massa e fazer, observar os resultados, corrigir a rota e fazer de novo. Esse ciclo de descoberta é o que nos leva a grande produtos.