Alura > Cursos de Programação > Cursos de Node.JS > Conteúdos de Node.JS > Primeiras aulas do curso SOLID com TypeScript: aplicando boas práticas em orientação a objetos

SOLID com TypeScript: aplicando boas práticas em orientação a objetos

Separando as responsabilidades - Apresentação

Olá! Boas-vindas a este curso de Solid.

Meu nome é Emerson Laranja, sou instrutor na Escola de Programação.

Audiodescrição: Emerson se descreve como um homem negro com barba e cabelo curtos. Está usando óculos quadrados e uma blusa azul. Ao fundo, há uma parede iluminada nas cores azul e verde.

Se desejam escrever um código mais limpo e seguro, este conteúdo é perfeito para vocês. Ao longo deste curso, vamos explorar os cinco princípios do Solid, que são:

Vamos ver tudo isso, na prática, em um sistema de gerenciamento de uma empresa. Vamos explorar o módulo que gerencia pessoas, pagamentos e até mesmo a infraestrutura desse sistema.

Para acompanhar melhor este conteúdo, é importante que já tenham conhecimento básico de TypeScript e de programação orientada a objetos.

Convidamos vocês a aproveitarem os recursos da plataforma e ir além dos vídeos, cumprindo as atividades, tirando suas dúvidas no fórum do curso e conversando com outras pessoas que estejam estudando este mesmo assunto na nossa comunidade no Discord.

Então, vamos lá?

Separando as responsabilidades - Entendendo o código ''faz-tudo''

Fomos contratados para implementar algumas melhorias e refatorações em um sistema de gerenciamento de uma empresa. As atividades que precisaremos executar foram listadas no Trello, que temos aberto.

Conhecendo as tarefas do projeto

A primeira tarefa é "Código Canivete Suíço". Vamos abrir o card para entendermos melhor do que se trata. Tivemos uma conversa inicial com o Tech Lead (Líder Técnico) e fomos informados que a maioria dessas melhorias não está relacionada a um código legado ou a uma nova biblioteca que precisa ser usada, mas sim a algumas refatorações de boas práticas que precisaremos implementar. Começando com a descrição desta primeira atividade:

Temos um módulo do sistema que realiza diversas funções e isso dificulta a manutenção. Trata-se da classe Sistema no código anexado.

Além disso, estamos repetindo código para calcular o salário, já que não conseguimos reutilizar código com tudo tao acoplado.

Por favor, resolva o problema e realize as refatoraçóes necessárias.

Explorando o projeto disponibilizado

No card da tarefa, temos um projeto no GitHub em anexo, que eu já deixei no nosso editor de código, o VS Code. Com o código aberto, vamos visualizar do que se trata essa primeira tarefa. No explorador de arquivos do VS Code, temos a pasta "tarefa-1" e alguns outros arquivos para a configuração do projeto Node.

Na pasta "tarefa 1", temos as pastas "dist", onde ficará o nosso código compilado em JavaScript e "enum", com o arquio cargos.ts, com os cargos possíveis na empresa:

Na pasta "tarefa-1", temos também a classe Colaborado, com três atributos: nome, _cargo e _saldo. Nela também temos, e algumas operações de get e set. Além disso, temos a classe main e a classe Sistema.

Podemos então abrir o Sistema.ts, onde está, de fato, nosso problema. Neste arquivo temos dois atributos: _colaboradores, do tipo Colaborador, e _salarioBase, do tipo number. Além disso, ele possui vários métodos, como contratarColaborador, demitirColaborador, calcularSalario, pagarColaborador, gerarRelatorioJSON e um get dos colaboradores desse sistema.

Analisando os problemas

Como podemos ver, é uma classe grande, o que dificulta tanto entender o código, quanto fazer a manutenção dele. Além disso, esse código apresenta baixa coesão, o que significa que ele assume responsabilidades que não são dele.

Por exemplo, temos um sistema que, além de lidar com colaboradores, lida com pagamento. Para termos uma alta coesão, o ideal é que os métodos da nossa classe se relacionem com a sua definição. Se é uma classe de pagamento, ali lidaremos com pagamento apenas, não com manipulações com colaborador.

Além disso, possuímos um alto acoplamento. Porque hoje, se precisarmos, como exigido pela tarefa, calcular o salário, precisaremos importar uma instância de todo o sistema, com vários outros métodos, só para calcular um salário. Quando o ideal seria ter um módulo para lidar apenas com isso.

Para facilitar o entendimento de como seria essa solução, já criei um diagrama. Voltando ao nosso navegador, vou abrir o Lucidchart, onde tem um quadrado com a metade superior sendo um fundo vermelho e a metade inferior é um fundo verde.

No fundo vermelho, temos a nossa situação atual, e, no fundo verde, onde queremos chegar, ou seja, o código correto, assim digamos. Vamos ampliar o que está escrito no fundo vermelho para entendermos como está a relação hoje dessas classes.

Diagrama da situação atual do projeto. Sobre o fundo vermelho, há três elementos: "Colaborador", "Cargos" e "Sistema". À esquerda, o retângulo "Colaborador" contém atributos nome, cargo e saldo, assim como métodos para obter e definir cargo e saldo. Ao centro superior, a caixa "Cargos", marcada como enumeração, lista os tipos de cargos: Estagiário, Junior, Pleno e Sênior. À direita, o retângulo "Sistema" mostra atributos colaboradores e salarioBase, e métodos para contratação, demissão, cálculo de salário, pagamento e obtenção de relatório dos colaboradores. Linhas pontilhadas com setas indicam relações entre os elementos.

Temos um Colaborador e um Sistema, que possuem alguns métodos que precisam dos Cargos. O Sistema se liga ao Colaborador, porque possui um quadro de colaboradores, e nosso objetivo é resolver o fato do nosso Sistema ter tantos métodos, ou seja, tantas responsabilidades.

No nosso cenário ideal, de fundo verde, continuamos com o Colaborador e o enum, ou seja, os Cargos. Não vamos alterar essa parte, apenas não teremos mais uma classe Sistema, mas sim, novas quatro classes, cada uma com uma das responsabilidades do método Sistema.

Se vamos lidar com pagamento, temos a classe Pagamento. Para calcular o salário, teremos uma classe CalculaSalario, bem como uma classe apenas para gerar o relatório (GeraRelatorio) e outra para lidar com o quadro de colaboradores (QuadroColaboradores), onde vamos contratar, demitir e buscar esses colaboradores.

A implementação do que temos como solução, veremos na sequência.

Separando as responsabilidades - Dividindo as responsabilidades em classes

Vimos anteriormente que a nossa classe Sistema possuía alguns problemas. O primeiro deles é se quisermos utilizar o método de calcular um salário em outras partes do nosso código.

Temos um problema de autoacoplamento, pois ele depende de todos os outros métodos e atributos definidos no Sistema. Precisaríamos criar uma instância do Sistema para conseguir utilizar apenas esse método de calcular o salário. Outro ponto que vimos é a baixa coesão, onde ele assume responsabilidades que não são dele. Seria melhor, como vimos no nosso diagrama, separar as responsabilidades.

Voltando ao nosso diagrama, notamos que essa separação das responsabilidades foi resolvida criando quatro classes: Pagamento, CalculaSalario, GeraRelatorio e QuadroDeColaboradores, que vai substituir o nosso Sistema. E é assim que vamos começar.

Voltando ao nosso VS Code, criarems esses arquivos. Clicaremos pasta tarefa-1 e depois no botão "Novo arquivo", criando o Pagamento.ts. Repetiremos o processo para criarmos os arquivos CalculaSalario.ts e o GeraRelatorio.ts.

Criando a classe CalculaSalario

Feito isso, retornamos ao Sistema.ts, onde recortaremos cada uma das responsabilidades, ou seja, cada método correspondente a cada um dos arquivos. Começaremos com o método calcularSalario(), que está na nossa linha 22. Selecionamos todo o método, que vai da linha 22 até a 40, pressionamos "Ctrl + X" para recortá-lo.

calcularSalario(cargo: Cargos) {

        if (cargo === Cargos.Estagiario) {
                return this.salarioBase * 1.2;
        }

        else if (cargo === Cargos.Junior) {
                return this.salarioBase * 3;
        }

        else if (cargo === Cargos.Pleno) {
                return this.salarioBase * 5;
        }

        else if (cargo === Cargos.Senior) {
                return this.salarioBase * 7;
        }
        return 0;
}

Antes de colar esse código no arquivo CalculaSalario, precisamos exportar e definir a classe. Para isso, escrevemos export default class CalculaSalario{}. Dentro das chaves, colaremos o código que recortamos. O próprio VS Code já indica que precisamos fazer a importação dos nossos Cargos, então clicamos nele, que está na linha 2, com uma marcação de erro, e pressionamos "Ctrl + espaço" para concluir a importação.

import { Cargos } from "./enum/cargos";

export default class CalculaSalario {

    calcularSalario(cargo: Cargos) {

        if (cargo === Cargos.Estagiario) {
            return this.salarioBase * 1.2;
        }

        else if (cargo === Cargos.Junior) {
            return this.salarioBase * 3;
        }

        else if (cargo === Cargos.Pleno) {
            return this.salarioBase * 5;
        }

        else if (cargo === Cargos.Senior) {
            return this.salarioBase * 7;
        }
        return 0;
    }
}

Precisamos voltar ao Sistema.ts e recortar também o atributo salarioBase. Como também vamos precisar do construtor, selecionamos da linha 6, onde temos a definição dos atributos, até a linha 12, onde temos final do construtor.

private _colaboradores: Colaborador[];
protected salarioBase: number;

constructor(salarioBase: number = 1000) {
        
        this.salarioBase = salarioBase;
}

Voltamos para o CalculaSalario.ts e colamos esse trecho embaixo do nome da classe. Como não precisaremos de _colaboradores, podemos excluir a linha private _colaboradores: Colaborador[];. Consequentemente, excluiremos também a this._colaboradores = [] de dentro do construtor.

import { Cargos } from "./enum/cargos";

export default class CalculaSalario {
    protected salarioBase: number;

    constructor(salarioBase: number = 1000) {
        this.salarioBase = salarioBase;
    }

    calcularSalario(cargo: Cargos) {

        if (cargo === Cargos.Estagiario) {
            return this.salarioBase * 1.2;
        }

        else if (cargo === Cargos.Junior) {
            return this.salarioBase * 3;
        }

        else if (cargo === Cargos.Pleno) {
            return this.salarioBase * 5;
        }

        else if (cargo === Cargos.Senior) {
            return this.salarioBase * 7;
        }
        return 0;
    }
}

Agora sim, temos o salarioBase, o construtor e o método funcionando. Agora, se precisarmos, em outra parte do nosso código, calcular o salário, só precisamos dessa classe. Não temos mais nenhuma outra dependência, resolvendo a questão do autoacoplamento do nosso código.

Criando a classe Pagamento

Ainda temos algumas melhorias para fazer. Vamos voltar para o nosso Sistema.ts e agora recortar o método pagaColaborador(). Portanto, selecionamos da linha 24 a 27 e pressionamos "Ctrl + X". Abriemos agora o arquivo Pagamento.ts, onde começaremos codando export default class Pagamento{}. Dentro das chaves, colaremos o método pagaColaborador().

export default class Pagamento {
    paraColaborador(colaborador: Colaborador) {
        const salarioColaborador = this.calcularSalario(colaborador.cargo);
        colaborador.saldo = salarioColaborador;
    }
}

Como a classe já se refere a pagamento, renomearemos o método para apenas pagar(). Precisaremos fazer a importação do tipo Colaborador, que está marcado com erro. Em seguida, na linha 5, temos uma reclamação do TypeScript, porque vamos precisar de um método para calcular o salário, referente à classe que acabamos de criar.

Para isso, escreveremos nosso construtor(){} e, nos parênteses, digitamos: private servicoCalculaSalario, que é o nome que quero dar para essa classe, e ela será do tipo calculaSalario. Depois, na const salarioColaborador, podemos substituir o método por this.servicoCalculaSalario.calcularSalario. Portanto, o servicoCalculaSalario tem o método calculaSalario.

import CalculaSalario from "./CalculaSalario";
import Colaborador from "./Colaborador";

export default class Pagamento {

    constructor(private servicoCalculaSalario: CalculaSalario) { }

    pagar(colaborador: Colaborador) {
        const salarioColaborador = this.servicoCalculaSalario.calcularSalario(colaborador.cargo);
        colaborador.saldo = salarioColaborador;
    }
}

Aprimorando a clareza do código

Podemos fazer uma na nossa classe calculaSalario, mudando o nome do método calcularSalario. Agora que resolvemos a coesão da classe, o método faz exatamente o que a classe diz, assumindo uma única responsabilidade. Sendo assim, mudaremos o nome do método para calcular(), apenas, porque já sabemos que será calculado um salário.

import { Cargos } from "./enum/cargos";

export default class CalculaSalario {
    protected salarioBase: number;

    constructor(salarioBase: number = 1000) {
        this.salarioBase = salarioBase;
    }

    calcular(cargo: Cargos) {
            // código omitido
        }
}

E agora quando voltarmos para o Pagamento.ts, fica muito mais claro que o servicoCalculaSalario possui o método calcular. E assim finalizamos também o nosso pagamento.

import CalculaSalario from "./CalculaSalario";
import Colaborador from "./Colaborador";

export default class Pagamento {

    constructor(private servicoCalculaSalario: CalculaSalario) { }

    pagar(colaborador: Colaborador) {
        const salarioColaborador = this.servicoCalculaSalario.calcular(colaborador.cargo);
        colaborador.saldo = salarioColaborador;
    }
}

Criando a classe QuadroColaboradores

Agora que no Sistema.ts temos o método contratarColaborador() e demitirColaborador(), podemos renomear a classe para QuadroColaboradores, como tínhamos definido no diagrama. Para isso, no Explocador de Arquivos, à esquerda, selecionaremos Sistema.ts e pressionaremos "F2".

Mudaremos o nome para QuadroColaboradores e pressionaremos "Enter". Uma janela irá aparecer no centro da tela pedindo para atualizar as importações para QuadroColaboradores. Clicaremos no botão "Não", no canto inferior direito da janela, porque faremos essas mudanças manualmente.

Começando pela linha 4 do QuadroColaboradores.ts, porque o nome da classe agora será QuadroColaboradores. Repassando o código, percebemos que, nessa classe, temos os métodos:

O gerarRelatorioJSON() não tem relação com a classe QuadroColaboradores, e sim com o arquivo GeraRelatorio.ts, que criamos anteriormente. Então vamos recortar esse trecho de código e abrir o arquivo GeraRelatorio.ts.

Criando a classe GeraRelatorio

No começo do arquivo, novamente vamos digitar export default class GeraRelatorio{}. Dentro das chaves, colaremos o código que recortamos.

export default class GeraRelatorio {
    gerarRelatorioJSON() {
        let relatorio = this._colaboradores.map((colaborador) => {
            return ({
                nome: colaborador.nome,
                cargo: colaborador.cargo,
                salario: this.calculaSalario(colaborador.cargo),
            })
        })
        return JSON.stringify(relatorio)
    }
}

Precisamos dos colaboradores e do método calculoSalario, então faremos isso inserindo o nosso constructor(){} logo abaixo do nome da classe. Nos parênteses, codamos private _colaboradores: Colaborador[]. Após a array de Colaborador[], escrevemos uma vírgula, porque precisaremos também do private servicoCalculaSalario: CalculaSalario.

export default class GeraRelatorio {
    constructor(
        private quadroDeColaboradores: Colaborador[],
        private servicoCalculaSalario: CalculaSalario
    ) { }
        //Código omitido

Agora basta importarmos nosso Colaborador, que está marcado com erro, e modificar o método do salario para this.servicoCalculaSalario.calcular(). Assim conseguimos separar as funcionalidades do nosso relatório. Inclusive, podemos renomear o método de geraRelatorioJSON() para apenas gerarJSON.

import CalculaSalario from "./CalculaSalario"
import Colaborador from "./Colaborador"

export default class GeraRelatorio {
    constructor(
        private quadroDeColaboradores: Colaborador[],
        private servicoCalculaSalario: CalculaSalario
    ) { }

    gerarJSON() {

        let relatorio = this.quadroDeColaboradores.map((colaborador) => {
            return ({
                nome: colaborador.nome,
                cargo: colaborador.cargo,
                salario: this.servicoCalculaSalario.calcular(colaborador.cargo),
            })
        })
        return JSON.stringify(relatorio)
    };
}

Voltando ao nosso QuadroColaboradores, notamos que agora só possuímos funções relacionadas a pessoas colaboradoras. Assim, resolvemos o problema do autocoplamento e a coesão, porque agora o que é feito dentro da classe tem a ver com a definição dela.

Alterando a main.js para testarmos o código

Agora podemos testar todas essas funcionalidades integradas. Para isso, no menu explorar, à esquerda, abriremos o arquivo main.ts. Essa é a versão sem as alterações que fizemos, usando o Sistema. Na linha 5, criamos uma instância desse Sistema e criamos três colaboradores diferentes:

const sistema = new Sistema();

const colaborador1 = new Colaborador("José", Cargos.Estagiario);
const colaborador2 = new Colaborador("Maria", Cargos.Junior);
const colaborador3 = new Colaborador("João", Cargos.Pleno);

// código omitido

Após isso, contratamos cada um deles, adicionando essas pessoas no nosso array Colaboradores. Em seguida, mostramos um relatório de quem são as pessoas colaboradoras e fazemos uma operação para mostrar o salário do colaborador1 antes e depois de pagar o salário.

// código omitido

sistema.contratarColaborador(colaborador1);
sistema.contratarColaborador(colaborador2);
sistema.contratarColaborador(colaborador3);

console.log(sistema.gerarRelatorioJSON());

console.log(colaborador1);
sistema.pagaColaborador(colaborador1);
console.log(colaborador1);

Podemos agora alterar essa funcionalidade respeitando as responsabilidades que dividimos. Para isso vamos renomear onde tem sistema para quadroColaboradores e excluir o import Sistema from "./Sistema";, na linha 3.

Eu já tinha feito uma "cola" do que mais precisamos alterar, então vou apenas substituir no código e depois importaremos as instâncias novas, com "Ctrl + Espaço". Lembrando que na GeraRelatorio(), precisamos passar o quadroColaboradores e a calculaSalario. Já no Pagamento() passamos apenas o calcularSalario.

// importações omitidas

const quadroColaboradores = new QuadroColaboradores();
const calculaSalario = new CalculaSalario();
const geradorDeRelatorios = new GeraRelatorio(quadroColaboradores.colaboradores, calculaSalario);
const pagamento = new Pagamento(calculaSalario);

const colaborador1 = new Colaborador("José", Cargos.Estagiario);
const colaborador2 = new Colaborador("Maria", Cargos.Junior);
const colaborador3 = new Colaborador("João", Cargos.Pleno);

quadroColaboradores.contratarColaborador(colaborador1);
quadroColaboradores.contratarColaborador(colaborador2);
quadroColaboradores.contratarColaborador(colaborador3);

console.log(quadroColaboradores.gerarRelatorioJSON());

console.log(colaborador1);
quadroColaboradores.pagaColaborador(colaborador1);
console.log(colaborador1);

O que vamos fazer agora é, mantendo a criação dos colaboradores, que não precisamos alterar, assim como os códigos de quadroColaboradores.contratarColaborador(), precisamos agora gerar os relatórios. Essa não é mais uma responsabilidade do quadroColaboradores, e sim do nosso geradorDeRelatorios.geraJSON().

console.log(geradorDeRelatorios.gerarJSON());

Outra modificação que não é o quadroColaboradores é no pagamento. Quem lida com isso agora é o Pagamento, portanto, na linha 27, codamos pagamento.pagar(), manteremos o colaborador1 como parâmetro.

// importações omitidas

const quadroColaboradores = new QuadroColaboradores();
const calculaSalario = new CalculaSalario();
const geradorDeRelatorios = new GeraRelatorio(quadroColaboradores.colaboradores, calculaSalario);
const pagamento = new Pagamento(calculaSalario);

const colaborador1 = new Colaborador("José", Cargos.Estagiario);
const colaborador2 = new Colaborador("Maria", Cargos.Junior);
const colaborador3 = new Colaborador("João", Cargos.Pleno);

quadroColaboradores.contratarColaborador(colaborador1);
quadroColaboradores.contratarColaborador(colaborador2);
quadroColaboradores.contratarColaborador(colaborador3);

console.log(geradorDeRelatorios.gerarJSON());

console.log(colaborador1);
pagamento.pagar(colaborador1);
console.log(colaborador1);

Executando o teste

Agora para executarmos, eu deixei alguns scripts no arquivo package.jsom. Entre eles, temos o tarefa-1, que é referente a main dessa tarefa que acabamos de concluir. Para testarmos, abriremos o terminal, com o atalho "Ctrl + Shift + '", e codaremos:

npm run tarefa-1

Após aguardarmos alguns instantes, até compilar o nosso código. O primeiro retorno que temos é o console.log do relatório, onde temos os nossos três colaboradores retornados como JSON: José, Maria e João. Em seguida, temos o console.log de pagamento do colaborador 1, que antes tinha o saldo igual a zero e, depois de fazermos o pagamento tem como saldo de 1.200, que é o valor do salário dele.

Portanto, conseguimos fazer as melhorias, as refatorações, sem afetar o comportamento que tínhamos antes. E essa foi a aplicação do primeiro princípio, o princípio da responsabilidade única, em que vamos entender mais detalhes na sequência.

Sobre o curso SOLID com TypeScript: aplicando boas práticas em orientação a objetos

O curso SOLID com TypeScript: aplicando boas práticas em orientação a objetos possui 89 minutos de vídeos, em um total de 54 atividades. Gostou? Conheça nossos outros cursos de Node.JS em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Node.JS acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas