Design de software: sua importância e dicas para a criação de bons designs
Tags: cleancode, tdd, refactoring, softwaredesign
Published: 2022-10-03 11:37:54

Design de software é o modo como organizamos os elementos que compõem o código, a separação, a dependência e a forma como esses elementos se comunicam.

O cuidado com o design do software é fundamental para que seja possível evoluir a aplicação com o menor custo e esforço possível. Além de tornar o desenvolvimento mais agradável a todas as pessoas do time e possibilitar a entrega de valor constante, acomodando as mudanças de forma natural.

Um bom design minimiza o custo de manutenção, pois seus elementos são claros e facilmente compreensíveis, o que impacta na capacidade de adaptação às inevitáveis e inesperadas mudanças que acontecem durante o ciclo de vida de um sistema.

Investir em um bom design é de extrema importância e todas as pessoas desenvolvedoras precisam assumir essa responsabilidade. Portanto, nesse artigo eu trago algumas dicas para a criação de bons designs. 

O propósito deste artigo é introduzir os conceitos, princípios, padrões e práticas, que são fundamentais para a criação de bons designs e acredito que cada tópico abordado merece uma série de artigos.

### Complexidade acidental x Complexidade herdada
Uma das piores coisas na programação é a complexidade. Ela atrapalha o ritmo do time e compromete a qualidade do produto. Para piorar, como programadores, temos uma habilidade especial para criar códigos e soluções complexas. Fazer o simples não é uma tarefa fácil, portanto, vale a pena começar falando sobre a relação entre design de software e complexidade.

Podemos considerar dois tipos de complexidade: a acidental e a herdada. A complexidade acidental está diretamente relacionada com a solução que criamos para um determinado problema. Esse tipo de complexidade geralmente ocorre quando propomos uma solução que não apenas resolve o problema atual, mas também é criada pensando em possíveis cenários que podem nunca acontecer, ou quando criamos um código confuso, que não revela a sua intenção. O código funciona, mas é difícil identificar o que ele faz e porque ele faz determinada ação. Assim, devemos evitar esse tipo de complexidade, pois ela aumenta significativamente o custo de manter o software.

Por outro lado, existe a complexidade herdada, que está relacionada com o problema a ser resolvido. Em alguns momentos, o problema por si só é complexo e naturalmente exige uma solução complexa. Não podemos evitar esse tipo de complexidade, mas devemos pensar em maneiras de minimizar o seu impacto. 

Saber a diferença entre esses dois tipos de complexidade é um bom começo para começarmos a criar bons designs e, consequentemente, bons produtos. 

O computador é capaz de "entender" qualquer código que escrevemos, seja ele complexo ou não, mas são as pessoas que alteram e são responsáveis por evoluir o software. Portanto, ao nos preocuparmos com a complexidade das soluções que criamos estaremos contribuindo diretamente com a capacidade de evolução dos produtos que desenvolvemos e com a qualidade de vida dos nossos colegas.

Um bom design elimina a complexidade acidental e esconde a complexidade herdada.

### Simple Design
Mais do que uma prática, o Simple Design é um estilo de programação que nos leva a pensar e criar soluções simples, que atendam os requisitos atuais, sem qualquer tipo de especulação sobre possíveis mudanças futuras. Ao invés de tentar prever como o software irá mudar, focamos no que ele precisa fazer hoje. Investimos tempo e esforço na criação de códigos que reflitam os requisitos com a maior clareza possível; utilizamos práticas que garantam o seu funcionamento correto, enxergamos cada mudança como uma oportunidade de aprendizado e evolução do design; preparamos o software para acomodar essas mudanças com naturalidade; nos apoiamos em princípios de design que nos ajudam a criar códigos que atendam os requisitos atuais e que também estejam prontos para evoluir com facilidade.

Esse estilo de programação nos afasta da complexidade acidental e nos leva a pensar em soluções que escondem a complexidade herdada, tendo como resultado, um software que entrega o valor esperado e pode ser evoluído com facilidade. Além disso, essas práticas geram um efeito psicológico extremamente positivo para quem está programando. Paramos de nos preocupar com problemas que não existem e focamos no que deve ser feito hoje. A ansiedade diminui e com isso conseguimos dar o nosso melhor para as coisas que realmente importam.

### Clean Code
Clean Code representa os detalhes do código. É sobre ter consciência de que os detalhes importam e que o código é a única representação válida do software. Somos comunicadores e o código é a nossa principal ferramenta de comunicação. Desta forma, devemos nos preocupar com a expressividade, clareza e intenção do código que criamos. Devemos criar códigos que sejam fáceis de ler e que não apenas funcionem, mas que também revelem o seu propósito de forma simples e direta.

### Design Principles
Os princípios de design são frutos de experiências vividas por programadores, que perceberam algumas características comuns entre softwares que evoluíam com mais facilidade. Essas características foram transformadas em princípios que servem para nos auxiliar na construção de softwares que atendam as necessidades hoje e que estejam prontos para evoluir.

Constantemente temos que tomar decisões que afetam diretamente o design do código. Ao conhecer os princípios de design, podemos tomar as decisões de forma consciente.

Além de possibilitar a construção de bons designs, dominar os princípios é fundamental no processo de aprendizagem e utilização de padrões de projeto. Basicamente, os padrões se baseiam nos princípios e é uma forma comprovada e documentada da aplicação deles para resolver um problema garantindo a qualidade do design do código. Portanto, dominar os princípios facilita o processo de aprendizagem e aplicação dos padrões e nos torna capazes de elaborar padrões para resolver problemas específicos do nosso software.

### Design Patterns
Os padrões de projeto são soluções documentadas e amplamente adotadas de como resolver um problema no desenvolvimento de um código. Eles têm como propósito, criar um código flexível e expressivo. Conhecer os padrões, entender a intenção de cada um e principalmente dominar os princípios e valores em que eles se baseiam, eleva consideravelmente a nossa habilidade de criar bons códigos.

Além disso, por se tratar de algo documentado e muito utilizado pelos programadores, a aplicação de padrões facilita a comunicação. Cada padrão possui uma bagagem carregada de informações e o simples fato de dizer a alguém que um determinado padrão foi utilizado, faz com que essa pessoa possa imaginar os detalhes da implementação.

Em contrapartida, a má utilização dos padrões aumenta a complexidade da aplicação. Uma situação comum é tentar utilizar um padrão para resolver um problema inadequado, é como se tentássemos utilizar uma chave de fenda para bater em um prego. Eles não foram criados para serem usados dessa forma. Por isso, é muito importante conhecer os padrões, para saber quando utilizar e principalmente quando não utilizar.

Um bom momento para utilizá-los é durante a refatoração. Neste momento estamos focados em melhorar o design do código, que pode já estar apresentando alguns problemas. Assim, podemos identificar se cabe ou não a aplicação de algum padrão para solucionar os problemas existentes.

Devemos conhecer e dominar os padrões de projeto, entendendo a intenção de cada um e utilizar a refatoração para identificar problemas que podem ser resolvidos através da utilização de algum padrão. É importante lembrar que os padrões foram criados para resolver problemas específicos. Utilizá-los para resolver problemas que eles não foram projetados para solucionar pode prejudicar a aplicação, aumentando a sua complexidade e diminuindo a sua capacidade de responder às mudanças.

### TDD
Test Driven Development (TDD) é uma excelente ferramenta para a construção de código, dessa forma irei aprofundar em alguns de seus detalhes e no motivo pelo qual eu entendo essa prática como um instrumento para se criar bons códigos.

A maioria dos códigos com boa qualidade que já li foram escritos por pessoas que dividem o processo da escrita de código em dois momentos. O primeiro momento é focado em resolver o problema, tendo pouco ou nenhum esforço investido no refinamento do design. O segundo momento é dedicado ao refinamento do design. Isso porque tentar criar um bom design, pensar na solução do problema e ainda construir o código que materializa esse pensamento, nem sempre é uma atividade trivial. Sendo assim, é uma boa estratégia focar na solução do problema e em seguida dedicar um tempo para refinar o design do código. Porém, para que essa estratégia funcione, é necessário garantir que alterações para melhorar o design do código não afetarão o seu comportamento. Além disso, essa estratégia tende a trazer melhores resultados quando a alternância entre os momentos acontece várias vezes durante a evolução do código.

Podemos utilizar o TDD para nos auxiliar na escrita de código, possibilitando focar no problema a ser resolvido e no design em momentos separados, através de pequenos ciclos iterativos que geram feedbacks. Esses feedbacks podem ser utilizados para melhorar o código gradativamente, tudo isso com a garantia de que as melhorias de design não irão alterar o comportamento da aplicação, desde que tenhamos disciplina para aplicar as regras do TDD.

As 3 regras do TDD:
1. Não escreva nenhum código de produção se não houver um teste falhando.
2. Escreva apenas o código de teste suficiente para falhar.
3. Escreva o mínimo necessário de código de produção para fazer o teste passar.

O propósito dessas 3 regras é garantir que todo código de produção escrito está devidamente testado, isso porque cada código de produção foi escrito para fazer passar um teste que representa um cenário específico.

Além dessas regras, o TDD é composto pelas etapas abaixo que nos orientam na aplicação correta da técnica:
1. RED: Nessa etapa focamos no cenário que queremos implementar, fazemos isso através da criação de um teste que representa esse cenário, esse teste deve falhar até que o cenário seja implementado. É muito importante executar o teste mesmo sabendo que ele irá falhar e certificar que o motivo pelo qual ele falhou é porque o cenário ainda não foi implementado.

2. GREEN: Nessa etapa focamos em fazer o teste da etapa anterior passar, ou seja, focamos na implementação do cenário sem muita preocupação com o design do código. É muito importante implementar apenas o mínimo necessário para que o teste passe, dessa forma evitamos criar código cujo comportamento não está coberto por teste.

3. REFACTORING: Nessa etapa focamos no design do código que criamos, devemos analisá-lo e identificar se existe algo a melhorar, é o momento ideal para identificar os odores do código e através da refatoração aplicar princípios e padrões de design com o propósito de melhorar a qualidade do código que criamos.

Essas etapas constituem um ciclo iterativo que se repetirá até que finalizemos o desenvolvimento. Esse ciclo nos dá feedback constantemente sobre a qualidade do código e nos possibilita dar pequenos passos constantemente em direção ao objetivo final, esses pequenos passos nos proporcionam um ritmo sustentável e nos mantém sob controle do código durante todo desenvolvimento.

### Refactoring
Refatoração é a prática de alterar o design do código sem modificar o comportamento da aplicação. Através dessa prática, podemos construir softwares que não apenas atendam às necessidades de hoje, mas que também estejam prontos para evoluir.

Programação é uma arte, e assim como qualquer outra arte, é necessário uma série de refinamentos até que o resultado seja realmente satisfatório. Não precisamos carregar o peso de criar o código perfeito logo de cara. Devemos nos comprometer com a realização desses refinamentos, com o propósito de melhorar o design do código e preparar nossas aplicações para acomodar as mudanças.

Infelizmente, algumas pessoas exploram pouco essa prática. Talvez por não conseguirem garantir de forma rápida que a refatoração não vai impactar no funcionamento do software, elas acabam desistindo da ideia de refatorar, afinal, para essas pessoas um software que funciona é melhor do que nada. Talvez elas não saibam que ao fazer isso, podem influenciar negativamente a capacidade de evolução do software. Mudanças que até então eram simples de serem feitas começam a ficar mais complexas e consequentemente mais caras.

Sabendo da importância dessa prática, precisamos ter tranquilidade e segurança para refatorar um código. Para isso, é importante que existam testes automatizados que podem ser executados constantemente, ajudando a garantir que as mudanças não tenham impacto no comportamento da aplicação. A utilização do TDD nos possibilita refatorar o código várias vezes durante a sua escrita. Como resultado, no final temos um código que funciona, uma cobertura de testes confiável e um design que possibilitará ao software continuar evoluindo com o menor custo possível. As pessoas do nosso time irão nos agradecer por esse cuidado com o código, inclusive nós mesmos no futuro.

### Pair Programming e Code Review
Somos bons para encontrar problemas no código dos outros. Sabendo disso, podemos explorar essa habilidade para contribuir com o código das outras pessoas e permitir que elas contribuam com o nosso.

Existem duas práticas que são úteis para que essa troca de feedback aconteça: Pair Programming e Code Review. Um dos propósitos dessas duas práticas é fazer com que o código seja construído por mais de uma pessoa.

Code Review é uma prática que geralmente acontece após concluir a implementação. O código é submetido a uma etapa de revisão, onde outras pessoas devem avaliar o funcionamento, os impactos da implementação e principalmente a qualidade do design criado. É um momento para avaliar a simplicidade e a intenção do código, uma vez que se os revisores tiverem dificuldades para compreender a implementação realizada é provável que o código possa ser melhorado. Quando isso acontece, o revisor deve dar feedback ao criador do código e ajudá-lo a melhorar o design criado.

Apesar de ser muito utilizado após a conclusão da implementação, acredito que o Code Review pode ser melhor utilizado durante o Pair Programming, que é a prática onde duas pessoas trabalham juntas na implementação, dividindo o mesmo teclado. Uma pessoa é responsável por escrever o código enquanto a outra deve ajudar em pesquisas, tomadas de decisão e principalmente na revisão de código. A grande vantagem é que durante o pareamento, a revisão de código acontece de forma contínua, durante todo o processo de escrita de código, o que faz com que o ciclo de feedback seja mais curto em relação à revisão de código realizada após a implementação e impede que a revisão se torne um bloqueio no fluxo de desenvolvimento.

Parece controverso que a utilização de Pair Programming possa funcionar, teoricamente seria mais produtivo se cada pessoa estivesse trabalhando em uma atividade separada e isso seria verdade se a nossa tarefa mais difícil fosse escrever código, mas sabemos que não é o caso. Durante o desenvolvimento de um software, existem muitos fatores que influenciam no seu sucesso, como o funcionamento esperado, o desempenho esperado e a sua capacidade de mudança. O software precisa fazer o que se propõe de maneira satisfatória e precisa estar pronto para evoluir. Sabendo disso, quando trabalhamos juntos em um mesmo objetivo aumentamos significativamente as nossas chances de criar um produto que possua todas as características desejadas, pois duas cabeças pensam melhor do que uma.

Podemos utilizar essas práticas como oportunidade de trocar feedbacks que ajudarão na construção de um código que seja fácil de manter, além de compartilhar conhecimento entre as pessoas do time.

### Conclusão
As pessoas esperam que possamos desenvolver softwares que atendam as necessidades delas hoje, mas que estejam prontas para evoluir. Portanto, temos como responsabilidade planejar o design das nossas aplicações. Para isso, é muito importante dominar os conceitos, princípios, padrões e práticas, que nos auxiliam na construção de bons designs. Por fim, devemos assumir uma postura profissional e ter disciplina para não abrir mão da qualidade do código que escrevemos. Nossas decisões devem contribuir com a qualidade do design do software, pois essa é a única forma de construir um produto que atenda aos clientes, se adapte às mudanças e que seja lucrativo ao longo do seu ciclo de vida.

### Referências:
- Extreme Programming Explained: Embrace Change, Kent Beck
- Clean Code, Uncle Bob
- TDD By Example, Kent Beck
- Refactoring, Improving the design of existing code, Martin Fowler
- Head First Design Patterns, Eric Freeman and Elisabeth Robson
- Practical Object-Oriented Design: An Agile Primer Using Ruby, Sandi Metz