Princípio da Responsabilidade Única e as implicações na Orientação a Objetos e Arquitetura de Software
Victor Lima Reboredo
Posted on September 3, 2024
Introdução
O paradigma orientado a objetos é amplamente difundido no mundo de desenvolvimento de software. Através dele conseguimos simular de maneira eficiente as modularidades de um processo no mundo real, com reaproveitamento de código e aplicando métodos e padrões que tornam o código manutenível, eficiente e mais limpo e direto.
Coesão
Dentre os diversos conceitos aplicados na Orientação a Objetos, um ótimo conceito para se aprofundar é a coesão
. A coesão é um princípio extremamente útil na programação (mesmo se utilizamos outro paradigma ao programar, como o estruturado
, por exemplo). Através da coesão conseguimos organizar o código de maneira a deixá-lo mais limpo e esteticamente bem estruturado.
Em seu livro "Orientação a Objetos: Aprenda seus conceitos e suas aplicabilidades de forma efetiva", o professor Thiago Leite e Carvalho define coesão como um conceito que:
"[...] preconiza que cada unidade de código deve ser responsável somente por possuir informações e executar tarefas que dizem respeito somente ao conceito que ela pretende representar."
Podemos aplicar a coesão através das classes e dos relacionamentos associativos das mesmas. Criar unidades de código coesas com classes e associações contribui para definição de blocos de códigos responsáveis somente por tarefas e conceitos às quais elas se propõem. (Thiago Leite e Carvalho, pg. 23)
Classes coesas são extremamente importantes em sistemas orientados a objetos. São mais fáceis de dar manutenção, são compostas por menos códigos e podem ser mais facilmente reutilizadas no software. Elas também são menos propensas a defeitos e bugs.
Single Responsability Principle
O SRP ou também Princípio da Responsabilidade Única é o primeiro pilar do solid e existe para que possamos aplicar a coesão no nosso código.
Ele nada mais é do que a parte prática de tudo o que vimos até aqui, portanto colocamos em prática a coesão através do SRP. Ele preconiza que:
Uma classe deve ter uma, e apenas uma, razão para mudar.
É um tanto complexo no dia a dia do programador, termos bem definido um grupo de regras que irão de certa forma definir o conceito de uma classe coesa
e de uma classe não-coesa
. Os conceitos que podem rodear isso acabam por ser subjetivos. Acredito que o principal a se verificar em uma análise de classe é quantos métodos diferentes ela tem/quão grande elas são e quantos comportamentos diferentes é possível encontrar nela. Quanto mais métodos, código e comportamentos uma classe tiver, mais ela se tornará uma candidata ao título de "Classe não-coesa".
Exemplo
Neste exemplo, criarei a classe User
. Ela será responsável por salvar um usuário em meu sistema, validar o e-mail e enviar um e-mail de boas-vindas para o usuário.
class User {
constructor(public name: string, public email: string) {}
saveToDatabase() {
console.log(`Saving user ${this.name} to the database...`);
}
sendWelcomeEmail() {
console.log(`Sending welcome email to ${this.email}...`);
}
validateEmail() {
console.log(`Validating email ${this.email}...`);
return this.email.includes("@");
}
}
const user = new User("John Doe", "john.doe@example.com");
if (user.validateEmail()) {
user.saveToDatabase();
user.sendWelcomeEmail();
}
Nossa classe está totalmente errada! Ela tem 3 tarefas totalmente distintas a que deveriam ser independentes. Se alguma lógica de validação do e-mail ou do banco de dados mudar, teremos que modificar a classe inteira, que deveria apenas representar a entidade Usuário. Criar testes unitários para testar a classe se torna uma dor de cabeça, já que devemos ter teste para cada método com lógicas distintas.
Aplicando o SRP...
class User {
constructor(public name: string, public email: string) {}
}
class UserRepository {
save(user: User) {
console.log(`Saving user ${user.name} to the database...`);
}
}
class EmailService {
sendWelcomeEmail(user: User) {
console.log(`Sending welcome email to ${user.email}...`);
}
}
class UserValidator {
validateEmail(user: User): boolean {
console.log(`Validating email ${user.email}...`);
return user.email.includes("@");
}
}
const user = new User("John Doe", "john.doe@example.com");
const validator = new UserValidator();
const repository = new UserRepository();
const emailService = new EmailService();
if (validator.validateEmail(user)) {
repository.save(user);
emailService.sendWelcomeEmail(user);
}
Agora, cada classe tem uma única responsabilidade. User
apenas modela a entidade, UserRepository
lida com o armazenamento no banco de dados, EmailService
cuida do envio de emails, e UserValidator
se concentra na validação.
Desta maneira podemos modularizar melhor qualquer mudança/dependência nas validações, isto é, se a validação mudar basta mudar um local do código. Se for necessário incluir mais campos na entidade de usuário e incluir uma validação para este novo campo, toda a edição no código ficará mais fácil!
Aplicando SRP em outras frentes
Um ponto importantíssimo no mundo de Software é a arquitetura que aplicamos. Pensando nesse princípio, um padrão arquitetural que vem à minha mente é o MVC (Model - View - Controller)
.
Esse modelo de software segrega as responsabilidades de um software monolítico em 3 frentes:
Model: É o domínio de nossa regra de negócio. São classes que representam tabelas de banco de dados ou entidades bem definidas. Através delas podemos gerenciar repositório de dados, fazer consultas e manipular dados.
View: É a camada que define a parte de interface de usuário da aplicação. Nela estarão os elementos gráficos interativos onde o cliente irá requisitar, manipular e deletar dados. É a parte de front.
Controller: É a camada responsável por ligar o
"core"
do software ao cliente (usuário, seja via app mobile, desktop ou web). Ele pode capturar uma requisição - seja via REST, GraphQL, gRPC e muitos outros - e converter tudo isso em uma ação dentro da aplicação.
Existem no mercado diversos frameworks/libs que utilizam o MVC como arquitetura, os principais e mais completos - na minha opinião, é claro - são: Laravel
(PHP), Express
(JavaScript), ASP.Net Core MVC
(C#), Spring
(Java) e Django
(Python).
Conclusão
O Princípio da Responsabilidade Única é muito mais do que uma regra teórica: é uma prática essencial para quem busca escrever um código orientado a objetos robusto e escalável. Ao aplicar o SRP, garantimos que nossas classes se mantenham coesas, focadas em uma única responsabilidade, o que facilita a manutenção, testes e futura evolução do sistema.
A coesão, como discutido, é um dos pilares para alcançar da boa arquitetura de software. Quando uma classe possui uma única responsabilidade, ela se torna mais fácil de entender, de modificar e de estender, permitindo que o código evolua de maneira mais natural conforme as necessidades do projeto mudam. Além disso, a aplicação do SRP reduz a chance de bugs, pois minimiza o impacto das mudanças ao isolar responsabilidades.
Adotar o SRP desde o início de um projeto pode parecer um desafio, mas os benefícios a longo prazo superam em muito o esforço inicial. Projetar classes pequenas e bem definidas contribui para a criação de um sistema modular, onde cada parte pode ser desenvolvida, testada e mantida independentemente. Isso não apenas melhora a qualidade do código, mas também promove uma maior colaboração entre equipes de desenvolvimento, permitindo que diferentes partes do sistema evoluam em paralelo sem interferências indesejadas.
Em suma, o SRP não é apenas um princípio de design, mas uma prática que influencia diretamente a qualidade e a sustentabilidade do software que desenvolvemos. Incorporá-lo em nossa abordagem à programação orientada a objetos é um passo fundamental para construir sistemas que sejam não apenas funcionais, mas também fáceis de manter e adaptar ao longo do tempo.
Posted on September 3, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.