I — Interface Segregation Principle
Baixar PDFNinguém deveria ser forçado a depender de métodos que não usa. Interfaces pequenas e específicas em TypeScript full-stack.
I — Interface Segregation Principle (ISP)
"Nenhum cliente deve ser forçado a depender de métodos que não utiliza." — Robert C. Martin
O que é, em português claro
Imagine um controle remoto universal com 200 botões — para TV, ar-condicionado, som, cortina elétrica, garagem. Você usa 5 botões para a TV. Mas o controle tem 195 botões que você nunca toca e que só complicam a vida.
ISP diz: prefira interfaces pequenas e específicas a uma interface grande e genérica. Quem precisa de TV usa o controle de TV. Quem precisa de ar usa o controle de ar.
Onde acontece na vida real
// ❌ Interface monolítica — força TODOS os serviços a implementar tudo
interface IService {
create(data: any): Promise<any>;
read(id: string): Promise<any>;
readAll(): Promise<any[]>;
update(id: string, data: any): Promise<any>;
delete(id: string): Promise<void>;
export(format: 'csv' | 'pdf'): Promise<Buffer>;
import(file: Buffer): Promise<void>;
subscribe(id: string, callback: Function): void;
getAnalytics(id: string): Promise<Analytics>;
}
// Um serviço de "Configurações do sistema" precisa de create/read/update
// Mas é FORÇADO a implementar export, import, subscribe, analytics
// Mesmo que nunca use — e o TypeScript exige que implementeExemplo prático — Frontend (React)
❌ Componente que recebe props que não usa
// Props monolítica — todo componente de lista recebe TUDO
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
onEdit: (item: T) => void;
onDelete: (item: T) => void;
onExport: (items: T[]) => void;
onImport: (file: File) => void;
onBulkAction: (action: string, items: T[]) => void;
showSearch: boolean;
showPagination: boolean;
showFilters: boolean;
}
// Lista de países — só precisa mostrar items
// Mas é FORÇADA a receber onEdit, onDelete, onImport...
function CountryList({ items, renderItem }: ListProps<Country>) {
// onEdit, onDelete, onExport, onImport... desestruturados mas ignorados
// Quem chama precisa passar tudo, mesmo que seja () => {}
return <ul>{items.map(item => <li>{renderItem(item)}</li>)}</ul>;
}✅ Interfaces compostas e específicas
// Interfaces pequenas — cada uma representa uma capacidade
interface Listable<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
interface Editable<T> {
onEdit: (item: T) => void;
onDelete: (item: T) => void;
}
interface Exportable<T> {
onExport: (items: T[]) => void;
onImport: (file: File) => void;
}
interface Searchable {
showSearch: boolean;
searchTerm: string;
onSearch: (term: string) => void;
}
// Composição — o componente recebe só o que precisa
function SimpleList<T>({ items, renderItem }: Listable<T>) {
return <ul>{items.map(item => <li>{renderItem(item)}</li>)}</ul>;
}
function EditableList<T>({ items, renderItem, onEdit, onDelete }: Listable<T> & Editable<T>) {
return (
<ul>
{items.map(item => (
<li>
{renderItem(item)}
<button onClick={() => onEdit(item)}>Editar</button>
<button onClick={() => onDelete(item)}>Remover</button>
</li>
))}
</ul>
);
}
// Uso — cada componente recebe só o que precisa
<SimpleList items={countries} renderItem={c => c.name} />
<EditableList items={users} renderItem={u => u.name} onEdit={editUser} onDelete={deleteUser} />Exemplo prático — Backend (NestJS)
❌ Repository com interface inchada
// Interface única para todos os repositories
interface IRepository<T> {
findAll(filter?: Filter<T>): Promise<T[]>;
findById(id: string): Promise<T | null>;
findOne(filter: Filter<T>): Promise<T | null>;
create(data: Partial<T>): Promise<T>;
createMany(data: Partial<T>[]): Promise<T[]>;
update(id: string, data: Partial<T>): Promise<T>;
delete(id: string): Promise<void>;
count(filter?: Filter<T>): Promise<number>;
aggregate(pipeline: any[]): Promise<any[]>;
watch(callback: (change: any) => void): void;
createIndexes(indexes: any[]): Promise<void>;
}
// AuditLog só precisa de create e findAll
// Mas é FORÇADO a implementar update, delete, watch, aggregate...
class AuditLogRepository implements IRepository<AuditLog> {
async update(id: string, data: Partial<AuditLog>): Promise<AuditLog> {
throw new Error('Audit logs não podem ser atualizados'); // ❌ Método morto
}
async delete(id: string): Promise<void> {
throw new Error('Audit logs não podem ser deletados'); // ❌ Método morto
}
// ... mais 9 métodos que nunca deveriam existir aqui
}✅ Interfaces segmentadas por capacidade
// Cada interface representa UMA capacidade
interface Readable<T> {
findById(id: string): Promise<T | null>;
findAll(filter?: Filter<T>): Promise<T[]>;
}
interface Writable<T> {
create(data: Partial<T>): Promise<T>;
update(id: string, data: Partial<T>): Promise<T>;
}
interface Deletable<T> {
delete(id: string): Promise<void>;
}
interface Countable {
count(filter?: Filter<any>): Promise<number>;
}
interface Aggregatable {
aggregate(pipeline: any[]): Promise<any[]>;
}
// Composição — cada repository implementa só o que precisa
class AuditLogRepository implements Readable<AuditLog>, Writable<AuditLog> {
// Só lê e cria — não deleta, não agrega, não faz watch
// E o TypeScript NÃO exige os outros métodos
}
class UserRepository implements Readable<User>, Writable<User>, Deletable<User>, Countable {
// CRUD completo + contagem — não agrega nem faz watch
}
class AnalyticsRepository implements Readable<Analytics>, Aggregatable {
// Lê e agrega — não escreve nem deleta
}// Service que depende só do que precisa — não vê métodos que não usa
class AuditService {
constructor(
private logs: Readable<AuditLog> & Writable<AuditLog>, // só precisa ler e criar
) {}
async log(action: string, userId: string) {
await this.logs.create({ action, userId, timestamp: new Date() });
}
async getRecent(limit: number) {
return this.logs.findAll({ sort: { timestamp: -1 }, limit });
}
}Exemplo prático — Backend (Express)
❌ Usuário com interface monolítica
// Uma interface só para "usuário" — mas nem todo endpoint precisa de tudo
interface User {
id: string;
name: string;
email: string;
password: string; // ❌ Rota pública nunca deveria ver isso
cpf: string; // ❌ Rota pública nunca deveria ver isso
address: Address; // ❌ Rota de login não precisa disso
paymentMethods: Card[]; // ❌ Rota de perfil público não precisa disso
role: 'admin' | 'user';
permissions: string[];
}
// GET /api/users/public-profile — retorna User inteiro, incluindo password hash
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user); // ❌ expõe password, cpf, paymentMethods
});✅ DTOs por contexto
// Cada rota vê só o que precisa
interface PublicUserProfile {
id: string;
name: string;
role: string;
}
interface PrivateUserProfile extends PublicUserProfile {
email: string;
address: Address;
}
interface AuthenticatedUser extends PrivateUserProfile {
permissions: string[];
}
// Rota pública — nunca vê password, cpf, paymentMethods
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
const profile: PublicUserProfile = { id: user.id, name: user.name, role: user.role };
res.json(profile); // ✅ só o que a rota pública precisa
});
// Rota de perfil privado — vê mais, mas não tudo
app.get('/api/users/:id/profile', authenticate, async (req, res) => {
const user = await db.users.findById(req.params.id);
const profile: PrivateUserProfile = pick(user, ['id', 'name', 'role', 'email', 'address']);
res.json(profile);
});Regra prática
- Uma interface com mais de 5-6 métodos provavelmente viola ISP. Pense se pode ser dividida.
- Implementações que têm métodos que só lançam
Error('not supported')são sinal de ISP violado. - Pergunte: "Esse componente/serviço realmente USA todos os métodos que a interface exige?"
Referências
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.
D — Dependency Inversion Principle
Dependa de abstrações, não de implementações concretas. Injeção de dependência em TypeScript full-stack — NestJS, Express e React.