Introdução ao SOLID - Princípios S O D
Tags: beginners, oop, typescript, braziliandevs
Published: 2022-05-20 21:56:47

## Tabela de Conteúdo
- [SOLID](#solid)
- [Single Responsibility Principle](#single-responsibility-principle)
- [Open/Closed Principle](#openclosed-principle)
- [Dependency Inversion Principle](#dependency-inversion-principle)
- [Repositório](#repositório)
- [Links Úteis](#links-úteis)

---

## SOLID

### O que é?

SOLID, na programação, refere-se a cinco princípios que focam na escrita de um código limpo, legível e de fácil manutenção.

### Princípios

- **S**ingle responsibility principle
- **O**pen/Close principle
- **L**iskov substitution principle
- **I**nterface segregation principle
- **D**ependency inversion principle

### Aviso

O SOLID propõe __PRINCÍPIOS__ a serem seguidos para o desenvolvimento de um código mais limpo, porém esses princípios não são absolutos e podem muito bem ser ignorados ou adaptados ao nosso contexto.

Em outras palavras, o SOLID nos da boas recomendações a serem seguidas, mas não passa disso, __RECOMENDAÇÕES__.

_"Regra nenhuma, princípio nenhum e caso especial nenhum deve piorar a legibilidade ou a manutenibilidade do seu código."_

<br />

<small>[Voltar ao topo](#tabela-de-conteúdo)</small>

---

## Single Responsibility Principle

### Recomendação

O princípio da responsabilidade única recomenda que cada função ou classe realize uma única ação, dessa forma teremos nosso código divido em pequenos blocos altamente _"especializados"_.

### Exemplo

Imagine que precisemos efetuar o pagamento de uma conta em outra moeda, para isso vamos precisar converter o valor para Real, acessar nossa carteira _(banco de dados)_, recuperarmos nosso saldo e se nosso saldo for suficiente, pagar a conta.

``` ts
const EXCHANGE = {
  USD: 4.6,
  EUR: 5,
  CAD: 3.7,
};

type ICurrency = keyof typeof EXCHANGE;
```

``` ts
const convertValue = (value: number, currency: ICurrency) => {
  return Math.floor(value * EXCHANGE[currency]);
};
```

``` ts
const getBalance = async () => {
  const query = `
    SELECT amount FROM user.wallet;
  `;

  const [{ amount }] = await mysql.execute(query);

  return amount;
};
```

``` ts
const isEnough = (personalBalance: number, totalAmount: number) => {
  return personalBalance > totalAmount;
};
```

``` ts
const payBill = async (billValue: number, billCurrency: ICurrency) => {
  const myBalance = await getBalance();
  const convertedValue = convertValue(billValue, billCurrency);

  if (isEnough(myBalance, convertedValue)) {
    return `Conta paga | saldo anterior ${myBalance} | saldo atual ${
      myBalance - convertedValue
    }`;
  }

  return `Saldo insuficiente | saldo anterior ${myBalance} | saldo atual ${myBalance}`;
};
```

No exemplo acima, apesar de termos omitido alguns passos, conseguimos criar um código desacoplado, legível e de fácil entendimento. Isso foi possível graças à alta especialização do código, onde cada função é responsável por executar uma única ação.

<br />

<small>[Voltar ao topo](#tabela-de-conteúdo)</small>

---

## Open/Closed Principle

### Recomendação

O princípio aberto/fechado diz que o comportamento de uma entidade deve ser aberto a extensões, mas fechado a modificações, em palavras mais simples, precisamos poder criar novos comportamentos sem alterar os já existentes.

### Exemplo

Imagine que uma faculdade nos pediu para desenvolvermos um software que irá validar se os candidatos conseguiram passar no vestibular para determinado curso.

Para isso nós vamos precisar saber a nota do candidato, a nota de corte do curso e quantas vagas o curso possui, ou seja, temos duas regras variáveis: nota de corte e quantidade de vagas.

``` ts
type Student = {
  name: string;
  examNote: number;
  course: Course;
}

type Course = {
  name: string;
  openings: number;
  minNote: number;
}
```

``` ts
const students = [
  {
    name: 'John',
    examNote: 87,
    course: {
      name: 'ADM',
      openings: 40,
      minNote:  62,
    },
  },
  {
    name: 'Joe',
    examNote: 42,
    course: {
      name: 'ENG',
      openings: 20,
      minNote:  76,
    },
  },
  {
    name: 'Katy',
    examNote: 66,
    course: {
      name: 'MED',
      openings: 10,
      minNote:  85,
    },
  },
];
```

``` ts
const rankingStudents = (students: Student[]) => {
  return students.sort((a, b) => a.examNote - b.examNote );
};

const hasOpening = (listLength: number, listLimit: number) => {
  return listLength < listLimit;
};

const hasNote = (studentNote: number, minNote: number) => {
  return studentNote >= minNote;
}
```

``` ts
const approvedList = {
  ADM: [],
  ENG: [],
  MED: [],
}

const approvedStudents = (students: Student[]) => {
  return rankingStudents(students)
    .reduce((acc, { name, examNote, course }) => {
      const courseApprovedList = acc[course.name];

      if (!hasOpening(courseApprovedList.length, course.openings)) return acc;

      if (hasNote(examNote, course.minNote)) {
        acc[couse.name].push(name);
      }

      return acc;
    }, approvedList);
};
```

No exemplo construído logo acima, estamos montando a lista de aprovados de acordo com o número de vagas e a nota de corte de cada curso, sendo que ambos os dados são variáveis, ou seja, podemos adicionar qualquer curso que quisermos a esse software sem a necessidade de alterá-lo.

<br />

<small>[Voltar ao topo](#tabela-de-conteúdo)</small>

---

## Dependency Inversion Principle

### Recomendação

Antes de entrar mais a fundo nas recomendações, vale a citação que o princípio de inversão de dependências anda em paralelo com a arquitetura de Injeção de Dependências, essa que não será abordada aqui.

O princípio de inversão de dependências consiste na implementação de uma dependência baseada em uma abstração, normalmente uma Interface, ao invés de um objeto, valor ou classe propriamente dito.

### Exemplo

Diferente dos exemplos anteriores, aqui iremos utilizar da Orientação a Objeto para exemplificar, pois acredito que seja mais fácil de entender dessa forma.

Pense no seguinte cenário, temos uma pessoa e ela tem vários jogos para jogar, porém todos esses jogos são de RPG, logo suas ações são bem similares. O objetivo é criar uma classe Pessoa que possa jogar qualquer jogo de RPG.

``` ts
export interface IGameRPG {
  fight(): number; // <- retorna o dano causado
  flee(): boolean; // <- retorna se conseguiu fugir ou não
  levelUp(): void;

  get level(): number; // <- nível do jogo
}
```

``` ts
class Gamer {
  constructor(private gameRpg: IGameRPG) {}

  combat() {
    return this.gameRpg.fight();
  }

  escape() {
    return this.gameRpg.flee();
  }

  improve() {
    this.gameRpg.levelUp();
  }
}
```

``` ts
class LightSouls implements IGameRPG {
  private _level: number = 1;

  // 10 à 100 de dano
  fight() {
    console.log('Bate, rola, rola, bate, potion, bate');

    return Math.floor(Math.random() * (100 - 10 + 1)) + 10;
  }

  // 30% de chance de fugir
  flee() {
    console.log('Correeeeee');

    return Math.random() < 0.3;
  }

  levelUp() {
    console.log("Welcome Home, ashen one. Speak thine heart's desire.");

    this._level += 1;
  }

  get level() {
    return this._level;
  }
}
```

``` ts
export class MoOnline implements IGameRPG {
  private _level: number = 1;

  // 1000 à 10000 de dano
  fight() {
    console.log('Bate, bate, bate, bate, bate, potion, bate, bate');

    return Math.floor(Math.random() * (10000 - 1000 + 1)) + 1000;
  }

  // 60% de chance de fugir
  flee() {
    console.log('Sair da missão');

    return Math.random() < 0.6;
  }

  levelUp() {
    console.log('Acessando novo andar...');

    this._level += 1;
  }

  get level() {
    return this._level;
  }
}
```

``` ts
const moOnline = new MoOnline();
const lightSouls = new LightSouls();

const moGamer = new Gamer(moOnline); // <- a classe Gamer recebe o jogo Mo Online
const soulsGamer = new Gamer(lightSouls); // <- a classe Gamer recebe o jogo Light Souls

moGamer.combat(); // Bate, bate, bate, bate, bate, potion, bate, bate
soulsGamer.combat(); // Bate, rola, rola, bate, potion, bate
```

Como podemos ver no exemplo acima, a classe Gamer pode receber qualquer jogo, desde que ele seja uma implementação da abstração `IGameRPG`.

Isso só é possível, pois estamos garantindo, através da interface `IGameRPG`, que todo a classe que a implemente vai ter os métodos `fight`, `flee` e `levelUp`.

<br />

<small>[Voltar ao topo](#tabela-de-conteúdo)</small>

---

## Repositório

~~_Todos os exemplos_~~ demonstrados aqui se encontram nesse [repositório](https://github.com/gabrielh-silvestre/solid-examples), fique a vontade para _favoritar_, clonar ou realizar um fork.

_Este repositório está em construção. Novos exemplos serão adicionados em breve._

<br />

<small>[Voltar ao topo](#tabela-de-conteúdo)</small>

---

## Links Úteis

- [Refactoring Guru](https://refactoring.guru/pt-br)
- [Medium - Design Patterns in TypeScript Factory](https://cesarwilliam.medium.com/design-patterns-in-typescript-factory-d870a00b0f31) por Cesar William
- [YouTube RocketSeat - Princípios do SOLID em uma API REST](https://www.youtube.com/watch?v=vAV4Vy4jfkc)
- [YouTube RocketSeat - Construindo uma API com SOLID + TDD](https://www.youtube.com/watch?v=mjBsii0eiuI)
- [YouTube Código Fonte TV - SOLID // Dicionário do Programador](https://www.youtube.com/watch?v=mkx0CdWiPRA)

<br />

<small>[Voltar ao topo](#tabela-de-conteúdo)</small>