Vamos falar sobre os princípios SOLID. Você conhece?

SOLID: Um pouco de história

SOLID nada mais é que um conjunto de princípios para Orientação Programada a Objetos (OOP). Esses princípios surgiram por volta do ano 2000 pelo Robert C. Martin. Em 2003 esses princípios foram popularizados quando lançou o seu livro "Agile Software Development, Principles, Patterns, and Practices".

Desde então essas práticas tem sido adotadas por empresas de tecnologia no mundo inteiro, especialmente nas linguagens OOP: Java, C#, Python, etc. Essas práticas tornam o software capaz de integrar com Agilistas e DevOps, pois hoje são considerados essenciais para boas práticas nos softwares modernos.

Afinal, quais são os princípios do SOLID?

SOLID é o acrônimo para o conjunto de princípios que serão detalhados logo abaixo. Não se preocupe desenvolvedor! Exemplos serão exibidos para que cada um deles fique claro.

S: Princípio da Responsabilidade Única (Single Responsibility Principle - SRP): A classe deve ter apenas um único motivo para ser alterada. Isso significa que ela deve ter apenas uma única responsabilidade;

O: Aberto/Fechado (Open/Closed Principle - OCP): Uma classe deve estar fechada para alteração. Mas deve estar "aberta" para extensão. Ou seja, você pode ser capaz de alterar um módulo sem mexer no código fonte da classe;

L: Princípio de Substituição de Liskov (Liskov Substitution Principle - LSP): Uma classe derivada pode ser capaz de substuir a "classe pai" sem causar qualquer comportamento inesperado;

I: Princípio da Segregação de Interface (Interface Segregation Principle - ISP): Nesse princípio o cliente não pode "ser forçado" a usar interfaces com "pedaços" que ele não usa. Ou seja, grandes interfaces são divididas em interfaces menores de acordo com a funcionalidade. Podemos fazer um paralelo com "interfaces com uma única responsabilidade - SRP";

D: Princípio da Inversão de Dependência (Dependency Inversion Principle - DIP): Módulos de alto nível não podem depender de módulos de baixo nível. Ambos dependerão de abstrações. Porém, abstrações não podem depender de detalhes. Mas os detalhes devem depender de abstrações. Apesar de parecer complicado, isso significa que você deve usar a injeção dependência e a inversão de controle para desacoplar os módulos de alto e baixo nível fazendo seus códigos dependerem de abstrações ao invés de implementações concretas.

Você pode programar sistemas sem usar os princípios aqui descritos. Entretanto, lembre-se que a aplicação do SOLID torna seu sistema mais fácil de fazer manutenções, mais flexível, simplifica os testes nos códigos e ainda você ganha escalabilidade tendo em vista que os módulos estarão desacoplados.

Vamos a prática!

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

Lembre-se que nesse princípio cada classe deve ter apenas uma única responsabilidade.

🚫
Esse é um exemplo, escrito em C#, que viola esse princípio.
public class Login
{
    public int Id { get; set; }
    public required String Email {get; set; }
    public required String Senha {get; set; }

    public void Autenticar() {
        // Lógica para autenticar no banco de dados
    }

    public void EnviaEmailLogado() {
        // Lógica para enviar e-mail ao usuário após login.
    }

}

Perceba que a classe "Login" apresenta duas responsabilidades violando o primeiro princípio SRP: Autenticar o usuário e enviar o e-mail após o usuário estar logado.

Agora veja o mesmo trecho de código reescrito da forma correta.
public class Login
{
    public int Id { get; set; }
    public required String Email { get; set; }
    public required String Senha { get; set; }
}

public class LoginService
{
    public void Autenticar(Login login)
    {
        // Lógica para autenticar no banco de dados
    }
}

public class LoginEmailService
{
    public void EnviaEmailLogado(Login login)
    {
        // Lógica para enviar e-mail ao usuário após login.
    }
}

Nesse código refatorado, perceba que em uma classe definimos as propriedades, na outra apenas autenticamos o usuário e na última enviamos o e-mail. Quando separamos as responsabilidades de cada classe, note que só tem uma razão para alterá-la. Se mudamos a autenticação do usuário, basta modificar a "LoginService". Por outro lado, se o envio de e-mail deve ser feito de outra forma, basta alterar a "LoginEmailService".

Aberto/Fechado (Open/Closed Principle - OCP)

Lembre-se que nesse princípio cada classe deve estar fechada para alteração mas aberta para extensão.

🚫
Esse é um exemplo, escrito em C#, que viola esse princípio.
public class EnviarNotificacao
{
    public void PorEmail(String msg) {
        // Lógica para Enviar e-mail
    }

    public void PorWhatsapp(String msg) {
        // Lógica para enviar por whatsapp
    }

    public void PorSMS(String msg) {
        // Lógica para enviar por SMS
    }
}

Imagina que em determinado dia o sistema precise ser modificado e enviar notificações pelo Instagram. Vamos criar um método novo para isso? A resposta é não!

Agora veja o mesmo trecho de código reescrito da forma correta.
public abstract class EnviarNotificacao
{
    public abstract void Enviar(String msg);
}

public class EnviarNotificacaoEmail : EnviarNotificacao
{
    public override void Enviar(string msg)
    {
        // Lógica para enviar e-mail
    }
}

public class EnviarNotificacaoWhatsApp : EnviarNotificacao
{
    public override void Enviar(string msg)
    {
        // Lógica para enviar WhatsApp
    }
}

public class EnviarNotificacaoSMS : EnviarNotificacao
{
    public override void Enviar(string msg)
    {
        // Lógica para enviar SMS
    }
}


public class Notificacao
{
    public void Enviar(EnviarNotificacao enviar, String Msg)
    {
        enviar.Enviar(Msg);
    }
}

Com o código refatorado perceba que temos uma classe abstrata que define o comportamento que queremos herdar. Entretanto, temos implementações de novas classes sem modificar o código existente. Ou seja, cada classe está fechada para modificações mas podem ter diversas outras notificações sem precisar alterar os códigos já existentes.

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

Para relembrar, uma classe derivada pode substituir um método da pai sem conhecê-la e sem por a integridade do programa em risco.

Exemplo da implementação que não viola LSP.
public class ContaBancaria
{
    public virtual decimal GetSaldo()
    {
        return 100;
    }
}

public class Juros : ContaBancaria
{
    public override decimal GetSaldo()
    {
        return base.GetSaldo() + GetJuros();
    }

    private decimal GetJuros()
    {
        return 0.1m * base.GetSaldo();
    }
}

Perceba nesse exemplo que a classe "ContaBancaria" retorna o Saldo da conta. Note também que a classe "Juros" herda "ContaBancaria" e mantém exatamente o mesmo comportamento da aplicação.

Se o objeto "ContaBancaria" está sendo usado no programa e depois mudamos para o objeto "Juros", a integridade do programa não será afetada. Isso ocorre porque a classe "Juros" cumpre o contrato com a classe "ContaBancaria".

static void Main(string[] args)
{
    ContaBancaria conta = new ContaBancaria();
    decimal saldo = conta.GetSaldo();
    Console.WriteLine("Saldo na conta: " + saldo);

    conta = new Juros();
    decimal saldoTotal = conta.GetSaldo();
    Console.WriteLine("Saldo com rendimentos: " + saldoTotal);
}

Nesse exemplo mesmo quando atribuímos um novo objeto a "conta", a integridade do programa não é comprometida.

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

Para relembrar, nesse princípio uma interface não deve ser imposta obrigando as classes que a implementam a ter métodos que não vão usar.

🚫
Esse é um exemplo, escrito em C#, que viola esse princípio.
public interface IAnimal
{
    void Dorme();
    void Come();
    void Voa(); // Atenção aqui
}

public class Passaro : IAnimal
{
    public void Dorme()
    {
        Console.WriteLine("Passaro dormindo...");
    }

    public void Come()
    {
        Console.WriteLine("Passaro comendo...");
    }

    public void Voa()
    {
        Console.WriteLine("Passaro voando...");
    }
}

public class Elefante : IAnimal
{
    public void Dorme()
    {
        Console.WriteLine("Elefante dormindo...");
    }

    public void Come()
    {
        Console.WriteLine("Elefante comendo...");
    }

    public void Voa()
    {
        // ???
    }
}

No exemplo acima, a classe "Elefante" é obrigada a implementar todos os métodos da interface. Mas o método "Voa()", ficará sem uso, já que Elefante não voa!

Exemplo da implementação que não viola ISP.
public interface IAnimal
{
    void Dorme();
    void Come();
}

public interface IAnimalVoa : IAnimal {
    void Voa();
}

public class Passaro : IAnimalVoa
{
    public void Dorme()
    {
        Console.WriteLine("Passaro dormindo...");
    }

    public void Come()
    {
        Console.WriteLine("Passaro comendo...");
    }

    public void Voa()
    {
        Console.WriteLine("Passaro voando...");
    }
}

public class Elefante : IAnimal
{
    public void Dorme()
    {
        Console.WriteLine("Elefante dormindo...");
    }

    public void Come()
    {
        Console.WriteLine("Elefante comendo...");
    }
}

Após o código ser refatorado, a interface foi dividida em interfaces menores e as classes que as implementam passaram a ter somente o que de fato o objeto vai usar.

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

Em resumo, módulos de alto nível não devem não podem depender de módulos de baixo nível. É comum muitos desenvolvedores instanciarem objetos dentro de outros objetos causando "forte dependência" entre os módulos.

🚫
Esse é um exemplo, escrito em C#, que viola esse princípio.
public class Json
{
    public void Salvar()
    {
        // Lógica para salvar
    }
}

public class XML
{
    public void Salvar()
    {
        // Lógica para salvar
    }
}


public class Log
{
    public void Registrar(string tipo)
    {
        if ("JSON" == tipo)
        {
            Json log = new Json();
            log.Salvar();
        }
        else if ("XML" == tipo)
        {
            XML log = new XML();
            log.Salvar();
        }
    }
}

Neste exemplo temos uma classe de "Log" que registra os registros em JSON ou XML. Perceba que dentro da classe "Log" os objetos são instanciados causando forte acoplamento. Agora imagine que a classe "Json" precise de um novo parâmetro no construtor. Você precisará "varrer" todo seu código para alterar.

Exemplo da implementação que não viola DIP.
public interface ILogTipo
{
    public void Salvar();
}

public class Json : ILogTipo
{
    public void Salvar()
    {
        // Lógica para salvar
    }
}

public class XML : ILogTipo
{
    public void Salvar()
    {
        // Lógica para salvar
    }
}


public class Log
{
    private readonly ILogTipo _logTipo;


    public Log(ILogTipo logTipo) {
        _logTipo = logTipo;
    }

    public void Registrar()
    {
        _logTipo.Salvar();
    }
}

Nesse exemplo, perceba que os módulos de baixo nível "Json" e "XML" implementam a interface "ILogTipo". Já o módulo de alto nível "Log" depende apenas da interface "ILogTipo" ao invés dos módulos de baixo nível. Essa implementação permite facilmente implementarmos diferentes classes sem modificar a classe "Log".

Em resumo, conseguimos alcançar o princípio removendo o acoplamento entre os módulos de alto e baixo nível e tornamos nosso código mais fácil de ler e dar manutenção.

Considerações

É possível construir sistemas sem o uso dos princípios SOLID. Entretanto, como na maioria dos sistemas de tecnologia, quando os sistemas crescem muito, acabam tornando-se "legados" ou "bicho de 7 cabeças" para dar manutenção ou implementar novas features.

Quando a arquitetura é planejada desde o início e a equipe (squad) resolve planejar dentro desses 5 princípios, novas implementações ou mudanças tornam-se muito menos dolorosas e exigem bem menos esforço e tempo do time.