Princípio da Responsabilidade Única: Uma Receita para o Grande Código
Publicados: 2022-03-11Independentemente do que consideramos um ótimo código, ele sempre exige uma qualidade simples: o código deve ser sustentável. Recuo adequado, nomes de variáveis nítidos, 100% de cobertura de teste e assim por diante só podem levá-lo até certo ponto. Qualquer código que não seja sustentável e não possa se adaptar às mudanças de requisitos com relativa facilidade é um código apenas esperando para se tornar obsoleto. Podemos não precisar escrever um ótimo código quando estamos tentando construir um protótipo, uma prova de conceito ou um produto mínimo viável, mas em todos os outros casos devemos sempre escrever um código que seja sustentável. Isso é algo que deve ser considerado uma qualidade fundamental da engenharia e design de software.
Neste artigo, discutirei como o Princípio da Responsabilidade Única e algumas técnicas que giram em torno dele podem dar ao seu código essa qualidade. Escrever um ótimo código é uma arte, mas alguns princípios sempre podem ajudar a dar ao seu trabalho de desenvolvimento a direção necessária para produzir software robusto e de fácil manutenção.
Modelo é tudo
Quase todos os livros sobre algum novo framework MVC (MVP, MVVM ou outro M**) estão repletos de exemplos de código ruim. Esses exemplos tentam mostrar o que o framework tem a oferecer. Mas eles também acabam dando maus conselhos para iniciantes. Exemplos como “digamos que temos este ORM X para nossos modelos, o mecanismo de modelagem Y para nossas visualizações e teremos controladores para gerenciar tudo” não conseguem nada além de controladores gigantescos.
Embora em defesa desses livros, os exemplos pretendem demonstrar a facilidade com que você pode começar com sua estrutura. Eles não se destinam a ensinar design de software. Mas os leitores que seguem esses exemplos percebem, apenas depois de anos, como é contraproducente ter pedaços monolíticos de código em seu projeto.
Os modelos são o coração do seu aplicativo. Se você tiver modelos separados do restante da lógica de seu aplicativo, a manutenção será muito mais fácil, independentemente de quão complicado seu aplicativo se torne. Mesmo para aplicativos complicados, uma boa implementação de modelo pode resultar em um código extremamente expressivo. E para conseguir isso, comece certificando-se de que seus modelos façam apenas o que devem fazer e não se preocupem com o que o aplicativo construído em torno dele faz. Além disso, ele não se preocupa com o que é a camada de armazenamento de dados subjacente: seu aplicativo depende de um banco de dados SQL ou armazena tudo em arquivos de texto?
À medida que continuarmos este artigo, você perceberá o quanto o código é ótimo sobre a separação de interesses.
Princípio da Responsabilidade Única
Você provavelmente já ouviu falar sobre os princípios SOLID: responsabilidade única, aberto-fechado, substituição de liskov, segregação de interface e inversão de dependência. A primeira letra, S, representa o Princípio de Responsabilidade Única (SRP) e sua importância não pode ser exagerada. Eu até argumentaria que é uma condição necessária e suficiente para um bom código. Na verdade, em qualquer código mal escrito, você sempre pode encontrar uma classe que tenha mais de uma responsabilidade - form1.cs ou index.php contendo alguns milhares de linhas de código não é algo tão raro de se encontrar e todos nós provavelmente já viu ou fez.
Vamos dar uma olhada em um exemplo em C# (ASP.NET MVC e Entity framework). Mesmo que você não seja um desenvolvedor C#, com alguma experiência em OOP você poderá acompanhar facilmente.
public class OrderController { ... public ActionResult CreateForm() { /* * View data preparations */ return View(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } using (var context = new DataContext()) { var order = new Order(); // Create order from request context.Orders.Add(order); // Reserve ordered goods …(Huge logic here)... context.SaveChanges(); //Send email with order details for customer } return RedirectToAction("Index"); } ... (many more methods like Create here) }
Esta é uma classe OrderController usual, seu método Create mostrado. Em controladores como este, muitas vezes vejo casos em que a própria classe Order é usada como parâmetro de solicitação. Mas eu prefiro usar classes de solicitação especial. Mais uma vez, SRP!
Observe no trecho de código acima como o controlador sabe muito sobre “fazer um pedido”, incluindo, mas não limitado a armazenar o objeto Order, enviar e-mails, etc. Isso é simplesmente muitos trabalhos para uma única classe. Para cada pequena alteração, o desenvolvedor precisa alterar todo o código do controlador. E apenas no caso de outro Controller também precisar criar pedidos, na maioria das vezes, os desenvolvedores recorrerão a copiar e colar o código. Os controladores devem controlar apenas o processo geral e não abrigar cada parte da lógica do processo.
Mas hoje é o dia em que paramos de escrever esses controladores gigantescos!
Vamos primeiro extrair toda a lógica de negócios do controlador e movê-la para uma classe OrderService:
public class OrderService { public void Create(OrderCreateRequest request) { // all actions for order creating here } } public class OrderController { public OrderController() { this.service = new OrderService(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } this.service.Create(request); return RedirectToAction("Index"); }
Feito isso, o controlador agora faz apenas o que se destina a fazer: controlar o processo. Ele sabe apenas sobre visualizações, classes OrderService e OrderRequest - o menor conjunto de informações necessário para que ele faça seu trabalho, que é gerenciar solicitações e enviar respostas.
Dessa forma, você raramente alterará o código do controlador. Outros componentes, como visualizações, objetos de solicitação e serviços, ainda podem ser alterados, pois estão vinculados aos requisitos de negócios, mas não aos controladores.
É disso que trata o SRP, e existem muitas técnicas para escrever código que atendem a esse princípio. Um exemplo disso é a injeção de dependência (algo que também é útil para escrever código testável).
Injeção de dependência
É difícil imaginar um grande projeto baseado no Princípio da Responsabilidade Única sem Injeção de Dependência. Vamos dar uma olhada em nossa classe OrderService novamente:
public class OrderService { public void Create(...) { // Creating the order(and let's forget about reserving here, it's not important for following examples) // Sending an email to client with order details var smtp = new SMTP(); // Setting smtp.Host, UserName, Password and other parameters smtp.Send(); } }
Este código funciona, mas não é o ideal. Para entender como funciona a classe OrderService do método create, eles são forçados a entender os meandros do SMTP. E, novamente, copiar e colar é a única saída para replicar esse uso de SMTP sempre que necessário. Mas com um pouco de refatoração, isso pode mudar:
public class OrderService { private SmtpMailer mailer; public OrderService() { this.mailer = new SmtpMailer(); } public void Create(...) { // Creating the order // Sending an email to client with order details this.mailer.Send(...); } } public class SmtpMailer { public void Send(string to, string subject, string body) { // SMTP stuff will be only here } }
Muito melhor já! Mas, a classe OrderService ainda sabe muito sobre o envio de e-mail. Precisa exatamente da classe SmtpMailer para enviar e-mail. E se quisermos mudar isso no futuro? E se quisermos imprimir o conteúdo do e-mail enviado para um arquivo de log especial em vez de enviá-lo em nosso ambiente de desenvolvimento? E se quisermos testar a unidade de nossa classe OrderService? Vamos continuar com a refatoração criando uma interface IMailer:

public interface IMailer { void Send(string to, string subject, string body); }
O SmtpMailer implementará essa interface. Além disso, nosso aplicativo usará um contêiner IoC e podemos configurá-lo para que o IMailer seja implementado pela classe SmtpMailer. OrderService pode ser alterado da seguinte forma:
public sealed class OrderService: IOrderService { private IOrderRepository repository; private IMailer mailer; public OrderService(IOrderRepository repository, IMailer mailer) { this.repository = repository; this.mailer = mailer; } public void Create(...) { var order = new Order(); // fill the Order entity using the full power of our Business Logic(discounts, promotions, etc.) this.repository.Save(order); this.mailer.Send(<orders user email>, <subject>, <body with order details>); } }
Agora estamos chegando a algum lugar! Aproveitei para fazer também outra mudança. O OrderService agora conta com a interface IOrderRepository para interagir com o componente que armazena todos os nossos pedidos. Ele não se importa mais com a forma como essa interface é implementada e qual tecnologia de armazenamento a está alimentando. Agora a classe OrderService tem apenas código que lida com a lógica de negócios do pedido.
Dessa forma, se um testador encontrar algo se comportando incorretamente ao enviar e-mails, o desenvolvedor saberá exatamente onde procurar: classe SmtpMailer. Se algo estava errado com os descontos, o desenvolvedor, novamente, sabe onde procurar: Código de classe OrderService (ou caso você tenha adotado o SRP de cor, então pode ser DiscountService).
Arquitetura orientada a eventos
No entanto, ainda não gosto do método OrderService.Create:
public void Create(...) { var order = new Order(); ... this.repository.Save(order); this.mailer.Send(<orders user email>, <subject>, <body with order details>); }
Enviar um e-mail não faz parte do fluxo principal de criação de pedidos. Mesmo que o aplicativo não envie o e-mail, o pedido ainda será criado corretamente. Além disso, imagine uma situação em que você precisa adicionar uma nova opção na área de configurações do usuário que permite que eles optem por não receber um e-mail após fazer um pedido com sucesso. Para incorporar isso em nossa classe OrderService, precisaremos introduzir uma dependência, IUserParametersService. Adicione localização à mistura e você terá outra dependência, ITranslator (para produzir mensagens de e-mail corretas no idioma de escolha do usuário). Várias dessas ações são desnecessárias, principalmente a ideia de adicionar tantas dependências e acabar com um construtor que não cabe na tela. Encontrei um ótimo exemplo disso na base de código do Magento (um popular CMS de comércio eletrônico escrito em PHP) em uma classe que possui 32 dependências!
Às vezes é difícil descobrir como separar essa lógica, e a classe do Magento provavelmente é vítima de um desses casos. É por isso que eu gosto da maneira orientada a eventos:
namespace <base namespace>.Events { [Serializable] public class OrderCreated { private readonly Order order; public OrderCreated(Order order) { this.order = order; } public Order GetOrder() { return this.order; } } }
Sempre que um pedido é criado, em vez de enviar um e-mail diretamente da classe OrderService, a classe de evento especial OrderCreated é criada e um evento é gerado. Em algum lugar nos manipuladores de eventos do aplicativo serão configurados. Um deles enviará um e-mail para o cliente.
namespace <base namespace>.EventHandlers { public class OrderCreatedEmailSender : IEventHandler<OrderCreated> { public OrderCreatedEmailSender(IMailer, IUserParametersService, ITranslator) { // this class depend on all stuff which it need to send an email. } public void Handle(OrderCreated event) { this.mailer.Send(...); } } }
A classe OrderCreated é marcada como Serializable propositalmente. Podemos tratar esse evento imediatamente ou armazená-lo serializado em uma fila (Redis, ActiveMQ ou qualquer outra coisa) e processá-lo em um processo/thread separado daquele que trata as solicitações da web. Neste artigo, o autor explica em detalhes o que é arquitetura orientada a eventos (não preste atenção à lógica de negócios dentro do OrderController).
Alguns podem argumentar que agora é difícil entender o que está acontecendo quando você cria o pedido. Mas isso não pode estar mais longe da verdade. Se você se sente assim, simplesmente aproveite a funcionalidade do seu IDE. Ao encontrar todos os usos da classe OrderCreated no IDE, podemos ver todas as ações associadas ao evento.
Mas quando devo usar a injeção de dependência e quando devo usar uma abordagem orientada a eventos? Nem sempre é fácil responder a essa pergunta, mas uma regra simples que pode ajudá-lo é usar a injeção de dependência para todas as suas atividades principais dentro do aplicativo e a abordagem orientada a eventos para todas as ações secundárias. Por exemplo, use a injeção de dependência com coisas como criar um pedido dentro da classe OrderService com IOrderRepository e delegar o envio de email, algo que não é uma parte crucial do fluxo de criação de pedido principal, para algum manipulador de eventos.
Conclusão
Começamos com um controlador muito pesado, apenas uma classe, e terminamos com uma elaborada coleção de classes. As vantagens dessas mudanças são bastante evidentes a partir dos exemplos. No entanto, ainda existem muitas maneiras de melhorar esses exemplos. Por exemplo, o método OrderService.Create pode ser movido para uma classe própria: OrderCreator. Como a criação de pedidos é uma unidade independente de lógica de negócios seguindo o Princípio de Responsabilidade Única, é natural que ela tenha sua própria classe com seu próprio conjunto de dependências. Da mesma forma, a remoção e o cancelamento de pedidos podem ser implementados em suas próprias classes.
Quando escrevi código altamente acoplado, algo semelhante ao primeiro exemplo deste artigo, qualquer pequena alteração no requisito poderia facilmente levar a muitas alterações em outras partes do código. O SRP ajuda os desenvolvedores a escrever códigos desacoplados, onde cada classe tem seu próprio trabalho. Se as especificações deste trabalho forem alteradas, o desenvolvedor fará alterações apenas nessa classe específica. É menos provável que a mudança interrompa todo o aplicativo, pois outras classes ainda devem estar fazendo seu trabalho como antes, a menos, é claro, que tenham sido quebradas em primeiro lugar.
Desenvolver código antecipadamente usando essas técnicas e seguindo o Princípio de Responsabilidade Única pode parecer uma tarefa assustadora, mas os esforços certamente serão recompensados à medida que o projeto crescer e o desenvolvimento continuar.