Article image
Diego Nunes
Diego Nunes30/04/2024 16:04
Share

SOLID

    Introdução

    O SOLID é um conjunto de princípios de design de software que visa melhorar a qualidade e a manutenção de código. É uma abreviação dos cinco princípios fundamentais que formam sua base e fornecem diretrizes valiosas para desenvolvedores que buscam criar sistemas flexíveis, escaláveis e sustentáveis.


    Origens e Autoria

    O SOLID foi introduzido por Robert C. Martin (também conhecido como Uncle Bob) em meados dos anos 2000, consolidando décadas de experiência no desenvolvimento de software. Uma pessoa de grande respeito na comunidade de programadores e um defensor fervoroso de boas práticas de codificação. Seu objetivo ao criar o estes princípios era oferecer uma estrutura conceitual que permitisse aos desenvolvedores criar software mais robusto e resistente às mudanças. O acrônimo S.O.L.I.D. foi identificado Michael Feathers enquanto lia o artigo  “The Principles of OOD” 


    Organização dos Princípios SOLID

    Os cinco princípios SOLID formam uma sequência lógica que, quando aplicada em conjunto, promove uma arquitetura de software sólida e de fácil manutenção. Esses princípios são:


    S - Single Responsibility Principle - SRP (Princípio da Responsabilidade Única)

    O - Open/Closed Principle - OCP (Princípio do Aberto/Fechado)

    L - Liskov Substitution Principle - LSP (Princípio da Substituição de Liskov)

    I - Interface Segregation Principle - ISP (Princípio da Segregação de Interface)

    D - Dependency Inversion Principle - DIP (Princípio da Inversão de Dependência)


    Benefícios da Aplicação do SOLID

    A aplicação rigorosa dos princípios SOLID oferece diversos benefícios aos desenvolvedores, alguns destes incluem:

    • Manutenção Facilitada: O código torna-se mais fácil de entender e manter, uma vez que cada classe tem uma responsabilidade clara e distinta;
    • Escalabilidade: A estrutura modularizada e flexível facilita a expansão do sistema sem comprometer a integridade do código existente;
    • Testabilidade Aprimorada: Classes isoladas e desacopladas permitem testes unitários mais eficazes e facilitam a adoção de práticas de desenvolvimento orientadas a testes (Test-Driven Development - TDD);
    • Redução do Acoplamento: Os princípios SOLID promovem a redução do acoplamento entre diferentes partes do código, resultando em sistemas mais flexíveis e menos propensos a efeitos colaterais indesejados.

    Na próxima parte, abordaremos mais detalhadamente cada um dos cinco princípios SOLID, explorando sua importância e como aplicá-los efetivamente em projetos de desenvolvimento de software.

    Princípio da Responsabilidade Única (Single Responsibility Principle - SRP)

    Conceito do SRP

    O Princípio da Responsabilidade Única (SRP) é um dos pilares fundamentais dos princípios SOLID, propondo que uma classe deve ter apenas uma razão para mudar. Em outras palavras, cada classe deve ter uma responsabilidade única e bem definida, evitando a acumulação de funcionalidades não relacionadas.

    O SRP é essencial para o desenvolvimento de sistemas que são fáceis de entender, manter e evoluir ao longo do tempo. Ao aplicar esse princípio, as classes tornam-se mais coesas, facilitando a identificação e modificação de código relacionado a uma determinada funcionalidade. De autoria de Robert C. Martin, a intenção por trás desse princípio é promover um design de software que seja adaptável a mudanças sem afetar indevidamente outras partes do sistema.


    Exemplo de SRP

    Considere um exemplo mais elaborado para entender como o SRP pode ser aplicado de forma prática em C#. Suponha que temos uma classe Relatório que é inicialmente responsável por gerar um relatório e enviá-lo por e-mail:

    using System;
    
    public class Relatorio
    
    {
    
      private readonly object dados;
      public Relatorio(object dados)
    
      {
          this.dados = dados;
      }
    
      public string GerarRelatorio()
    
      {
          // Lógica para gerar relatório com base nos dados
          return "Relatório gerado.";
      }
    
      public string EnviarPorEmail(string email)
    
      {
          // Lógica para enviar o relatório por e-mail
          return $"Relatório enviado para {email}";
      }
    
    }
    

    Neste exemplo, a classe Relatorio possui duas responsabilidades: gerar o relatório e enviá-lo por e-mail. Para aplicar o SRP, podemos dividir essas responsabilidades em duas classes distintas:

    using System;
    
    public class GeradorRelatorio
    {
      private readonly object dados;
    
    
      public GeradorRelatorio(object dados)
      {
          this.dados = dados;
      }
    
    
      public string GerarRelatorio()
      {
          // Lógica para gerar relatório com base nos dados
          return "Relatório gerado.";
      }
    }
    
    
    public class EnviadorEmail
    {
      public string EnviarPorEmail(string relatorio, string email)
      {
          // Lógica para enviar o relatório por e-mail
          return $"Relatório enviado para {email}";
      }
    }
    

    Agora, temos duas classes, cada qual com uma responsabilidade única. A classe GeradorRelatorio é responsável apenas por gerar o relatório, enquanto a classe EnviadorEmail é responsável apenas por enviar o relatório por e-mail. Isso torna o código mais modular, facilitando a manutenção e evolução do sistema.

    Ao aplicar o SRP, estamos não apenas organizando melhor nosso código, mas também construindo um sistema mais flexível e adaptável às mudanças futuras.

    Princípio do Aberto/Fechado (Open/Closed Principle - OCP) - Ampliado com Benefícios

    O Princípio do Aberto/Fechado (Open/Closed Principle - OCP) é focado na extensibilidade do código. Este princípio estabelece que uma classe deve ser aberta para extensão, mas fechada para modificação, proporcionando diversos benefícios ao desenvolvimento de software.


    Conceito do OCP

    O OCP incentiva os desenvolvedores a criarem código que seja extensível sem a necessidade de alterações em seu código-fonte original. Isso é alcançado por meio da abstração de funcionalidades que podem ser estendidas por novas classes, sem modificar o código existente. Ao aderir ao OCP, um sistema se torna mais resistente a mudanças e mais apto a evoluir ao longo do tempo.


    Autoria do OCP

    O OCP foi inicialmente proposto por Bertrand Meyer em seu livro "Object-Oriented Software Construction" e posteriormente popularizado por Robert C. Martin como um dos cinco princípios SOLID. A ênfase está na construção de software que seja capaz de evoluir sem a necessidade de alterações substanciais em seu código-fonte original.


    Benefícios do OCP

    Ao adotar o OCP, os desenvolvedores desfrutam de uma série de benefícios que contribuem para a qualidade e a manutenibilidade do código:

    • Extensibilidade Sem Modificação: A principal vantagem do OCP é a capacidade de estender o comportamento do sistema sem modificar o código existente. Isso é especialmente valioso em projetos de grande escala, onde as mudanças frequentes em código já existente podem ser propensas a erros e efeitos colaterais indesejados;
    • Manutenção Facilitada: Ao seguir o OCP, as modificações nas funcionalidades existentes são minimizadas. Isso facilita a manutenção do código, uma vez que as alterações são restritas a adições de novas funcionalidades, reduzindo o risco de introduzir bugs ou afetar o comportamento existente;
    • Maior Flexibilidade e Adaptação: Sistemas construídos seguindo o OCP tendem a ser mais flexíveis e adaptáveis às mudanças nos requisitos do negócio. A adição de novas funcionalidades pode ser feita de maneira mais eficiente, permitindo que o software evolua de acordo com as demandas do usuário ou do mercado;
    • Redução do Acoplamento: O OCP promove a redução do acoplamento entre diferentes partes do código. Ao estabelecer interfaces abstratas para extensão, as dependências tornam-se mais claras e menos propensas a interferências não planejadas quando novas funcionalidades são introduzidas;
    • Estímulo à Reutilização de Código: A criação de abstrações e interfaces claras no OCP incentiva a reutilização de código. Classes e módulos bem definidos podem ser estendidos e adaptados para diferentes contextos, promovendo a eficiência no desenvolvimento.

    Exemplo de aplicação do OCP

    Vamos considerar novamente o exemplo de geração de relatórios, mas desta vez focando nos benefícios do OCP. Vamos considerar um exemplo prático em C# para entender como aplicar o OCP. Suponha que temos um sistema de geração de relatórios e inicialmente, temos uma classe GeradorRelatorio que gera relatórios em formato PDF:

    using System;
    
    
    public class GeradorRelatorio
    {
      public string GerarRelatorioPDF()
      {
          // Lógica para gerar relatório em PDF
          return "Relatório gerado em PDF.";
      }
    }
    

    Agora, suponha que precisamos adicionar suporte para gerar relatórios em formato CSV sem modificar a classe existente. Podemos aplicar o OCP introduzindo uma interface GeradorRelatorioInterface e criando uma nova classe GeradorRelatorioCSV.

    using System;
    
    
    public interface IGeradorRelatorio
    {
      string GerarRelatorio();
    }
    
    
    public class GeradorRelatorioPDF : IGeradorRelatorio
    {
      public string GerarRelatorio()
      {
          return "Relatório gerado em PDF.";
      }
    }
    
    
    public class GeradorRelatorioCSV : IGeradorRelatorio
    {
      public string GerarRelatorio()
      {
          return "Relatório gerado em CSV.";
      }
    }
    
    
    public class GeradorRelatorioJSON : IGeradorRelatorio
    {
      public string GerarRelatorio()
      {
          return "Relatório gerado em JSON.";
      }
    }
    

    Agora, podemos adicionar novos formatos de relatórios sem modificar as classes existentes. Isso ilustra como o OCP permite a extensibilidade do código.

    Ao adotar o OCP, os desenvolvedores estão investindo em um design de software que é mais adaptável, menos propenso a erros e capaz de evoluir de forma eficiente à medida que novos requisitos surgem.

    Princípio da Substituição de Liskov (Liskov Substitution Principle - LSP)

    O Princípio da Substituição de Liskov (Liskov Substitution Principle - LSP) é o terceiro princípio dos SOLID e foi proposto pela cientista da computação Barbara Liskov. Este princípio estabelece que objetos de uma classe base devem ser substituíveis por objetos de suas classes derivadas sem afetar a corretude do programa. Em outras palavras, uma classe derivada deve ser capaz de substituir a classe base sem introduzir comportamentos indesejados.


    Conceito do LSP

    Originalmente apresentado da seguinte forma:

    "Subtype Requirement: Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T."

    Em português, isso pode ser traduzido como:

    "Requisito de Subtipo: Seja φ(x) uma propriedade comprovável sobre objetos x do tipo T. Então φ(y) deve ser verdadeira para objetos y do tipo S, onde S é um subtipo de T."

    Em resumo, o LSP diz que um objeto de uma classe filha deve poder ser substituído por um objeto da classe pai sem afetar o funcionamento correto do programa..


    Autoria do LSP

    O LSP foi introduzido por Barbara Liskov em seu artigo "A Behavioral Notion of Subtyping" em 1987. Barbara Liskov é uma renomada cientista da computação e foi premiada com o Prêmio Turing em 2009 por suas contribuições significativas para a ciência da computação.


    Exemplo de uso do LSP

    Vamos criar um exemplo simples em C# para ilustrar a violação do princípio de substituição de Liskov e depois um exemplo corrigido que respeita o princípio:

    using System;
    
    
    class Veiculo
    {
      public virtual void Acelerar()
      {
          Console.WriteLine("Acelerando o veículo...");
      }
    }
    
    
    class Carro : Veiculo
    {
      public override void Acelerar()
      {
          Console.WriteLine("Acelerando o carro e acionando os faróis...");
      }
    }
    
    
    class Program
    {
      static void Main()
      {
          Veiculo veiculo = new Carro();
          veiculo.Acelerar(); // Isso irá acionar os faróis, o que não era esperado para um veículo genérico
      }
    }
    

    Neste exemplo, a classe Carro sobrescreve o método Acelerar da classe Veiculo para também acionar os faróis. No entanto, ao criar um objeto Carro e atribuí-lo a uma variável do tipo Veiculo, podemos ver que o comportamento não é o esperado, pois ao chamar Acelerar, também acionamos os faróis, o que não era previsto para um veículo genérico.

    Exemplo respeitando o princípio de substituição de Liskov:

    using System;
    
    
    class Veiculo
    {
      public virtual void Acelerar()
      {
          Console.WriteLine("Acelerando o veículo...");
      }
    }
    
    
    class Carro : Veiculo
    {
      public override void Acelerar()
      {
          base.Acelerar();
          Console.WriteLine("Acionando os faróis...");
      }
    }
    
    
    class Program
    {
      static void Main()
      {
          Veiculo veiculo = new Carro();
          veiculo.Acelerar(); // Isso irá acelerar o veículo e acionar os faróis, de forma esperada
      }
    }
    

    Neste segundo exemplo, a classe Carro ainda sobrescreve o método Acelerar, mas agora ele chama primeiro o método da classe base (base.Acelerar()) para garantir que o comportamento original seja respeitado antes de adicionar o comportamento específico do carro. Assim, quando chamamos Acelerar em um objeto Carro atribuído a uma variável do tipo Veiculo, o comportamento é o esperado, acelerando o veículo e acionando os faróis.

    Princípio da Segregação de Interface (Interface Segregation Principle - ISP)

    O Princípio da Segregação de Interface (Interface Segregation Principle - ISP) é o quarto princípio SOLID, autorado por Robert C. Martin. Este princípio enfatiza que uma classe não deve ser forçada a implementar interfaces que ela não utiliza. Em outras palavras, é mais benéfico ter interfaces específicas para as necessidades de cada classe do que ter interfaces mais amplas que abrangem mais funcionalidades do que a classe requer.


    Conceito do ISP

    O ISP visa evitar interfaces "gordas" que contenham métodos que podem não ser relevantes para todas as implementações. Ele sugere a criação de interfaces mais específicas, cada uma contendo apenas os métodos que fazem sentido para as classes que a implementam. Isso promove uma melhor coesão e evita a introdução de métodos desnecessários.

    Ao seguir o ISP, o design do sistema se torna mais flexível, permitindo que as classes escolham as interfaces que fazem sentido para elas, sem forçá-las a implementar funcionalidades que não são relevantes.


    Exemplo de aplicação do ISP

    Considere um exemplo em C# onde inicialmente temos uma interface ITrabalhador que contém métodos relacionados ao trabalho, incluindo Trabalhar() e Descansar():

    using System;
    
    
    public interface ITrabalhador
    {
      void Trabalhar();
      void Descansar();
    }
    

    Agora, suponha que temos duas classes, uma representando um Programador e outra um Gerente, ambas implementando a interface ITrabalhador:

    using System;
    
    
    public class Programador : ITrabalhador
    {
      public void Trabalhar()
      {
          // Lógica específica do programador
      }
    
    
      public void Descansar()
      {
          // Lógica específica do programador
      }
    }
    
    
    public class Gerente : ITrabalhador
    {
      public void Trabalhar()
      {
          // Lógica específica do gerente
      }
    
    
      public void Descansar()
      {
          // Lógica específica do gerente
      }
    }
    

    No entanto, pode ocorrer que nem todos os métodos da interface sejam relevantes para ambas as classes. Para resolver isso, podemos aplicar o ISP e criar interfaces mais específicas:

    using System;
    
    
    public interface ITrabalhador
    {
      void Trabalhar();
    }
    
    
    public interface IDescanso
    {
      void Descansar();
    }
    
    
    public class Programador : ITrabalhador, IDescanso
    {
      public void Trabalhar()
      {
          // Lógica específica do programador
      }
    
    
      public void Descansar()
      {
          // Lógica específica do programador
      }
    }
    
    
    public class Gerente : ITrabalhador, IDescanso
    {
      public void Trabalhar()
      {
          // Lógica específica do gerente
      }
    
    
      public void Descansar()
      {
          // Lógica específica do gerente
      }
    }
    

    Agora, cada classe pode implementar apenas as interfaces relevantes para ela. Isso evita que métodos desnecessários sejam forçados nas implementações e promove um design mais coeso.


    Benefícios do ISP

    Ao adotar o ISP, os desenvolvedores desfrutam de vários benefícios que contribuem para a qualidade e a manutenibilidade do código:

    • Coesão Melhorada: Interfaces mais específicas resultam em classes mais coesas, uma vez que cada classe implementa apenas o conjunto de métodos que são relevantes para sua funcionalidade;
    • Redução da Sobrecarga de Implementação: Classes não são obrigadas a implementar métodos que não fazem sentido para elas, reduzindo a sobrecarga de implementação e tornando o código mais enxuto;
    • Facilidade de Manutenção: O ISP facilita a manutenção do código, pois as alterações em uma interface não afetarão as classes que não implementam essa interface específica;
    • Flexibilidade na Adição de Funcionalidades: Interfaces mais específicas permitem adicionar novas funcionalidades sem impactar as classes existentes, promovendo um sistema mais flexível e adaptável.

    Ao seguir o ISP, os desenvolvedores estão construindo um código mais modular, flexível e fácil de manter, contribuindo para a qualidade global do software.

    Princípio da Inversão de Dependência (Dependency Inversion Principle - DIP)

    O Princípio da Inversão de Dependência (Dependency Inversion Principle - DIP) é o quinto e último princípio dos SOLID, formulado por Robert C. Martin. Este princípio sugere que as classes de alto nível não devem depender diretamente das classes de baixo nível, mas ambas devem depender de abstrações. Além disso, ele propõe que detalhes de implementação devem depender de abstrações, não o contrário.


    Conceito do DIP

    O Princípio da Inversão de Dependência (Dependency Inversion Principle - DIP) estabelece que módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações. Além disso, abstrações não devem depender de detalhes, e sim detalhes devem depender de abstrações. Em resumo, o DIP busca inverter a direção das dependências no código, promovendo a dependência em abstrações em vez de implementações concretas. Isso é alcançado por meio do uso de interfaces ou classes abstratas que definem contratos e fornecem uma maneira de desacoplar módulos do sistema. Ao seguir o DIP, o código se torna mais flexível, resistente a mudanças e fácil de estender.


    Exemplo de aplicação do DIP

    Considere um exemplo em C# onde temos inicialmente uma classe Luz que depende diretamente de uma classe de baixo nível Interruptor:

    using System;
    
    
    public class Interruptor
    {
      public void Acionar()
      {
          // Lógica para acionar o interruptor
      }
    }
    
    
    public class Luz
    {
      private Interruptor interruptor;
    
    
      public Luz()
      {
          this.interruptor = new Interruptor();
      }
    
    
      public void Ligar()
      {
          this.interruptor.Acionar();
          // Lógica específica para ligar a luz
      }
    
    
      public void Desligar()
      {
          this.interruptor.Acionar();
          // Lógica específica para desligar a luz
      }
    }
    

    Neste exemplo, a classe Luz está diretamente dependente da classe Interruptor. Podemos aplicar o DIP invertendo a dependência por meio da introdução de uma interface Dispositivo:

    using System;
    
    
    public interface IDispositivo
    {
      void Acionar();
    }
    
    
    public class Interruptor : IDispositivo
    {
      public void Acionar()
      {
          // Lógica para acionar o interruptor
      }
    }
    
    
    public class Luz
    {
      private IDispositivo dispositivo;
    
    
      public Luz(IDispositivo dispositivo)
      {
          this.dispositivo = dispositivo;
      }
    
    
      public void Ligar()
      {
          this.dispositivo.Acionar();
          // Lógica específica para ligar a luz
      }
    
    
      public void Desligar()
      {
          this.dispositivo.Acionar();
          // Lógica específica para desligar a luz
      }
    }
    

    Neste exemplo refatorado, a classe Luz depende da abstração IDispositivo, seguindo o DIP. Isso permite maior flexibilidade, pois diferentes dispositivos podem ser utilizados sem afetar a classe Luz.


    Benefícios do DIP

    Ao adotar o DIP, os desenvolvedores colhem diversos benefícios que contribuem para a qualidade e a manutenibilidade do código:

    • Maior Flexibilidade: O DIP proporciona maior flexibilidade ao permitir que módulos de alto nível dependam de abstrações, facilitando a substituição de implementações concretas sem afetar as classes que dependem delas.
    • Redução do Acoplamento: A inversão de dependência reduz o acoplamento entre módulos, tornando o código mais modular e resistente a mudanças. Isso facilita a manutenção e a evolução do software.
    • Facilita Testes Unitários: O DIP facilita a realização de testes unitários, pois as dependências podem ser facilmente substituídas por mocks ou implementações específicas para testes.
    • Melhora a Manutenibilidade: A inversão de dependência contribui para uma melhor manutenibilidade do código, pois as alterações nas implementações concretas têm um impacto mínimo nas classes de alto nível.
    Share
    Comments (0)