Aprenda SOLID com Projetos Práticos: Guia para Desenvolvedores

Aprenda SOLID com Projetos Práticos: Guia para Desenvolvedores





Projetos Práticos para Aprender SOLID


Projetos Práticos para Aprender SOLID

Camada prática para internalizar SRP, OCP, LSP, ISP e DIP por meio de projetos reais e enxutos.


Single Responsibility Principle

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;
  }
}

Open/Closed Principle

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);
  }
}

ISP e LSP

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)}`;
    }
  }
}

DIP – Dependency Inversion Principle

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');

Gostou? Este conjunto de projetos práticos visa transformar teoria em ações rápidas e seguras no código do dia a dia.

Explore mais conteúdos para ampliar sua visão sobre arquitetura de software e padrões de projeto.

Leia mais posts sobre SOLID
Padrões de projeto clássicos