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>;
}
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]
)
}
}
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 });
}
}
import { IUserRepository } from 'IUserRepository';
class CreateUSerService {
constructor(private userRepository: IUserRepository) {}
async execute(name: string, age: number) {
this.userRepository.create(name, age);
}
}
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' });
}
}
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.
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>;
}
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;
}
}
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
.
Top comments (0)