Categorias
Software Architecture Software Development

Responsabilidade Única: limitando o impacto das mudanças

Entenda melhor o princípio que não trata apenas de separar as coisas, mas também de juntá-las quando são relacionadas. Reduza o impacto das alterações!

Ao contrário do que parece ser interpretado pela maioria dos desenvolvedores, este princípio não trata apenas de separar as coisas, mas também de juntar conceitos relacionados.

E não sou eu quem estou dizendo, veja só:

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

Robert “Uncle Bob” Martin

Numa tradução livre, junte as coisas que mudam pelos mesmos motivos, e separe as coisas que mudam por razões diferentes.

Muito bem. Mas surgem as questões…

Quando separar? Quando juntar?

A sacada para entender este princípio está em compreender que a decisão de separar ou juntar é feita com foco nas pessoas que trazem os requisitos.

Em seu post sobre o assunto, Uncle Bob dá o exemplo de uma classe que contém 3 métodos, sendo que cada um deles é requisitado por um diretor diferente:

  • O diretor de operações pede um método que calcula a folha de pagamento
  • O diretor financeiro pede um método que gera um relatório financeiro
  • O diretor de tecnologia pede um método que salva tudo no banco de dados

Um erro grave em qualquer desses métodos pode causar a demissão do diretor responsável. Mas, como está tudo em uma única classe, é fácil estragar o que um método faz enquanto se dá manutenção em outro.

A questão é como evitar que uma pessoa A seja demitida porque houveram modificações no método de outra pessoa B.

A solução sugere a separação em 3 classes, cada uma com seu método. Com isso, os limites ficam mais bem definidos e é mais fácil evitar o problema, porque essa abordagem impede que os métodos compartilhem campos e métodos privados.

Mas é importante entender que Uncle Bob não está sugerindo que você crie uma classe para cada método a partir de agora! Não é assim que funciona.

Entendendo a Responsabilidade Única num e-commerce

Vamos aumentar o escopo do problema para o meu exemplo preferido: o site de e-commerce. Com isso, já conseguimos inclusive entender a Responsabilidade Única no contexto dos micro serviços.

O conceito de “Produto” geralmente é compartilhado por toda a base de código de um e-commerce. Quero dizer, existe uma única entidade Product que é usada em todo o sistema, certo?

namespace Ecommerce
{
    public class Product
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public int Stock { get; set; }
        public Uri MainImage { get; set; }

        // demais propriedades seguem...
    }
}

Mas se você pensar bem, o produto tem significados diferentes em cada “departamento” de um e-commerce. No catálogo (busca), o produto é algo com uma descrição e fotos. No checkout, ele é algo com um nome e um preço. Já no estoque, ele é algo com um código de barras e uma quantidade.

Agora, pense na Netshoes ou qualquer e-commerce de médio a grande porte. As pessoas ou os departamentos que ditam os requisitos do sistema são diferentes. O pessoal do marketing (catálogo) se preocupa com coisas bem diferentes do pessoal do faturamento (checkout).

Como aplicar o princípio da Responsabilidade Única de forma que alterações nos produtos no catálogo não tenham chances de impactar o funcionamento do checkout?

O mais interessante é definir um módulo para cada uma dessas áreas. Um módulo pode ser um namespace, mas idealmente seria uma DLL (ou JAR, ou GEM). Ou seja, cada módulo é completo, contendo desde a interface de usuário até o banco de dados. Um módulo é fisicamente separado dos demais, e tem tudo o que precisa para funcionar de forma isolada.

namespace Ecommerce.CatalogModule
{
    public class Product
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public Uri MainImage { get; set; }
    }
}

namespace Ecommerce.CartModule
{
    public class Product
    {
        public Guid Id { get; set; }
        public decimal Price { get; set; }
    }
}

namespace Ecommerce.CheckoutModule
{
    public class Product
    {
        public Guid Id { get; set; }
        public decimal Price { get; set; }
    }
}

Dessa forma, você teria várias classes Product, uma em cada módulo. Todas elas compartilham o mesmo Id, pois afinal de contas em todas elas o produto é o mesmo cara. Mas em cada módulo, além do Id o produto teria propriedades e métodos diferentes, próprios daquele módulo.

Repare algo que deixei de propósito: Os módulos de Carrinho e Checkout apresentam, inicialmente, um Produto com as mesmas propriedades. Mas aqui temos coisas importantes para entender:

  1. Não é porque duas coisas têm a mesma estrutura que elas são a mesma coisa. Como venho dizendo, o produto tem significados diferentes em cada contexto. Sendo assim, as demandas de cada área vão puxar suas definições de produto para caminhos diferentes. Entender isso e separá-los evita que produto se torne uma daquelas classes gigantes (God Object), com 50 propriedades e vários métodos com responsabilidades de negócio bem distintas.
  2. Aqui eu estou usando um modelo simplificado que só tem dados (propriedades), mas a Orientação a Objetos vem justamente para viabilizar o encapsulamento, o que significa que a classe Product deve ter métodos que representam as operações que se pode fazer com produto. E aqui sim, com certeza teremos diferenças desde o início.

Além disso, cada módulo teria seus repositórios, DTOs, classes de serviço e tudo mais, podendo inclusive ter sua própria camada de apresentação!

namespace Ecommerce.CatalogModule
{
    public class Product { ... }
    public interface SearchService { ... }
    public interface ICatalogRepository { ... }
    public interface ICatalogQueries { ... }
    public class SearchResultsDto { ... }
}

namespace Ecommerce.CartModule
{
    public class Cart { ... }
    public class Product { ... }
    public interface ICartRepository { ... }
    public interface ICartQueries { ... }
}

namespace Ecommerce.CheckoutModule
{
    public class Order { ... }
    public class Product { ... }
    public class Customer { ... }
    public interface ICheckoutRepository { ... }
    public interface ICheckoutQueries { ... }
}

O exemplo acima é simplista. Cada módulo teria bem mais classes que isso. Mas cada módulo engloba uma área do negócio do e-commerce. As coisas que mudam juntas estão juntas, e as coisas que mudam separadamente estão separadas.

Então aqui fica claro que a Responsabilidade Única vai além de métodos e classes: Ele vai para o nível das camadas do software, impactando em toda a sua arquitetura.

Se isso parece maluquice, saiba que não é, pois é a realidade dos micro serviços. Cada módulo é um serviço que geralmente é desenvolvido por um time diferente. Esses módulos podem, inclusive, ser desenvolvidos com linguagens e tecnologias diferentes! Tamanho é o nível de separação.

Outro ponto importante de se notar neste exemplo é que a divisão de namespaces (ou de pastas no projeto) não se dá por questões técnicas. Dentro de um módulo, eu não separo as classes em pastinhas como “Database”, “Services”, “Entities”, porque essas seriam separações da perspectiva técnica. Mas a ideia aqui é manter juntas, no mesmo nível, as classes que pertencem à mesma área de negócio. Se houver separação, tem que ser porque o negócio apresenta a mesma separação em suas áreas e seus processos.


Se quiser se aprofundar neste assunto e já conhece o DDD (Domain-Driven Design), recomendo esta excelente palestra do Nick Tune:

A Responsabilidade Única limita o impacto das mudanças

Esta é a essência do princípio. Quando ele é bem aplicado, você traça uma linha mais bem-definida entre os módulos do seu sistema, o que te permite focar as mudanças somente nas partes relacionadas ao requisito. A mudança é cirúrgica.

Repare que essa linha de raciocínio te leva a quebrar o que seria uma única classe em diversas classes espalhadas por vários módulos, mas também te leva a aproximar classes relacionadas ao mesmo conceito, sejam elas entidades, serviços ou infraestrutura. Você tem um código mais desacoplado e coeso.

Assim, quando o departamento de marketing vem requisitar alterações na página de anúncio do produto, não tem como impactar acidentalmente os demais módulos. Eles são fisicamente separados.

Da mesma forma, uma mudança ou nova implementação pode requerer mudanças nas classes de serviço e infraestrutura envolvidas. Geralmente acontece mesmo, porque dificilmente você tem um requisito que só te peça pra mudar uma constante num cálculo, e mais nada. Se você tiver que adicionar um campo, esse campo virá acompanhado de regras e de persistência, possivelmente de gatilhos que ativam regras em outro lugar. Mas tudo bem, porque agora as mudanças acontecem em diversas classes dentro do mesmo módulo, fisicamente separado dos demais.

O contrário disso, que infelizmente vemos no dia-a-dia de tantos projetos, seria o que Martin Fowler chama de Shotgun Surgery. Devido à má separação de responsabilidades, temos projetos com muito código duplicado, que requerem mais tempo para desenvolver funcionalidades pequenas, e o sistema como um todo é difícil de manter.

Técnicas que ajudam a aplicar a Responsabilidade Única

Como aplicar o princípio da Responsabilidade Única em código? Bom, além das dicas que já vimos acima, aqui estão algumas técnicas que ajudam muito na hora de desacoplar as coisas, o que vai de encontro a tudo o que falamos aqui:

Domain Events

Esta técnica é simples de aplicar, mas é muito eficiente na separação de responsabilidades.

Imagine que um cliente do e-commerce atingiu mil reais em compra, foi promovido a Premium, e você precisa avisá-lo disso por email.

Numa abordagem acoplada, o código que avalia o total de compras do cliente estaria junto ao código do checkout. Também o código do envio de email.

Com o Domain Events, você dispara um evento após a compra, algo como NewOrderPlaced. Um handler captura este evento para identificar se o cliente deve ser promovido, promove o cliente e lança um novo evento ClientPromotedToPremium. Por fim, um segundo handler captura este evento e envia o email.

Nesta abordagem, o fechamento da compra e o handler de promoção de clientes fazem parte da camada de domínio (negócio). Já o handler de envio de emails fica em outra camada, visto que comunicação com um servidor de SMTP é infraestrutura.

Veja a solução detalhada aqui:

CQRS

O próprio nome da técnica tem muito a ver com a Responsabilidade Única. CQRS é a sigla para Command and Query Responsibility Segregation, que consiste basicamente na separação entre operações que lêem dados das operações que escrevem dados.

A maior parte dos sistemas tem desproporcionalmente mais consultas que escritas. Ao abrir qualquer página, lá se vão algumas consultas. Possivelmente você só terá alguma escrita se o usuário alterar alguma coisa.

Se é assim, por que é que colocamos consultas e escritas num mesmo “pacotão”? O que faz mais sentido é otimizar cada caso.

Em breve teremos um post detalhando o CQRS aqui no blog. Por enquanto, recomendo a leitura aqui.

Conclusão

A definição do princípio da Responsabilidade Única diz que uma classe deve ter uma única razão para mudar, e essa razão são as pessoas para quem construímos o código. Se você entende isso e conhece profundamente as necessidades de negócio que está atendendo, fica muito mais fácil criar classes com um significado claro e direto.

Qualquer alteração nos requisitos de negócio para um sistema vão naturalmente demandar alterações no código em diversos locais. Mas, se o sistema estiver estruturado segundo o princípio de “agrupar as coisas que mudam pelas mesmas razões”, você poderá minimizar o número de classes que deverão mudar.

Por Phillippe Santana

Apaixonado por escrever código que as pessoas possam entender. Sou um desenvolvedor de software, gerente de projetos, empreendedor e entusiasta de pessoas/cultura. Me adicione no [Linkedin](https://www.linkedin.com/in/phillippesantana/) e no [Medium](https://medium.com/@phillippesantana).