TL;DR
every module, class or function in a computer program should have responsibility over a single part of that program's functionality.
Em outras palavras, cada função, arquivo, componente ou classe da sua aplicação, deve ser responsável por apenas, e unicamente, UMA funcionalidade do seu programa.
Para começar...
Primeiramente, muito prazer me chamo José Bezerra, dev fullstack desde que me entendo por gente, fundador&CTO das falecidas Propi e Vestibulando e hoje eu vou compartilhar com você uma dica que transformou a minha forma de pensar software.
Antes de qualquer coisa, pra melhorar a qualidade do teu código e subir o nível dos teus jobs me segue lá no instagram @josebezerra12 pra acompanhar as dicas que saem.
Todos nós, em algum momento da nossa carreira, escrevemos um servidor ou aplicativo achando que estava MEGA organizado (só na nossa cabeça) e a medida que as funcionalidades cresciam, o projeto ficava cada vez mais impossível de se manter organizado. Parece que pra cada bug resolvido, 2 outros outros aparecem. 😂
Pra resolver esse tipo problema, código acoplado, confuso, difícil de entender e manter existe o famoso S.O.L.I.D. Sigla que postula cinco princípios para escrever um código desacoplado, manutenível e mais robusto.
S — Single responsibility principle
O — Open closed principle
L — Liskov substitution principle
I — Interface segregation principle
D — Dependency Inversion principle
Hoje nós não falaremos sobre tudo, mas sim como aplicar de forma prática o principio que eu acredito ser o mais importante, o S do SOLID, o Princípio da Responsabilidade Única. Seguindo este entendimento, em muito dos casos ja é o suficiente para você subir o nível dos seus projetos.
Princípio da responsabilidade única
every module, class or function in a computer program should have responsibility over a single part of that program's functionality.
Em outras palavras, cada função, arquivo, componente ou classe da sua aplicação, deve ser responsável por apenas, e unicamente, UMA funcionalidade do seu programa.
Explorando o problema
Como um código vale mais que mil palavras, aqui está um exemplo de um servidor em express a não ser seguido:
const LISTEN_PORT = 3333;
const app = express();
app.get('/user/:id', async (request, response) => {
const { id } = request.params;
const user = await User.findOne({ id })
if (!user) {
throw new Error('Usuário não existente.')
}
return response.status(200).json({ user });
});
app.listen(LISTEN_PORT, () => {
console.log('🚀 Server started on port 3333');
});
Repare que estou utilizando o express para criar as rotas de nosso servidor, e o mongoose para buscar um usuário em nosso banco de dados (Linha 6).
Para começar a destrinchar esse princípio, aviso que podemos aplicá-lo em diferentes profundidades, ou seja, até qual nível faz sentido refatorar seu código é relativo a sua necessidade.
Indo direto ao ponto, perceba que em um mesmo arquivo, possuímos diferentes responsabilidades da aplicação:
- Em primeiro lugar, a instância e configurações de servidor.
- Em segundo lugar, todas as rotas da aplicação.
- Em terceiro lugar, as regras de negócio de nossa rota.
Mas qual é o real problema?
O problema é que aqui temos um belo exemplo de código extremamente acoplado. Semanticamente, nós não conseguimos saber onde começa uma parte de nosso software e acaba outra. Uma outra forma de enxergar isso é se perguntar:
- qual arquivo altero a porta de nosso servidor? server.js
- qual arquivo altero o nome de minha rota? server.js
- qual arquivo mudo a query feita pelo banco? server.js
- qual arquivo acrescento uma regra de negócio na rota? server.js
Respondido? Aqui temos um problema... não podemos atribuir tantas responsabilidades a apenas uma camada em nossa aplicação, apenas nesse trechinho de código, a longo prazo caso as configurações de nosso servidor ou as regras de negócio mudem, ou a quantidade de rotas cresça, pelo forte casamento de todas essas partes, teremos um código com um alto custo para ser alterado, com grande potencial para bugs e difícil correção para erros.
Por exemplo, se pensarmos em fazer rotas de CRUD para os usuários e outro para agendamentos, ja teremos pelo menos 8 rotas, de diferentes áreas de nosso sistema, com diferentes regras de negócios, no mesmo lugar. O código não possui semântica, ele não se auto-explica.
Aplicando o princípio da responsabilidade única
Para resolver, dividiremos o nosso código utilizando uma pequena "arquitetura", com o fim de separar cada funcionalidade em uma camada diferente.
- A instância de nosso servidor ficará em server.js
- As rotas de nossa aplicação ficará em /routes/user.routes.js
- As nossas regras de negócio ficarão em /services/GetUserService.js
Sem muita enrolação, vamos ao código!
A camada do servidor
import express from 'express';
import userRoute from './user.routes';
const LISTEN_PORT = 3333;
const app = express();
app.use(express.json());
app.use(userRoute);
app.listen(LISTEN_PORT, () => {
console.log('🚀 Server started on port 3333');
});
Aqui temos a camada de nosso servidor, propriamente dito. Todas as configurações de nosso servidor está isolado e desacoplado do resto, chamamos o express, fazemos as suas configurações de uso, json(), as rotas e instanciamos o nosso servidor.
A camada das rotas
import { Router } from 'express';
import GetUserService from '../services/GetUserService';
const userRouter = Router();
userRouter.get('/user/:id', async (request, response) => {
const { id } = request.params;
const getUser = new GetUserService();
const user = getUser.execute({ id });
return response.status(200).json({ user });
});
export default userRouter;
Aqui, a diferença pode ser sutil, mas extremamente poderosa. A nossa camada de rotas está responsável apenas por receber o request nos endpoints, repassar os dados do request para a camada lógica (logo abaixo) e responder este request com o respectivo response.
Repare que toda a funcionalidade de capturar e processar as entradas do request, e responder o request que foi processado fica nessa camada.
A camada das regras de negócio
class GetUserService {
public async execute({ id }) {
const user = await User.findOne({ id });
if (!user) {
throw new Error('Usuário não existente.')
}
return user;
}
}
export default GetUserService;
Por último, finalmente temos as nossas regras de negócio isoladas em uma classe, uma classe para cada "ação" do nosso sistema, ou como estamos chamando aqui, serviço.
Normalmente cada endpoint de nosso servidor fará uso de pelo menos 1 serviço, onde cada serviço isola a lógica de negócio do "pedido" feito pelo request. Neste exemplo, estamos chamando de serviço o ato de "pegar" de um usuário no banco.
Exemplo de outros serviços seriam, UpdateUserService, ChangePasswordUserService, BookAppointmentService, enfim, isso é papo para outro artigo. Posteriormente posso falar mais sobre Service-Oriented-Architecture se vocês quiserem.
Pensamentos finais
Claro, tudo que fizemos aqui depende do tamanho e o propósito do projeto que você está escrevendo. Cada caso é um caso e não adianta escrever o melhor código do mundo para um projeto que com poucas funcionalidades que possivelmente irá morrer em breve.
Lembre-se que cada alteração tem o propósito de tornar o código cada vez mais desacoplado, claro e coeso. Este princípio pode ser aplicado de diferentes formas, lembre-se que o que mostrei é a aplicação prática do Single Responsibility Principle, e não uma arquitetura específica. O exemplo que trouxe é pra deixar claro uma maneira de aplicar esta poderosa filosofia na prática.
Para mais informações, você pode ler estes artigos:
- https://medium.com/@cramirez92/s-o-l-i-d-the-first-5-priciples-of-object-oriented-design-with-javascript-790f6ac9b9fa
- https://blog.logrocket.com/solid-principles-single-responsibility-in-javascript-frameworks/#whatarethesolidprinciples
- https://en.wikipedia.org/wiki/Single-responsibility_principle
Ou fala comigo que vai ser mais fácil.
Dúvidas? Feedbacks? Sugestões? me manda la no @josebezerra12. 🚀
Top comments (0)