DEV Community

Gabriel_Silvestre
Gabriel_Silvestre

Posted on

Introdução ao SOLID - Princípios L e I

Tabela de Conteúdos


Liskov Substitution Principle

Recomendação

O Princípio de Substituição de Liskov diz que devemos poder utilizar uma sub-classe, no lugar de uma super-classe. Na prática isso ocorre através da implementação de Interfaces, ou através da herança de classes, dessa forma toda a sub-classe que implementa determinada interface, ou herda de determinada super-classe, deve poder ser usada como substituto.

Exemplo

A forma mais fácil de se entender o Princípio de Substituição de Liskov é através de classes que fazem a conexão com o banco de dados, dessa forma podemos ter diversas classes, responsáveis por diversos bancos, podendo se substituírem sem a geração de efeitos colaterais (bugs).

Pensando no contexto acima, vamos exemplificar a criação de um usuário em dois DB diferentes, o MySQL e o MongoDB. Para isso iremos criar duas classes que implementam uma mesma Interface e utilizá-las da mesma forma.

interface IUserRepository {
  create(name: string, age: number): Promise<void>;
}
Enter fullscreen mode Exit fullscreen mode
import mysql from 'mysql2/promise';  // Esse pacote foi utilizado apenas como exemplo

class UserRepositoryMySQL implements IUserRepository {
  constructor() {
    this.mysql = mysql.createPool({
      /* configuração da conexão */
    });
  }

    async create(name: string, age: number): Promise<void> {
      await this.mysql.execute(
        'INSERT INTO users (name, age) VALUES (?, ?);',
        [name, age]
      )
    }
  }
Enter fullscreen mode Exit fullscreen mode
import { User } from 'mongooseModels';  // pasta "fictícia"que armazena as Models do Mongoose

class UserRepositoryMongo implements IUserRepository {
  constructor() {
    this.userModel = new User();
  }

  async create(name: string, age: number): Promise<void> {
    await this.userMode.create({ name, age });
  }
}
Enter fullscreen mode Exit fullscreen mode
import { IUserRepository } from 'IUserRepository';

class CreateUSerService {
  constructor(private userRepository: IUserRepository) {}

  async execute(name: string, age: number) {
    this.userRepository.create(name, age);
  }
}
Enter fullscreen mode Exit fullscreen mode
import { Request, Response, NextFunction } from 'express';

import { UserRepositoryMySQL } from 'UserRepositoryMySQL';
import { UserRepositoryMongo } from 'UserRepositoryMongo';

import { CreateUserService } from 'CreateUserService';

const userRepositoryMySQL = new UserRepositoryMySQL();
const userRepositoryMongo = new UserRepositoryMongo();

/* ---------- Criando usuário no MySQL ---------- */
const createUserService = new CreateUserService(userRepositoryMySQL);
/* ---------- ----------------------- ---------- */

/* ---------- Criando usuário no MongoDB ---------- */
const createUserService = new CreateUserService(userRepositoryMongo);
/* ---------- ------------------------- ---------- */

const createUserRoute = async (req: Request, res: Response, next: NextFunction) => {
  const { name, age } = req.body;

  try {
    await createUserService.execute(name, age);
    res.status(200).end();
  } catch {
    res.status(500).json({ message: 'Internal server error' });
  }
}
Enter fullscreen mode Exit fullscreen mode

No exemplo acima criamos duas classes que lidam com DB diferentes, porém podemos utilizar qualquer uma das duas em nosso serviço de criação de usuário CreateUSerService, isso porque o serviço espera a Interface IUserRepository e como nossas classes implementam essa Interface podem ser usadas como substitutas.

Voltar ao top


Interface Segregation Principle

Recomendação

O Princípio de Segregação de Interfaces recomenda que separemos nossas Interfaces em "blocos mínimos", em outras palavras, as criemos altamente especializadas e caso surja a necessidade, podemos criar uma Interface mais completa estendendo as mais específicas.

Exemplo

O Princípio de Segregação de Interfaces é, na minha opinião, o mais simples de se entender o conceito teórico, porém o mais difícil de se aplicar em um caso real.

Nosso exemplo será uma classe de serviço de uma API, ele deverá obrigatoriamente possuir o método execute para executar sua tarefa e opcionalmente poderá possuir métodos de validação, como por exemplo: validar se um email já está em uso.

// Iremos utilizar generics para "tipar" o input <T> e o output <O> posteriormente

interface IServiceExecute<T, O> {
  execute(T): Promise<O>;
}

interface IServiceValidUnique<T> {
  isUnique(T): Promise<boolean>;
}
Enter fullscreen mode Exit fullscreen mode
interface IRequest {
  username: string;
  email: string;
  password: string;
}

interface ICreatedUser extends IRequest {
  id: string;
}

type UniqueUser = Pick<IRequest, 'email'>

class RegisterUserService implements IServiceExecute<IRequest | null, ICreateUser>, IServiceValidUnique<UniqueUser> {
  constructor(private repository: IRepository) {}

  async isUnique({ email }: UniqueUSer): Promise<boolean> {
    const emailAlreadyInUse = await this.repository.find(email);

    if (emailAlreadyInUse) {
      return false;
    }

    return true;
  }

  async execute({ username, email, password }: IRequest): Promise<ICreatedUser> {
    const isUnique = await this.isUnique({ email });

    if (!isUnique) {
      return null;
    }

    const newUSer = await this.repository.create({ username, email, password });
    return newUser;
  }
}
Enter fullscreen mode Exit fullscreen mode

No exemplo acima estamos aplicando duas Interfaces em uma única classe, isso porque cada Interface é responsável apenas por uma funcionalidade, dessa forma se precisarmos construir um serviço que não precise de validação, podemos apenas implementar a IServiceExecute.

Obs: As Interfaces criadas junto da classe são um "complemento" as outras, isso porque optei por utilizar Generics na criação das Interfaces de serviço, logo é necessário inferir seu tipo posteriormente através de tipos primitivos, types ou interfaces.

Voltar ao top


Links Úteis

Voltar ao top

Discussion (0)