S.O.L.I.D.: Princípio Aberto/Fechado

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

Introdução

Este é o segundo artigo de uma série na qual descreveremos em detalhes cada um dos princípios do SOLID – um conjunto de princípios que orientam o desenvolvimento de código compreensível e fácil de manter. No primeiro, discorremos sobre o Princípio da Responsabilidade Única. Aqui falaremos sobre o princípio Aberto/Fechado (Open/Closed Principle).

Open/Closed Principle (OCP)

O princípio de Aberto/Fechado propõe que entidades (classes, funções, módulos, etc.) devem ser abertas para extensão, mas fechadas para modificação.

Aberto para extensão significa que, ao receber um novo requerimento, é possível adicionar um novo comportamento. Fechado para modificação significa que, para introduzir um novo comportamento (extensão), não é necessário modificar o código existente.

Exemplo

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

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

      def invite
            email = InviteTemplate.new(@recipient)
            mailer.deliver(@recipient, email)
      end

      private

      def mailer
            Mailer.new
      end
     end

Nesse exemplo, a classe MailerService é responsável por enviar um e-mail com um template específico que é definido por cada método da classe.

O problema dessa classe é que a quantidade de templates existentes na aplicação corresponde à quantidade de métodos que essa classe deve implementar. Então, se a aplicação possui 20 templates, teoricamente, essa classe deveria implementar 20 métodos.

Talvez seja melhor aplicar uma refatoração nessa classe.

Primeira Refatoração

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

      def deliver
            email = templates[template].new(recipient)
            mailer.deliver(recipient, email)
      end

      private

      attr_reader :recipient, :template

      def mailer
            Mailer.new
      end

      def templates
            {
        welcome: WelcomeTemplate,
        invite: InviteTemplate,
        promotion: PromotionTemplate
            }
      end
    end

A classe espera receber um symbol com o tipo de template que ela deve utilizar, buscar no método templates e instanciá-la. Não existe mais a necessidade de implementar um novo método para cada template. Além disso, também foi removida a duplicidade de código.

Ainda assim, essa classe continua ferindo o OCP. Toda vez que um template for criado, será necessário adicioná-lo no método templates. Logo, é necessário alterar código já existente para adicionar um novo comportamento.

Talvez seja melhor aplicar mais uma refatoração nessa classe.

Segunda Refatoração

    class MailerService
      def initialize(template: WelcomeTemplate)
            @template = template
      end

      def deliver_to(recipient)
            email = template.new(recipient)
            mailer.deliver(recipient, email)
      end

      private

      attr_reader :template

      def mailer
            Mailer.new
      end
    end

     class WelcomeTemplate
       # código da classe
     end

     class InviteTemplate
       # código da classe
     end

     PromotionTemplate
       # código da classe
     end

Agora, ao invés de especificar os templates diretamente na classe, ela espera que seja passada uma classe de template no construtor como parâmetro: WelcomeTemplate, InviteTemplate, PromotionTemplate. Chamamos essa solução de Dependency Injection ou Injeção de Dependência.

Dependency Injection nada mais é do que passar uma dependência como argumento para uma entidade qualquer. Isso permite que o código dependa mais de uma abstração do que de algo concreto. No exemplo de código acima, a classe Mailer pode receber um template qualquer, mas por padrão ela recebe WelcomeTemplate.

Dependency Injection é apenas uma maneira de estender uma classe sem alterar seu comportamento. Existem muitas outras soluções como Decorator Pattern, Strategy Pattern, Composição e Herança.

Entre essas soluções, é importante atentar-se a Herança. Apesar da herança permitir resolver o OCP, se não houver o devido cuidado, ela pode aumentar o acoplamento do código, dificultando a manutenção, extensão e permitindo criar código que viole o Single Responsibility Principle.

Considerações Finais

OCP foca em facilitar a manutenção das entidades. A ideia é que nossas entidades comportem-se como um plugin do software, adicionando algo novo sem que isso interfira no comportamento do que já existe.

Precisamos evitar que ao alterar uma entidade, não seja necessário fazer alterações em diversos outros trechos da nossa base de código (chamamos isso de shotgun surgery). Uma das maneiras de adicionar comportamento mantendo o OCP, seria utilizar herança. Poderíamos ter uma classe User à qual queremos adicionar uma funcionalidade de usuário administrador. Para isso, criamos uma segunda classe Admin que herda de User somente o necessário e em Admin adicionamos a nova funcionalidade.

O problema é que a herança pode fazer com que aumente o acoplamento entre as classes, se dependermos mais da forma como a classe é implementada, do que da maneira como ela é representada no sistema (sua abstração).

O ideal é que a entidade dependa de uma abstração que forneça uma interface para interagirmos, sem nos importarmos como ela funciona.

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