Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptClean CodeSOLID

L — Liskov Substitution Principle

Subtipos devem ser substituíveis por seus tipos base sem quebrar o comportamento. O pato de borracha e outros exemplos full-stack.

L — Liskov Substitution Principle (LSP)

"Se S é um subtipo de T, então objetos do tipo T podem ser substituídos por objetos do tipo S sem alterar as propriedades corretas do programa." — Barbara Liskov (1987)

O que é, em português claro

Se seu código diz "preciso de um Pato", qualquer subtipo de Pato deveria funcionar sem causar erros.

A analogia clássica:

  • Pato nada e voa → pato.nada(), pato.voa()
  • PatoDeBorracha só quacka, não voa → se o código espera que todo Pato voe, PatoDeBorracha quebra a expectativa

Isso não é só sobre herança — é sobre contratos. Se uma função aceita um tipo base, todo subtipo deve respeitar o comportamento esperado.

Onde acontece na vida real

CenárioProblema
Array tem .push() mas ReadonlyArray nãoSe a função chama .push(), não aceite ReadonlyArray como Array
AdminUser tem deleteAll() mas GuestUser nãoSe a função espera User, não chame métodos de AdminUser
Um repository que deveria retornar `Usernull` mas lança exceção

Exemplo prático — Frontend (React)

❌ Componente que espera comportamento que nem todo subtipo tem

// Interface base
interface Animal {
  name: string;
  speak(): string;
  walk(): string;
}

// Subtipos
class Dog implements Animal {
  name = 'Rex';
  speak() { return 'Au au!'; }
  walk() { return 'Correndo no parque'; }
}

class Goldfish implements Animal {
  name = 'Nemo';
  speak() { return '...'; } // peixe não fala — mas a interface obriga
  walk() { return '...'; }   // peixe não anda — mas a interface obriga
}

// Componente que usa — espera que TODO animal fale e ande
function AnimalCard({ animal }: { animal: Animal }) {
  return (
    <div>
      <h3>{animal.name}</h3>
      <p>Fala: {animal.speak()}</p> {/* Nemo retorna '...' — enganoso */}
      <p>Anda: {animal.walk()}</p>   {/* Nemo retorna '...' — enganoso */}
    </div>
  );
}

✅ Interfaces que respeitam o contrato real

// Interfaces menores que representam comportamentos reais
interface Named {
  name: string;
}

interface Speakable {
  speak(): string;
}

interface Walkable {
  walk(): string;
}

// Cada animal implementa só o que realmente faz
class Dog implements Named, Speakable, Walkable {
  name = 'Rex';
  speak() { return 'Au au!'; }
  walk() { return 'Correndo no parque'; }
}

class Goldfish implements Named {
  name = 'Nemo';
  // Não implementa Speakable nem Walkable — e isso é OK
}

// Componentes que aceitam só o que precisam
function NameTag({ entity }: { entity: Named }) {
  return <h3>{entity.name}</h3>;
}

function SpeechBubble({ speaker }: { speaker: Speakable }) {
  return <p>{speaker.speak()}</p>;
}

// Composição — monta o card com os comportamentos que o animal realmente tem
function DogCard({ dog }: { dog: Dog }) {
  return (
    <div>
      <NameTag entity={dog} />
      <SpeechBubble speaker={dog} />
    </div>
  );
}

function GoldfishCard({ fish }: { fish: Named }) {
  return (
    <div>
      <NameTag entity={fish} />
      <p>Peixes não falam!</p>
    </div>
  );
}

Exemplo prático — Backend (NestJS)

❌ Repository base que subtipos não conseguem cumprir

// Interface base
interface Repository<T> {
  findAll(): Promise<T[]>;
  findById(id: string): Promise<T>;
  create(data: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}

// Implementação de cache — NÃO consegue deletar de verdade
class CachedUserRepository implements Repository<User> {
  async findAll(): Promise<User[]> { return this.cache.get('users') ?? []; }
  async findById(id: string): Promise<User> { /* ... */ }
  async create(data: Partial<User>): Promise<User> { /* ... */ }
  async delete(id: string): Promise<void> {
    // ❌ Cache não deleta do banco — o chamador espera que delete de verdade
    this.cache.invalidate(id);
    // O banco ainda tem o registro!
  }
}

✅ Interfaces que refletem capacidades reais

// Contratos separados por capacidade
interface ReadableRepository<T> {
  findAll(): Promise<T[]>;
  findById(id: string): Promise<T | null>;
}

interface WritableRepository<T> {
  create(data: Partial<T>): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T>;
}

interface DeletableRepository<T> {
  delete(id: string): Promise<void>;
}

// Implementações combinam só o que conseguem cumprir
class MongoUserRepository
  implements ReadableRepository<User>, WritableRepository<User>, DeletableRepository<User>
{
  // Pode ler, escrever e deletar — MongoDB suporta tudo
}

class CachedUserRepository implements ReadableRepository<User> {
  // Só lê do cache — não pode escrever nem deletar
  // E o TypeScript NÃO deixa ele ser usado onde se espera WritableRepository
}
// Service que usa só o que precisa — LSP respeitado
class UserReportService {
  constructor(private users: ReadableRepository<User>) {}
  // Só precisa ler — tanto faz se é Mongo, cache ou mock de teste

  async generate(): Promise<UserReport> {
    const users = await this.users.findAll();
    return { total: users.length, /* ... */ };
  }
}

Exemplo prático — Backend (Express)

❌ Middleware que subtipos não cumprem o contrato

// Contrato: middleware de auth DEVE adicionar req.user
interface AuthMiddleware {
  authenticate(req: Request, res: Response, next: NextFunction): void;
}

// JWT auth — cumpre o contrato ✅
class JwtAuth implements AuthMiddleware {
  authenticate(req: Request, res: Response, next: NextFunction) {
    const token = req.headers.authorization;
    req.user = verifyToken(token); // ✅ req.user é preenchido
    next();
  }
}

// API Key auth — NÃO cumpre o contrato ❌
class ApiKeyAuth implements AuthMiddleware {
  authenticate(req: Request, res: Response, next: NextFunction) {
    const key = req.headers['x-api-key'];
    if (key === process.env.API_KEY) {
      next(); // ❌ Não preenche req.user — handlers que esperam user vão quebrar
    }
  }
}

✅ Contratos claros

// Contrato separado para cada nível de autenticação
interface GuestAuth {
  authenticate(req: Request, res: Response, next: NextFunction): void;
}

interface UserAuth {
  authenticate(req: Request, res: Response, next: NextFunction): void;
  extractUser(req: Request): User; // garante que req.user existe
}

// Handler que precisa de user — aceita só UserAuth
function createProtectedRoute(handler: (req: AuthenticatedRequest) => Response) {
  return (req: Request, res: Response) => {
    const auth: UserAuth = getUserAuth(); // JWT, OAuth, etc.
    auth.authenticate(req, res, () => {
      const user = auth.extractUser(req); // ✅ garantido pelo contrato
      handler({ ...req, user });
    });
  };
}

Regra prática

Antes de criar uma herança ou implementar uma interface, pergunte:

  1. O subtipo consegue cumprir TODOS os métodos do tipo base? Se não, talvez a interface esteja grande demais (veja I — Interface Segregation).
  2. O subtipo pode substituir o tipo base sem alterar o comportamento do programa? Se o chamador precisa saber qual subtipo está recebendo → LSP violado.
  3. O chamador faz instanceof para decidir o que fazer? Isso é um sinal claro de violação de LSP.

Referências

On this page