Salvando um projeto Flask com TDD, SOLID e Arquitetura de Software
Link do projeto
Olha o contexto
Nas últimas semanas, tenho estudado um pouco sobre TDD e como os princípios SOLID e Arquitetura de Software ajudam no desenvolvimento, prevenindo que problemas maiores sejam criados e se espalhem, gerando efeitos colaterais dentro das aplicações. Com isso, decidi pegar uma aplicação antiga e aplicar esses conceitos, de forma que conseguisse consolidar ainda mais esses conhecimentos.
A linha de chegada como o início
O projeto, por si só, não possui grande complexidade em seu uso ou construção. Foi desenvolvido com Flask no back-end e utiliza templates no front-end para fornecer dados estáticos e seguros para o cliente. Esse tipo de arquitetura pode ser conhecida como uma abordagem mais tradicional, na qual processamos todos os dados no back-end para fornecer ao usuário páginas estáticas. Nelas, o usuário não possui interatividade dinâmica direta com o sistema, mas sim com as ferramentas que proporcionamos para que ele compreenda o funcionamento do sistema de forma fluida.
Do lado do cliente, decidi usar Bootstrap para facilitar a estilização e JavaScript para adicionar interatividade às funcionalidades. Entre as features desenvolvidas, temos um editor de texto que utiliza Markdown, uma to-do list interativa, um dashboard de análise para demonstrar estatísticas do usuário e um temporizador.
Por outro lado, os conceitos de TDD e SOLID podem ser usados para facilitar o desenvolvimento de software sustentável, permitindo que a refatoração e a evolução do código ocorram sem traumas.
TDD
Test Driven Development (Desenvolvimento Guiado por Testes) é um conceito em que usamos os testes para guiar o desenvolvimento e verificar se o que construímos funciona conforme estipulado, ou se bugs e outros efeitos colaterais surgiram. Ele também garante que, em refatorações — quando precisamos modificar a estrutura de um elemento mantendo o mesmo resultado —, nenhum efeito colateral imediato seja gerado.
Existem muitos mitos em volta do TDD; algumas pessoas dizem que essa metodologia atrasa o projeto pela necessidade de escrever mais código. Entretanto, todos conhecemos casos em que, ao detectar um bug, passamos horas tentando solucioná-lo e, ao conseguir, acabamos criando outros dois. Isso ocorre justamente pela falta de testes automatizados.
O TDD — como dito pelo conhecido Fabio Akita — funciona como um seguro. Entendemos o problema, reproduzimos o erro, solucionamos o caso e documentamos a solução por meio do teste. Caso o problema volte a ocorrer, o teste indicará imediatamente a sua presença, impedindo que código vulnerável ou quebrado suba para o ambiente de produção.
SOLID
Assim como o TDD traz segurança, os princípios SOLID oferecem um desenvolvimento mais limpo e ágil, evitando redundâncias e códigos excessivamente verbosos. Esses princípios são pensados para aplicações que utilizam o paradigma de Orientação a Objetos.
A sigla corresponde aos seguintes princípios:
- S → Princípio da Responsabilidade Única (Single Responsibility Principle)
- O → Princípio Aberto/Fechado (Open/Closed Principle)
- L → Princípio de Substituição de Liskov (Liskov Substitution Principle)
- I → Princípio de Segregação de Interface (Interface Segregation Principle)
- D → Princípio da Inversão de Dependência (Dependency Inversion Principle)
O que isso tem a ver com o projeto?
Tudo. Quando dei início ao projeto, não tinha ideia do que era TDD ou como o SOLID facilitaria minha vida. Era comum ver no meu código padrões de iniciante, como a falta de padronização e a repetição exagerada de processos. Um exemplo pode ser visto abaixo:
Com tudo isso em mente, pude reestruturar meu projeto com o seguinte entendimento dele:
├── forms
│ ├── __init__.py
│ ├── login_form.py
│ └── signin_form.py
├── interfaces
│ ├── auth_service_interface.py
│ ├── note_service_interface.py
│ ├── task_service_interface.py
│ └── user_service_interface.py
├── models
│ ├── __init__.py
│ ├── note_model.py
│ ├── task_model.py
│ └── user_model.py
├── repositories
│ ├── note_repository.py
│ ├── task_repository.py
│ └── user_repository.py
├── resources
│ ├── api_models.py
│ ├── __init__.py
│ ├── note_resource.py
│ ├── todo_resource.py
│ └── user_resource.py
├── services
│ ├── auth_service.py
│ ├── note_service.py
│ ├── task_service.py
│ └── user_service.py
├── templates
│ └── pages
├── utils
│ ├── api.py
│ ├── erros.py
│ └── security.py
└── views
├── admin.py
├── auth.py
├── home.py
└── __init__.py
Embora pareça uma rota comum na comunidade Flask, essa visualização apresenta problemas de acoplamento, conforme detalhado a seguir:
Apontando Problemas
Há um grande acoplamento: partes do sistema que não deveriam se comunicar diretamente estão presentes na mesma função, dificultando a manutenção e criando dependências rígidas. Nesta rota, estamos:
- Executando uma query diretamente no banco de dados;
- Criando um usuário manualmente (sem tratamento de erros adequado);
- Autenticando o usuário;
- Disparando mensagens de feedback.
Embasando a Prática na Teoria
Com o tempo, adquiri conhecimentos sobre arquitetura e engenharia de software. Aprendi padrões que ajudam a organizar o projeto de maneira estruturada:
- Separação de Responsabilidades: Cada trecho de código deve ter uma função clara.
- Testabilidade: Com o TDD, o teste é feito primeiro; a implementação só ocorre para fazer o teste passar.
- Depender para dentro, não para fora: O núcleo do código (regras de negócio) deve ser puro e não depender de ferramentas externas ou frameworks.
── conftest.py
├── e2e
│ └── test_login_e2e.py
├── integration
│ ├── home_page_test.py
│ ├── __init__.py
│ └── test_home_page_view.py
└── unit
├── app_test.py
├── __init__.py
├── interfaces
│ ├── __init__.py
│ ├── test_note_service_interface.py
│ └── test_user_service_interface.py
├── models
│ ├── __init__.py
│ ├── test_note_model.py
│ ├── test_task_model.py
│ └── test_user_model.py
├── repositories
│ ├── __init__.py
│ ├── test_note_repository.py
│ ├── test_task_repository.py
│ └── test_user_repository.py
├── services
│ ├── __init__.py
│ ├── test_auth_service.py
│ ├── test_note_service.py
│ ├── test_task_service.py
│ └── test_user_service.py
├── test_config.py
└── test_utils.py
Esta organização permitiu separar as responsabilidades e organizar as rotinas de testes em unitários, de integração e ponta a ponta (E2E).
Por que se preocupar com isso?
Reusabilidade, manutenibilidade e clareza. Daqui a seis meses, ao reabrir o projeto, será fácil entender cada parte:
- Models: Representação das entidades e do esquema do banco de dados.
- Repositories: Lógica de persistência e acesso a dados.
- Services: Implementação das regras de negócio e orquestração.
- Views/Resources: Comunicação com o usuário ou front-end (Páginas ou API).
- Interfaces: Definição de contratos e assinaturas de métodos.
E com isso, necessidades futuras -- como a adição de uma API que funcione lado-a-lado com esse sistema -- se tornam mais fácil, pois tudo já existe e já está implementado,a questão é apenas o re-uso, e adição das novas necessidades, como em: