L — Liskov Substitution Principle
Baixar PDFSubtipos 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,
PatoDeBorrachaquebra 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ário | Problema |
|---|---|
Array tem .push() mas ReadonlyArray não | Se a função chama .push(), não aceite ReadonlyArray como Array |
AdminUser tem deleteAll() mas GuestUser não | Se a função espera User, não chame métodos de AdminUser |
| Um repository que deveria retornar `User | null` 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:
- O subtipo consegue cumprir TODOS os métodos do tipo base? Se não, talvez a interface esteja grande demais (veja I — Interface Segregation).
- O subtipo pode substituir o tipo base sem alterar o comportamento do programa? Se o chamador precisa saber qual subtipo está recebendo → LSP violado.
- O chamador faz
instanceofpara decidir o que fazer? Isso é um sinal claro de violação de LSP.
Referências
O — Open/Closed Principle
Aberta para extensão, fechada para modificação. Exemplos full-stack com strategy pattern, middleware e componentes React.
I — Interface Segregation Principle
Ninguém deveria ser forçado a depender de métodos que não usa. Interfaces pequenas e específicas em TypeScript full-stack.