Projetos Práticos para Aprender SOLID
Camada prática para internalizar SRP, OCP, LSP, ISP e DIP por meio de projetos reais e enxutos.
Projeto 1: Sistema de Cadastro com SRP bem definido
Este projeto demonstra como dividir responsabilidades com clareza, evitando que uma única classe carregue lógica de validação, persistência e regras de negócios. A ideia é manter cada parte com uma responsabilidade única e coesa.
- Separar validação de entrada em um Validator independente.
- Extrair acesso a dados para um UserRepository específico.
- Encapsular regras de domínio em um serviço de usuário (UserService).
- Facilitar testes unitários ao ter componentes com responsabilidades bem definidas.
// Modelo de domínio
class User {
constructor(public id: string, public nome: string, public email: string) {}
}
// Validação isolada (SRP)
export interface IValidator { validate(item: T): boolean; }
export class UserValidator implements IValidator {
validate(u: User): boolean {
if (!u.nome || !u.email) return false;
// validações simples
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(u.email);
}
}
// Repositorio isolado (SRP)
export interface IUserRepository {
add(user: User): void;
getById(id: string): User | undefined;
}
export class UserRepository implements IUserRepository {
private items = new Map();
add(user: User): void { this.items.set(user.id, user); }
getById(id: string): User | undefined { return this.items.get(id); }
}
// Serviço de domínio (SRP)
export class UserService {
constructor(private repo: IUserRepository, private validator: IValidator) {}
register(user: User): boolean {
if (!this.validator.validate(user)) return false;
this.repo.add(user);
return true;
}
}
Projeto 2: Sistema de Notificações com OCP
Neste projeto, o objetivo é permitir a expansão de canais de notificação sem alterar o código existente. Novos canais devem ser adicionados por meio de novas implementações, mantendo o contrato estável.
- Definir uma abstração de canal de notificação (INotificationChannel).
- Fornecer implementações para Email, SMS, Push, etc.
- Adicionar novos canais sem modificar o código de envio existente.
export interface INotificationChannel {
send(to: string, message: string): Promise;
}
export class EmailNotification implements INotificationChannel {
async send(to: string, message: string) {
// lógica de envio de email
}
}
export class SmsNotification implements INotificationChannel {
async send(to: string, message: string) {
// lógica de envio de SMS
}
}
// Novo canal: PushNotification pode ser adicionado sem tocar no código existente
export class PushNotification implements INotificationChannel {
async send(to: string, message: string) {
// lógica de push
}
}
export class Notifier {
constructor(private channel: INotificationChannel) {}
notify(to: string, msg: string) {
return this.channel.send(to, msg);
}
}
Projeto 3: Editor de Relatórios com ISP e LSP
Para evitar interfaces inchadas, aplicamos Interface Segregation e garantimos que subclasses possam substituir bases sem quebrar o comportamento esperado.
- Dividir interfaces em pequenas responsabilidades: IPrintable, IExportable, etc.
- As classes derivadas devem respeitar o contrato da base (LSP) e não exigir métodos irrelevantes.
- Fácil extensão para novos formatos de exportação sem impactar clientes existentes.
// Interfaces segregadas
export interface IPrintable {
print(): string;
}
export interface IExportable {
export(format: 'json'|'csv'|'xml'): string;
}
// Classe que implanta apenas o que precisa
export class RelatorioResumo implements IPrintable, IExportable {
constructor(private data: any) {}
print(): string { return `Resumo: ${JSON.stringify(this.data)}`; }
export(format: 'json'|'csv'|'xml'): string {
switch (format) {
case 'json': return JSON.stringify(this.data);
case 'csv': return 'id,nome\n' + Object.values(this.data).join(','); // simplificado
case 'xml': return `${JSON.stringify(this.data)} `;
}
}
}
Projeto 4: Inversão de Dependência com DI simples
O princípio de inversão manda que dependamos de abstrações, não de implementações concretas. Aqui usamos interfaces para desacoplar camadas e facilitar testes e evolução.
- As classes de alto nível não devem depender de classes de baixo nível; ambas dependem de abstrações.
- Injeção de dependências via construtor facilita testes e flexibiliza substituições.
- Promove composição sobre acoplamento estático.
// Exemplo de DI simples
export interface IRepository {
add(item: T): void;
get(id: string): T | undefined;
}
export class User { constructor(public id: string, public name: string) {} }
export class UserRepository implements IRepository {
private store = new Map();
add(u: User) { this.store.set(u.id, u); }
get(id: string) { return this.store.get(id); }
}
export class UserService {
constructor(private repo: IRepository) {}
create(id: string, name: string) {
const u = new User(id, name);
this.repo.add(u);
}
}
// Composição via DI
const repo = new UserRepository();
const service = new UserService(repo);
service.create('u1', 'Ana Silva');
Explore mais conteúdos para ampliar sua visão sobre arquitetura de software e padrões de projeto.
Sou Apaixonado pela programação e estou trilhando o caminho de ter cada diz mais conhecimento e trazer toda minha experiência vinda do Design para a programação resultando em layouts incríveis e idéias inovadoras! Conecte-se Comigo!