Executando verificação de segurança...
1

Salvando um projeto Flask com TDD, SOLID e Arquitetura de Software

https://github.com/VitorMours/NeuroFlow

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:

  1. Executando uma query diretamente no banco de dados;
  2. Criando um usuário manualmente (sem tratamento de erros adequado);
  3. Autenticando o usuário;
  4. 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:

Imagem da Api

Carregando publicação patrocinada...