Entendendo SOLID
Tags: braziliandevs, agile, rust, solidprinciples
Published: 2024-05-19 15:40:18

Qualidade de software é a base fundamental para o desenvolvimento de qualquer sistema. Afinal, quanto maior a qualidade do software, menor a quantidade de erros, além de facilitar a manutenção e a expansão de novas funcionalidades no futuro.

De forma simples, pode-se dizer que a qualidade do software é inversamente proporcional à incidência de erros.

Com isso em mente, diversos estudos e abordagens foram desenvolvidos para aumentar a qualidade do software.

Dentre essas abordagens, surgiu o tão amado ou temido *SOLID*.

Mas o que é *SOLID*, afinal?

*SOLID* é um acrônimo que representa cinco princípios que facilitam o processo de desenvolvimento, deixando o código mais limpo, separando responsabilidades, diminuindo acoplamentos, facilitando a refatoração e estimulando o reaproveitamento de código, aumentando, desta forma, a qualidade do seu sistema.

Esses princípios podem ser aplicados em qualquer linguagem que adote a Programação Orientada a Objetos (*POO*).

Vamos começar do começo, então:
## S - Single Responsibility Principle ou Princípio da Responsabilidade Única

De forma simples, esse princípio define que **"Cada classe deve ter um, e somente um, motivo para mudar"**, sendo essa a essência do conceito.

Isso significa que uma classe ou módulo deve realizar uma **única** **tarefa**, tendo responsabilidade somente por ela.

Por exemplo, quando você começou a aprender programação orientada a objetos, provavelmente se deparou com classes nesse estilo:

```rust
struct Report;

impl Report{
	fn load(){}
	fn save(){}
	fn update(){}
	fn delete(){}

	fn print_report(){}
	fn show_report(){}

	fn verify(){}
}
```

> Nota: Isso é em Rust. Em Rust, não existe o conceito de classes; em vez disso, usamos estruturas como `struct` e `traits` para atingir esse tipo de comportamento. No entanto, a implementação e explicação do *SOLID* também são possíveis nesta linguagem.

Podemos ver que, neste exemplo, temos a `struct Report` (que em outras linguagens seria uma classe) e ela implementa diversos métodos. O problema não está na quantidade de métodos em si, mas no fato de que cada um deles faz coisas totalmente desconexas, o que faz com que a `struct` tenha mais de uma responsabilidade no sistema.

Isso quebra o princípio do Single Responsibility Principle, fazendo com que uma classe tenha mais de uma tarefa e, desta forma, mais de uma responsabilidade dentro do sistema. Essas são chamadas de _God Classes_ (Classes Deus) — classes ou structs que fazem de tudo. Num primeiro momento, isso pode parecer eficiente, mas, quando há necessidade de alterações nessa classe, será complicado modificar uma das responsabilidades sem comprometer as demais.

**_God Class_** — *Classe Deus: Na programação orientada a objetos, é uma classe que sabe demais ou faz demais.*

Quebrar esse princípio pode gerar uma falta de coesão, pois uma classe não pode assumir responsabilidades que não são suas, além de criar um alto acoplamento, devido ao aumento de dependências, e dificuldade de reaproveitar o código.

- **Falta de coesão**:
    - Uma classe não pode assumir responsabilidades que não são suas.
- **Alto acoplamento**:
    - Devido ao aumento de responsabilidades, gera-se um nível maior de dependências.
- **Dificuldade de reaproveitar o código**:
    - Código com muitas responsabilidades é mais difícil de reutilizar.

Podemos corrigir aquele código simplesmente aplicando a regra do Single Responsibility Principle:

```rust
struct Report;

impl Report {
	fn verify(){}
	fn get_report(){}
}

struct ReportRepository;

impl ReportRepository {
	fn load(){}
	fn save(){}
	fn update(){}
	fn delete(){}
}

struct ReportView;

impl ReportView {
	fn print_report(){}
	fn show_report(){}
}
```

Fazendo isso, você deixa cada struct/classe com uma única tarefa, ou seja, com apenas uma responsabilidade. Lembrando que esse princípio não se aplica somente a structs/classes, mas também a funções e métodos.
## O - Open-Closed Principle ou Princípio Aberto-Fechado

Essa sigla é definida como **_"entidades de software (como classes e métodos) devem estar abertas para extensão, mas fechadas para modificação"_**. Isso significa que você deve poder adicionar novas funcionalidades a uma classe sem alterar o código existente. Basicamente, quanto mais recursos adicionarmos a uma classe, mais complexa ela fica.

Para entender melhor, vamos pensar em uma ``struct`` de Forma:

```rust
struct Shape {
    t: String,
    r: f64,
    l: f64,
    h: f64,
}

impl Shape {
    fn area(&self) -> f64 {
        if self.t == "circle" {
            3.14 * self.r * self.r
        } else {
            0.0
        }
    }
}
```

Para adicionar a identificação de um retângulo, um programador menos experiente poderia sugerir alterar a estrutura `if`, adicionando uma nova condição. No entanto, isso vai contra o Princípio Aberto-Fechado, pois alterar uma classe já existente e totalmente funcional pode introduzir novos bugs.

Então, o que deveríamos fazer para adicionar essa nova tarefa?

Basicamente, precisamos construir a função usando uma interface ou um ``trait``, isolando o comportamento extensível atrás dessa estrutura. No Rust, isso ficaria desta forma:

```rust
trait Shape {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        3.14 * self.radius * self.radius
    }
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}
```

Com essa abordagem, podemos adicionar diversas outras formas sem alterar o código já existente.

Isso é o princípio do Open-Closed Principle em ação, deixando tudo mais limpo e simples de analisar, se necessário, no futuro.

A chave é que cada nova forma implementa a ``trait Shape`` com seu próprio método ``area``, permitindo que o sistema seja estendido com novas formas sem modificar as implementações já existentes. Isso promove a extensibilidade do software e minimiza o risco de introduzir erros em código já testado e funcional.

Dessa forma, conseguimos manter o código organizado e reduzir o impacto de futuras mudanças, facilitando a manutenção e evolução do sistema

## L - Liskov Substitution Principle ou Princípio da Substituição de Liskov

Esse princípio tem a seguinte definição: **_"Classes derivadas (ou classes-filhas) devem ser capazes de substituir suas classes-base (ou classes-mães)"_**. Ou seja, uma classe filha deve ser capaz de executar todas as ações de sua classe pai.

Vamos direto para um exemplo prático para entendermos melhor:

```rust
struct Square {
    side: f64,
}

impl Square {
    fn set_height(&mut self, height: f64) {
        self.side = height;
    }

    fn set_width(&mut self, width: f64) {
        self.side = width;
    }

    fn area(&self) -> f64 {
        self.side * self.side
    }
}

fn main() {
    let mut sq = Square { side: 5.0 };
    sq.set_height(4.0);
    println!("Area: {}", sq.area());
}
```

Nesse caso, o quadrado quebra essa regra diretamente, pois a ``trait`` fala que tanto altura quanto largura podem ser diferentes, o que para um quadrado não é válido.

Neste caso, o que podemos fazer? Simples:

```rust
trait Rectangle {
    fn set_height(&mut self, height: f64);
    fn set_width(&mut self, width: f64);
    fn area(&self) -> f64;
}

struct Square {
    side: f64,
}

impl Square {
    fn set_side(&mut self, side: f64) {
        self.side = side;
    }
}

impl Rectangle for Square {
    fn set_height(&mut self, height: f64) {
        self.set_side(height);
    }

    fn set_width(&mut self, width: f64) {
        self.set_side(width);
    }

    fn area(&self) -> f64 {
        self.side * self.side
    }
}

fn main() {
    let mut sq = Square { side: 5.0 };
    sq.set_height(4.0);
    println!("Area: {}", sq.area());
}
```

Agora, o quadrado adota o Princípio da Substituição de Liskov, pois ele respeita as regras do ``trait`` do Retângulo. Este é apenas um dos exemplos de como essa regra se aplica.

### Exemplos de violação do LSP:

- Sobrescrever/implementar um método que não faz nada.
- Lançar uma exceção inesperada.
- Retornar valores de tipos diferentes da classe base.

Para não violar esse princípio, é necessário, em muitos casos, utilizar injeção de dependência, além de outros princípios do próprio SOLID.

Com isso, dá para perceber que esses princípios se conectam e se completam conforme você vai entendendo como eles funcionam.
## I - Interface Segregation Principle ou Princípio da Segregação de Interfaces

De forma simples, o Princípio da Segregação de Interfaces afirma que uma classe não deve ser forçada a implementar interfaces e métodos que não irá utilizar. Ou seja, não devemos criar uma única interface genérica.

Vamos para um exemplo prático para entender melhor:

```rust
trait Worker {
    fn work(&self);
    fn eat(&self);
}

struct Human;

impl Worker for Human {
    fn work(&self) {}

    fn eat(&self) {}
}

struct Robot;

impl Worker for Robot {
    fn work(&self) {}

    fn eat(&self) {} // robos comem????
}
```

Nesse exemplo, temos uma interface (trait) `Worker` que exige que as classes que a implementam possuam dois métodos: `work` e `eat`. Faz sentido que a classe `Human` implemente esses dois métodos, já que humanos trabalham e comem, mas e o `Robot`? Um robô trabalha, mas ele não come. Portanto, ele é forçado a implementar o método `eat`, mesmo sem necessidade.

Como podemos arrumar isso? A resposta é criar interfaces específicas.

```rust
trait Workable {
    fn work(&self);
}

trait Eatable {
    fn eat(&self);
}

struct Human;

impl Workable for Human {
    fn work(&self) {}
}

impl Eatable for Human {
    fn eat(&self) {}
}

struct Robot;

impl Workable for Robot {
    fn work(&self) {}
}
```

Pronto, problema resolvido! Agora temos duas interfaces distintas: `Workable` e `Eatable`. Cada uma representa uma responsabilidade específica. A classe `Human` implementa ambas as interfaces, enquanto a classe `Robot` implementa apenas a interface `Workable`.

Ao adotar interfaces específicas, evitamos forçar classes a implementar métodos desnecessários, mantendo o código mais limpo, coeso e fácil de manter. Este é o cerne do Princípio da Segregação de Interfaces, que ajuda a criar sistemas mais flexíveis e robustos.
## D - Dependency Inversion Principle ou Princípio da Inversão de Dependência

Este princípio tem duas regras explícitas:

- Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender da abstração.
- Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.

E o que isso quer dizer? Simples: dependa de abstrações e não de implementações.

Vamos para um exemplo prático para entender melhor:

```rust
struct Light;

impl Light {
    fn turn_on(&self) {
        println!("Light is on");
    }

    fn turn_off(&self) {
        println!("Light is off");
    }
}

struct Switch {
    light: Light,
}

impl Switch {
    fn new(light: Light) -> Switch {
        Switch { light }
    }

    fn operate(&self, on: bool) {
        if on {
            self.light.turn_on();
        } else {
            self.light.turn_off();
        }
    }
}

fn main() {
    let light = Light;
    let switch = Switch::new(light);
    switch.operate(true);  
    switch.operate(false); 
}
```

Podemos ver que a classe `Switch` depende totalmente da classe `Light`. Nesse caso, se precisarmos substituir a classe `Light` por uma classe `Fan`, teríamos que modificar a classe `Switch` também.

Como resolver isso? Dependa de abstrações e não de implementações, conforme o princípio:

```rust
trait Switchable {
    fn turn_on(&self);
    fn turn_off(&self);
}

struct Light;

impl Switchable for Light {
    fn turn_on(&self) {
        println!("Light is on");
    }

    fn turn_off(&self) {
        println!("Light is off");
    }
}

struct Fan;

impl Switchable for Fan {
    fn turn_on(&self) {
        println!("Fan is on");
    }

    fn turn_off(&self) {
        println!("Fan is off");
    }
}

struct Switch<'a> {
    device: &'a dyn Switchable,
}

impl<'a> Switch<'a> {
    fn new(device: &'a dyn Switchable) -> Switch<'a> {
        Switch { device }
    }

    fn operate(&self, on: bool) {
        if on {
            self.device.turn_on();
        } else {
            self.device.turn_off();
        }
    }
}

fn main() {
    let light = Light;
    let switch_for_light = Switch::new(&light);
    switch_for_light.operate(true); 
    switch_for_light.operate(false); 

    let fan = Fan;
    let switch_for_fan = Switch::new(&fan);
    switch_for_fan.operate(true);  
    switch_for_fan.operate(false); 
}
```

Agora, `Switch` depende da abstração `Switchable`, e tanto `Light` quanto `Fan` implementam essa abstração. Isso permite que `Switch` funcione com qualquer dispositivo que implemente a interface `Switchable`.

### Benefícios:

- **Flexibilidade**: Adicionar novos dispositivos que implementam `Switchable` é fácil, sem precisar alterar o código existente de `Switch`.
- **Manutenção**: Alterações no comportamento específico dos dispositivos não afetam a lógica de controle.
- **Desacoplamento**: Reduz o acoplamento entre módulos de alto e baixo nível, facilitando testes e desenvolvimento paralelo.

## Conclusão
Utilizando os princípios SOLID, você pode tornar seu software mais robusto, escalável e flexível, facilitando a modificação sem muita dor de cabeça e dificuldade.

SOLID é essencial para desenvolvedores, e geralmente é usado em conjunto com as práticas de Clean Code para aumentar ainda mais a qualidade do seu sistema.

Embora possa parecer extremamente difícil entender esses conceitos e exemplos no início, é importante lembrar que nem sempre conseguiremos aplicar todos esses princípios durante o desenvolvimento. No entanto, com prática e persistência, você conseguirá escrever códigos cada vez mais maduros e robustos. SOLID será seu guia nessa jornada.

Se quiser entender um pouco mais e com ilustrações, recomendo este vídeo do Filipe Deschamps:

- [SOLID fica FÁCIL com Essas Ilustrações](https://www.youtube.com/watch?v=6SfrO3D4dHM&t)

Se você leu até aqui, recomendo fortemente que comece a implementar esses princípios em seus projetos, pois para aprender a programar, não há nada melhor que: PROGRAMAR.

Obrigado por ler!
## Referências:

- https://www.youtube.com/watch?v=6SfrO3D4dHM&t
- https://www.tomdalling.com/blog/software-design/solid-class-design-the-liskov-substitution-principle/
- https://medium.com/desenvolvendo-com-paixao/o-que-%C3%A9-solid-o-guia-completo-para-voc%C3%AA-entender-os-5-princ%C3%ADpios-da-poo-2b937b3fc530