DEV Community

Gabriel_Silvestre
Gabriel_Silvestre

Posted on

Introdução ao SOLID - Princípios S O D

Tabela de Conteúdo


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

  • Single responsibility principle
  • Open/Close principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency 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."

Voltar ao topo


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.

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

type CURRENCY = {
  [key in EXCHANGE]: string;
}
Enter fullscreen mode Exit fullscreen mode
const convertValue = (value: number, currency: CURRENCY) => {
  return value * EXCHANGE[currency];
};
Enter fullscreen mode Exit fullscreen mode
const getBalance = async () => {
  const query = `
    SELECT amount FROM user.wallet;
  `;

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

  return amount;
};
Enter fullscreen mode Exit fullscreen mode
const isEnough = (personalBalance: number, totalAmount: number) => {
  return personalBalance > totalAmount;
};
Enter fullscreen mode Exit fullscreen mode
const payBill = async (billValue: number, billCurrency: CURRENCY) => {
  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`;
}
Enter fullscreen mode Exit fullscreen mode

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.

Voltar ao topo


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.

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

type Course = {
  name: string;
  openings: number;
  minNote: number;
}
Enter fullscreen mode Exit fullscreen mode
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,
    },
  },
];
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode
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);
};
Enter fullscreen mode Exit fullscreen mode

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.

Voltar ao topo


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.

interface IGameRPG {
  fight(): void;
  flee(): void;
  levelUp(): void;
}
Enter fullscreen mode Exit fullscreen mode
class Gamer {
  constructor(private gameRpg) {}

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

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

  improve() {
    this.gameRpg.levelUp();
  }
}
Enter fullscreen mode Exit fullscreen mode
class LightSouls implements IGameRPG {
  fight() {
    console.log('Bate, rola, rola, bate, potion, bate');
  }

  flee() {
    console.log('Correeeeee');
  }

  levelUp() {
    console.log('Welcome Home, ashen one. Speak thine heart\'s desire.');
  }
}
Enter fullscreen mode Exit fullscreen mode
class MoOnline implements IGameRPG {
  fight() {
    console.log('Bate, bate, bate, bate, bate, potion, bate, bate');
  }

  flee() {
    console.log('Sair da missão');
  }

  levelUp() {
    console.log('+1 força');
  }
}
Enter fullscreen mode Exit fullscreen mode
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.fight();  // Bate, bate, bate, bate, bate, potion, bate, bate
soulsGamer.fight();  // Bate, rola, rola, bate, potion, bate
Enter fullscreen mode Exit fullscreen mode

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.

Voltar ao topo


Links Úteis

Voltar ao topo

Discussion (0)