S.O.L.I.D.: Princípio da Responsabilidade Única

Artigos - 22/Jan/2020 - por André Benjamim

Introdução

Em qualquer projeto desenvolvido em times, será necessário em algum momento resolver divergências sobre como solucionar um problema. Por mais que todos concordem com a mesma solução, cada pessoa poderá escrevê-la à sua maneira. Para que o time possa continuamente entregar valor com qualidade, é necessário estabelecer um conjunto de regras e orientações de como o trabalho deve ser executado.

Na programação orientada a objetos, existem padrões que servem de guia para que as pessoas escrevam um código mais compreensível, flexível e fácil de manter. Esse conjunto de 5 princípios foi introduzido por Robert C. Martin, o “Uncle Bob”, em um paper denominado “Design Principles and Design Patterns”. Posteriormente, Michael Feathers cunhou o acrônimo SOLID:

  • Single Responsibility Principle (Responsabilidade Única)
  • Open/Closed Principle (Aberto e Fechado)
  • Liskov Substitution Principle (Substituição de Liskov)
  • Interface Segregation Principle (Segregação de Interface)
  • Dependency Inversion Principle (Inversão de Dependência).

Devs que procuram seguir os princípios do SOLID produzem códigos mais fáceis de serem refatorados e com menos "code smells".

Este é o primeiro de uma série de artigos nos quais descreveremos em detalhes cada um dos princípios do SOLID. Vamos mostrar exemplos de cada um dos princípios utilizando a linguagem de programação Ruby e, para facilitar a compreensão, usaremos somente Plain Old Ruby Object, ou seja, não estaremos trabalhando dentro de nenhum framework específico.

Single Responsibility Principle (SRP)

O Princípio da Responsabilidade Única está definido como:

“Uma classe deve ter somente uma razão para mudar”

Isso quer dizer que uma classe deve ter somente uma responsabilidade, ou seja, deve ser responsável por executar apenas uma tarefa.

Durante o processo de criação de uma aplicação os requerimentos mudam e consequentemente a responsabilidade de pelo menos uma classe pode mudar.

Quanto mais responsabilidades uma classe tiver, mais vezes ela terá que ser alterada e mais acopladas tornam-se as partes da aplicação.

Exemplo

    require 'net/smtp'
    class ProcessUser
      def initialize(params)
        @params = params
      end

      def call
        # Process user params
        user_params = {
          name: "#{params[:user_name]} #{params[:user_surname]}",
          email: params[:user_email]
        }
        # Create User
        user = User.create(user_params)

        # Create a message and sends
        from    = "First Last <person@example.com>"
        to      = "#{user.name} <#{user.email}>"
        subject = "Welcome User"

        message = <<MESSAGE_END
    From: #{from}
    To: #{to}
    Subject: #{subject}
    Mime-Version: 1.0
    Content-Type: text/html
    Content-Disposition: inline

    <b>Welcome email</b><br/>

    We welcome you to our new platform.
    MESSAGE_END

        Net::SMTP.start('localhost', 25) do |smtp|
          smtp.send_message message, from, to
        end
      end
    end

No exemplo acima, a classe ProcessUser possui o método call, que é responsável por formatar os parâmetros do usuário, criar o usuário, configurar o e-mail e enviá-lo, ou seja, essa classe possui muitas responsabilidades.

Por possuir muitas responsabilidades, ela possui muitos motivos para ser alterada. Por exemplo, se decidirmos usar uma gem de envio de e-mail ao invés do Net::SMTP, se o corpo do e-mail mudar, se alterarmos o modo como acessamos os valores dos parâmetros ou se adicionarmos outros atributos ao user_params, precisaremos alterar a classe.

Agora vejamos como isso ficaria quando tentamos extrair cada responsabilidade para uma classe isolada.

Refatoração

    class ProcessUser
      def initialize(params)
        @params = params
      end

      def call
        user = UserFactory.create(params)
        MailerService.new(user).welcome
      end
    end

    class UserFactory
      def self.create(params)
        parameters = UserParamsFormatter.format(params)
        User.create(parameters)
      end
    end

    class UserParamsFormatter
      def self.format(params)
        {
          name: "#{params[:user_name]} #{params[:user_surname]}",
          email: params[user_email]
        }
      end
    end

    class MailerService
      def initialize(recipient)
        @recipient = recipient
      end

      def welcome
        email = WelcomeTemplate.new(@recipient)
        mailer.deliver(@recipient, email)
      end

      def mailer
        Mailer.new
      end
    end

    class WelcomeTemplate
      # template formatted with recipients values (name/email)
    end

    class Mailer
      # responsible for sending the e-mail (Net:SMTP)
    end

Agora com as responsabilidades separadas em classes, podemos notar que cada classe tem somente uma razão para mudar.

A classe Mailer pode utilizar uma gem ao invés do Net::SMTP e somente ela vai precisar ser alterada; WelcomeTemplate pode alterar as informações do e-mail; UserParamsFormatter só precisa se preocupar em como acessar os parâmetros e quais atributos devem ser adicionados ao hash.

Considerações finais

Conseguimos quebrar uma classe que ao poucos poderia virar um God Object (classe que faz tudo e sabe tudo) para algo mais fácil de administrar, porém, acabamos criando seis classes no processo.

Dependendo do projeto isso não seria um problema, mas, se considerarmos essa classe como parte inicial de um projeto, talvez tenhamos ido longe demais.

Inicialmente todo fluxo de como a aplicação funcionava era bastante simples e realizado por apenas uma classe. Mas agora precisamos ler até 6 classes para entender o que está acontecendo.

Esse é um dos problemas quando começamos a aplicar o SOLID: nos focamos muito no que cada princípio diz e, às vezes, podemos aplicar uma abstração errada ou um código que pode vir a ser um problema no futuro (um code smell).

No caso do Single Responsibility Principle, é comum nos preocuparmos somente se a classe tem apenas uma função, mas não verificamos se ao criar as novas classes, a compreensão do código ficou comprometida. Isso acontece quando criamos classes que possuem funções muito simples (lazy class). Por exemplo: no código anterior, criamos a classe UserParamsFormatter que é responsável por preparar os dados de entrada para a classe User. Essa classe tem apenas a função de retornar um hash simples com os atributos do usuário.

Talvez, nesse momento, fosse melhor mover o conteúdo da classe UserParamsFormatter para a classe UserFactory. No futuro, se a complexidade do código aumentar para lidar com os dados antes da criação do objeto, podemos avaliar a necessidade de quebrar as responsabilidades da UserFactory.

Existem casos em que pode ser melhor esperar o amadurecimento da aplicação para depois verificar se vale a pena ou não separar as responsabilidades das classes.

Se você estiver interessado em ler mais sobre os princípios de S.O.L.I.D., recomendamos os demais artigos dessa série:

Referências

Videos

Livros e Artigos

Foto de perfil do autor
André Benjamim

Dev na Rebase

Desenvolvedor Ruby formado em Ciência da Computação. Gosta de falar de Design de Código, Arquitetura e reclamar do calor