Projeto para treinar princípios de SOLID e Clean Architecture

Já vos falei uma vez sobre arquitetura de software e a importância que ela tem no software.

Desafiei desde o início das minhas férias da faculdade a desenvolver algo para treinar as minhas competências na parte de backend e testes unitários. Hoje quero falar um bocado sobre SOLID e Clean Architecture e as demais companhias que vão ter ao mesmo resultado, como o Ports and Adapters.

Antes de falar de Clean Architecture, quero vos falar de SOLID porque o SOLID está praticamente incluso na Clean Architecture.

SOLID é um design pattern que define princípios de como modelar as funções e as classes. A realidade é que as pessoas não olham muito para isto quando estão a programar e isto causa depois na vossa aplicação problemas de acoplamento com frameworks, bases de dados e etc. E a vossa aplicação torna-se difícil de manter ou substituir algum componente.

O SOLID é um acrónimo de 5 letras que contêm um significado específico:

  • S:(Single Responsability Principle): a classe ou função deve fazer apenas uma coisa. Ex: middleware express chama base de dados e executa regra de negócio e retorna resposta HTTP.
  • O:(Open-Closed Principle): as classes estão fechadas a modificação e abertas a expansão. Isto é, devem sempre que podem utilizar herança/polimorfismo. Ex:
    BankAccount com uma função deposit normal e PremiumBankAccount que herda BankAccount com função deposit em que em cada depósito paga uma taxa...(isto na realidade não acontece mas serve de exemplo)
  • L:(Liskov Substituition Principle): no fundo objetos acima devem poder ser substituidos pelas subclasses sem quebrar a aplicação.
  • I: (Interface Segregation Principle): neste caso vocês definem uma interface genérica e depois podem utilizar qualquer classe que implemente essa interface: Ex: IBankAccount com método deposit. CurrentBankAccount implementa IBankAccount e PremiumBankAccount implementa IBankAccount e deposit faz outra coisa
  • D:(Dependency Inversion): significa que em vez de instanciar diretamente a classe na função ou método vocês passam uma instância da classe. Recomendado que se use uma interface neste princípio. Ex: IConnection com uma função connect. MongoDBConnection implementa IConnection. LoginUseCase tem um construtor que recebe IConnection

Com isto vocês conseguem desacoplar camadas tornando-as independentes.

Tinha vos falado do projeto que comecei a desenvolver no início das férias e agora vou vos falar um bocado do projeto.

Como ainda só estou no início da minha carreira como programador, quis aprimorar as minhas habilidades de backend e então falei com o meu amigo ChatGPT para me dar algumas ideias de projeto a desenvolver e ele recomendou-me uma To-do API.

Pedi a ele para me dar requisitos de software para utilizar algumas técnicas de design de software. Bem, isto é uma brincadeira simples, mas é algo que pode ser útil para aprender design de software. Hoje vocês vêm muitos vídeos de programação no YouTube que pouco ou nada se preocupam com código limpo. Há uns canais pelo menos no Brasil que procuram ensinar design de software e dou-vos dois exemplos: a Rocketseat e a Full Cycle.

Estes canais preocupam-se em ensinar SOLID e design patterns pelo motivo que eu vos recomendo a assistir às lives deles.

Foi graças a eles que eu estou a mudar muito a minha visão de ver código. Já assistia estes vídeos ainda na universidade e procurava que a minha equipa dos projetos aplicasse pelo menos SOLID, mas mesmo assim eu considerava o código muito acoplado a frameworks e tudo. Sinceramente eu queixava muito que o código estava difícil de ler e aqui está a realidade. Ele estava mesmo.

A minha equipa aprendeu SOLID numa disciplina de Programação Orientada a Objetos e a questão é que nos projetos que eu trabalhei nunca se aplicou SOLID. Eu procurava sempre que possível aplicar mas acabavam sempre por fazer copy paste das funções e acoplar tudo à mesma função.

Porquê que isto acontece ?

Sinceramente, não há motivo para eles não utilizarem SOLID. E como vos falei no post sobre trabalho em equipa, algumas coisas que eu falei como a falta de compromisso levam a estas brigas desnecessárias. E como falei, se o outro falha, a responsabilidade do sucesso do trabalho é minha. Portanto, eu sou o que está no controlo e por isso tento usar SOLID. Se o outro falha, não tem que se vir queixar depois que não percebe o código. O trabalho de casa tem de ser feito. Como costumam dizer, só faz falta quem está presente.

Uma coisa muito importante que eu também vi a acontecer é não escrever testes unitários. E sim, vi a escrever testes E2E, mas testes unitários as equipas que trabalhei não faziam. Porquê ?

Provavelmente, porque dava muito trabalho ou não tinham conhecimentos suficientes. Mas vejamos uma coisa, uma disciplina de projeto é uma disciplina de investigação. E vamos supor que estava a desenvolver um projeto para uma empresa e tinha que desenvolver testes e eu não sabia desenvolver. Primeiro ponto, o patrão não me ia pôr com testes unitários se não soubesse. Provavelmente seria outra função e segundo ponto, talvez iria ter uma formação com testes unitários onde aprenderia a fazer mocks spys e stubs. No contexto académico, cabe a equipa investigar e aplicar um design de código limpo.

Isso iria permitir desenvolver testes unitários.

Vejamos a diferença entre teste unitário e teste E2E. Um teste E2E pode ser um teste do Postman ou usando uma framework de cliente HTTP.

Um teste de integração é a forma como os componentes da aplicação se comportam entre eles. E os testes unitários testam uma unidade de código independente.

Aprendendo SOLID, fica fácil fazer testes de unidade e até mesmo testar rotas sem necessidade de um servidor HTTP. O quê ? Como assim ?

Sim isto é possível, mas se for testes unitários, provavelmente para as rotas utilizariam um mock um spy ou um stub porque o que interessa aqui é verificar se retorna 200 404 400 e verificar se ele retorna o que é pretendido.

Testes unitários têm de ser executados muito rápido e se demora mais do que 5 segundos, vocês fizeram alguma coisa mal. E isto pode acontecer, por exemplo se ele estiver a fazer algum acesso a uma base de dados. Neste caso o código está acoplado a base de dados e num teste de unidade não é necessário a base de dados. Vocês ou terão de utilizar uma base de dados em memória ou podem utilizar uma base de dados falsa. Isto é, um array de objetos.

Depois de aprender, fica divertido escrever testes.

Neste projeto que eu fiz, comecei utilizando um pouco de DDD (Domain Driven Design) para modelar a camada central da aplicação e a escrever testes de unidade. De seguida comecei a escrever a camada de casos de uso obedecendo sempre ao S do SOLID. Cada caso de uso uma função.

De seguida comecei a escrever a camada de infraestrutura. Aqui vocês implementam as frameworks. Não esquecer de utilizar adaptadores. Ex: ExpressAdapter

Mas podem abstrair ainda mais. Vocês podem criar controladores desacoplados do express e disso. Como ?

Vou dar um exemplo de um controlador que escreva.

Observem esta interface em Typescript:

Esta interface é um contrato para definir um controller(linhas 6-8).Nas linhas 1-4 está definido uma interface que é uma resposta HTTP. A função handle (linha 7) retorna uma Promise de HttpResponse ou seja uma função assíncrona.

Imaginemos que na minha API tem uma função para registar-me como utilizador:

Observem agora esta implementação:

 

A classe RegisterUserController recebe a abstração de um caso de uso de criação de utilizador(CreateUserUseCase). O construtor não sabe qual é a implementação do caso de uso a única coisa que ele sabe é que o caso de uso tem uma função execute.

Uma coisa uma interessante que eu também decidi experimentar foi o functional error handling. O caso de uso não me retorna um user mas sim um resultado que pode ser um erro ou o resultado pretendido.

Se você investigarem na Internet vocês vão encontrar uma implementação do functional error handling para vossa linguagem de programação. No caso do typescript existe várias formas. Neste caso, eu pedi ao ChatGPT para me gerar um código de functional error handling. E acreditem numa coisa, aprendi a fazer sozinho e agora nem preciso do ChatGPT.

Uma coisa que é muito importante falando agora de ferramentas como o ChatGPT, é a documentação. Para mim o ChatGPT é a minha documentação neste momento, mas devem fazer comentários e documentar o vosso código. 

E como podem ver o controller retorna BadRequest, InternalServerError e OK. Aqui entra a questão do Interface Segregation.

Vou só dar um exemplo de resposta Http.

Observem esta classe:

 

Reparem numa coisa, eu coloquei no construtor um Error e depois ele coloca o erro certo e lá na mensagem o motivo de erro e o tipo de erro.

E no controller retorno esta classe e entre outras.

Pergunta que eu vos faço. Eu falei no meu código do Express ? Nunca falei em Express.

Este controller como está desacoplado dá para fazer testes unitários a ele com os mocks. E isto segue principios de Clean Architecture e SOLID.

Em suma, abordei aqui neste post num projeto que parece tão simples que é só meter um servidor uma base de dados e escrever rotas como é possível usar um design de código limpo. Isto é muito importante. É uma pena é pouca gente seguir princípios de código limpo, mas isto no futuro faz muita diferença e muda completamente a nossa forma de escrever código.