Você quer ser um programador que faz códigos ou um que faz bons códigos?
O que é essa publicação e o porque do título?
O que é ser um programador? De forma leviana, é ser uma pessoa da qual tem capacidade de através da programação ("código") criar programas. Certo? Ok, até aqui beleza, até aqui você se encaixa. Mas será que você faz isso da forma mais eficiente? Com as melhores estruturas, patterns, reaproveitamentos e todas essas coisas com nome difícil? Você é um bom programador? Olha, você até pode ser mas EU não sou tão bom quanto eu quero ser. Por isso essa publicação existe. Vou realizar uma série de estudos publicados aqui do qual compartilho meus aprendizados e experiências sobre minha jornada em ser um programador melhor. Estudando mais o tão aclamado CLEAN CODE, design patterns, essas coisas. Coisas que não são voltados a conhecimentos específicos, como linguagens, libs ou bancos de dados, mas coisas que melhoram o seu código em geral, te tornam um programador melhor, afinal a linguagem é só a ferramenta.
ESTUDO NÚMERO 1: Você conhece o S.O.L.I.D?
Todos os desenhos são de autoria da Ugonna Thelma, link para o artigo dela a baixo
Se você é um programador mais experiente com certeza conhece, e até pode deixar mais detalhes, correções e dicas a baixo. Com certeza são bem-vindas!
SOLID é acrônimo (sigla que forma uma palavra) de uma metodologia para linguagens com POO (Programação orientada a objeto) do qual permite criar códigos de forma mais eficiente e agil. São princípios que prometem fazer você escrever códigos mais limpos, que separam responsabilidas, diminuem o acoplamento, facilita refatoração, estímula o reaproveitamento do código. Promete mais que coach vendedor de curso.
S - Single Resposibility Principle: SRP
Single Resposibility Principle, ou no bom e velho pt-br, Princípio da Responsabilidade Única.
Este princípio diz que cada classe deve ter uma única função, especializada em único assunto, mesmo que se tratem da mesma "entidade". Por exemplo, ao invés de ter uma única classe "Pedido" você ter uma classe para gerir cada responsabilidade de um pedido:
Exemplo em código:
Sem SRP:
class Pedido {
constructor(id, cliente, itens) {
this.id = id;
this.cliente = cliente;
this.itens = itens;
}
calcularTotal() {
return this.itens.reduce((total, item) => total + item.preco, 0);
}
salvarNoBanco() {
console.log(`Pedido ${this.id} salvo no banco de dados.`);
}
gerarRelatorio() {
console.log(`Relatório do pedido ${this.id}.`);
}
}
Com SRP:
class Pedido {
constructor(id, cliente, itens) {
this.id = id;
this.cliente = cliente;
this.itens = itens;
}
calcularTotal() {
return this.itens.reduce((total, item) => total + item.preco, 0);
}
}
class RepositorioDePedidos {
salvar(pedido) {
console.log(`Pedido ${pedido.id} salvo no banco de dados.`);
}
}
class GeradorDeRelatorios {
gerarRelatorio(pedido) {
console.log(`Relatório do pedido ${pedido.id}.`);
}
}
O - Open and Close Principle: OCP
Open and Close Principle, ou, Princípio do Aberto e fechado. Ele diz que objetos e entidades estão abertos a extensões e fechados para modificações. Deve ser genérico o suficienta para aumentar sem se modificar. De forma leviana, você pode colocar mais coisas nele mas o que já estava antes tem que permancer igual. Falando em metáfora, (já deve ter percebido que gosto de ser simplório para explicar né 🙃), imagine que você vai compras pneus para seu carro, você não compra qualquer pneu, não faz isso de qualquer jeito, e depois tenta encaixar na roda, ficar mudando ela até funcionar, você tem que ter o pneu do tamanho certo, do jeito certo.
Código de exemplo:
Sem OCP:
Código incial: Método que aplica descontos para clientes regulares
class Desconto {
calcularDesconto(tipoCliente, valor) {
if(tipoCliente === "regular") return valor * 0.1;
return valor;
}
}
Agora se quisermos adicionar um desconto para clientes vip temos a necessidade de mudar o método:
class Desconto {
calcularDesconto(tipoCliente, valor) {
if (tipoCliente === 'regular') {
return valor * 0.1;
} else if (tipoCliente === 'vip') {
return valor * 0.2;
} else {
return 0;
}
}
}
Imagine agora que temos dez tipos de clientes com diferentes tipos de desconto, esse if/else se tornaria provavelmente um enorme switch case alterado toda vez que tiver mais um tipo
Com OCP:
Veja como não precisamos alterar o método de cálculo mesmo adicionando mais um tipo de cliente
interface tipoCliente{
calcularDesconto(valor: number): number;
}
class ClienteRegular implements TipoCliente{
calcularDesconto(valor: number): number {
return valor * 0.1;
}
}
class ClienteVip implements TipoCliente {
calcularDesconto(valor: number): number {
return valor * 0.2;
}
}
class ClientePremium implements TipoCliente {
calcularDesconto(valor: number): number {
return valor * 0.3;
}
}
class Desconto {
calcular(valor: number, tipoCliente: TipoCliente): number {
return tipoCliente.calcularDesconto(valor);
}
}
L - Liskov Substitution Principle: LSP
Liskov Substitution Principle, ou Princípio da Substituição de Liskov. O princípio diz que se B é um subtipo de A ele deve manter as características (métodos e propriedades) de A e é capaz de funcionar em seu lugar. Agora você pensa, por quê? Porque se você precisar desconstruir uma abstração, logo, você abstraiu errado.
Exemplo de quebra do LSP:
class Animal {
cacar() {
console.log("caçando animais...");
}
comer() {
console.log("comendo");
}
}
class Leao extends Animal{
rugir() {
console.log("rugindo");
}
}
class Vaca extends Animal {
cacar() {
throw new Error("Vacas não caçam");
}
}
Nesse caso, a classe Vaca não pode realizar todas as ações da classe Animal, portanto a classe Animal tem abstrações ruins.
I - Interface Segregation Principle: ISP
Interface Segregation Principle ou Princípio de Segregação de Interface. Este diz que uma classe não pode ser forçada a ter classes que não usam, então, devemos segregar (separar) as interfaces da class.
Código de exemplo:
sem ISP:
interface Animal {
cacar(): void
comer(): void
}
class Leao implements Animal{
cacar() {
console.log("caçando animais...");
}
comer() {
console.log("comendo");
}
}
class Vaca implements Animal {
cacar() {
// Não faz nada
}
comer() {
console.log("comendo");
}
}
Com ISP:
interface Animal {
comer(): void
}
interface AnimalQueCaca {
cacar(): void
}
class Leao implements AnimalQueCaca{
cacar() {
console.log("caçando animais...");
}
comer() {
console.log("comendo");
}
}
class Vaca implements Animal {
comer() {
console.log("comendo");
}
}
Nesse caso a classe vaca não precisa ter um método da qual ela não vai utilizar, você separa uma nova interface para a finalidade de caçar.
D - Dependency Invernsion Principle: DIP
Dependency Invernsion Principle ou Princípio da Inversão de Dependência. O DIP diz que:
- Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.
- Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.
Vamos para a forma leviana, um pedreiro deve ser capaz de realizar uma tarefa sem depender de uma ferramenta específica, utilizando uma qualquer ferramenta que se encaixe, de modo que a lógica do trabalho não precise se preocupar com os detalhes de cada ferramenta específica.
Código de exemplo:
sem o DIP:
class ChaveDeFenda {
usarChave() {
console.log("Usando chave de fenda para apertar parafuso.");
}
}
class Pedreiro {
constructor() {
this.chaveDeFenda = new ChaveDeFenda();
}
apertarParafuso() {
this.chaveDeFenda.usarChave();
}
}
Com DIP:
interface Chave {
usarChave(): void;
}
class ChaveDeFenda implements Chave {
usarChave() {
console.log("Usando chave de fenda para apertar parafuso.");
}
}
class ChavePhillips implements Chave {
usarChave() {
console.log("Usando chave Phillips para apertar parafuso.");
}
}
class Pedreiro {
constructor(chave) {
this.chave = chave;
}
apertarParafuso() {
this.chave.usarChave();
}
}
Conclusão
Aplicar os princípios do SOLID não significa apenas escrever códigos que funcionem, mas também escrever códigos melhores. Se aprofundar nesses conceitos pode realmente te tornar um programador de códigos bons. E é isso que tentarei trazer nos próximos posts. Obrigado pela leitura e agradeço por dicas, correções e sugestões.