Princípio da Responsabilidade Única e as implicações na Orientação a Objetos e Arquitetura de Software

Victor Lima Reboredo - Sep 3 - - Dev Community

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

. . . .
Terabox Video Player