Uma interface extensa parece conveniente porque reúne todas as operações de um recurso. Para os consumidores, porém, ela frequentemente cria dependências que não representam uma necessidade real.
O Interface Segregation Principle, ou ISP, recomenda que nenhum cliente dependa de métodos que não utiliza. O contrato deve ser definido pela necessidade de quem consome, não pelo inventário completo de quem implementa.
O problema que o princípio resolve
Um módulo de importação precisa receber um arquivo e ler seu conteúdo. Um painel administrativo também apaga arquivos. Se ambos dependem de uma interface única de armazenamento, o importador passa a conhecer capacidades destrutivas sem utilizá-las.
Isso dificulta implementações restritas, aumenta mocks de teste e amplia o impacto de mudanças no contrato.
Interfaces representam capacidades
Uma interface coesa responde a uma necessidade específica. Ela pode ter vários métodos, desde que mudem juntos e façam sentido para o mesmo consumidor.
Segregar não significa criar uma interface por método automaticamente. Significa impedir que operações sem relação sejam impostas ao cliente.
Exemplo ruim
type StoredFile = {
path: string;
sizeInBytes: number;
};
interface FileStorage {
upload(path: string, content: Buffer): Promise<StoredFile>;
download(path: string): Promise<Buffer>;
delete(path: string): Promise<void>;
list(prefix: string): Promise<StoredFile[]>;
createSignedUrl(path: string, expiresInSeconds: number): Promise<string>;
}
class ImportFileProcessor {
constructor(private readonly storage: FileStorage) {}
async process(path: string): Promise<number> {
const content = await this.storage.download(path);
const lines = content.toString("utf8").split("\n");
return lines.filter(Boolean).length;
}
}Por que o exemplo é ruim?
O processador utiliza somente download, mas seu tipo permite apagar, listar e publicar arquivos. Um armazenamento de anexos somente leitura precisa inventar quatro métodos ou lançar erros.
Nos testes, um objeto falso precisa declarar funções irrelevantes. Uma mudança na assinatura de URL temporária também força revisão de um importador que não conhece URLs.
Refatoração
Podemos modelar capacidades independentes:
interface FileReader {
download(path: string): Promise<Buffer>;
}
interface FileWriter {
upload(path: string, content: Buffer): Promise<StoredFile>;
}
interface FileRemover {
delete(path: string): Promise<void>;
}
interface FileCatalog {
list(prefix: string): Promise<StoredFile[]>;
}
interface FileUrlSigner {
createSignedUrl(path: string, expiresInSeconds: number): Promise<string>;
}Uma implementação completa pode satisfazer vários contratos. Os consumidores não precisam receber todos eles.
Exemplo bom
class ImportFileProcessor {
constructor(private readonly reader: FileReader) {}
async process(path: string): Promise<number> {
const content = await this.reader.download(path);
const lines = content.toString("utf8").split(/\r?\n/);
return lines.filter((line) => line.trim().length > 0).length;
}
}
class RemoveExpiredExports {
constructor(
private readonly catalog: FileCatalog,
private readonly remover: FileRemover
) {}
async execute(prefix: string): Promise<number> {
const files = await this.catalog.list(prefix);
await Promise.all(files.map((file) => this.remover.delete(file.path)));
return files.length;
}
}O primeiro caso de uso só pode ler. O segundo recebe exatamente as capacidades necessárias para encontrar e remover arquivos.
Testes menores
Um substituto para o importador agora é simples e comunica intenção:
import assert from "node:assert/strict";
import test from "node:test";
test("conta registros não vazios do arquivo", async () => {
const reader: FileReader = {
async download() {
return Buffer.from("invoice-1\n\ninvoice-2\n", "utf8");
}
};
const processor = new ImportFileProcessor(reader);
assert.equal(await processor.process("imports/today.csv"), 2);
});Não há stubs de upload, remoção ou assinatura. O teste descreve o único relacionamento do objeto.
Segregar pelo consumidor
Uma divisão puramente técnica pode continuar inadequada. Um fluxo de upload talvez sempre precise gravar e gerar uma URL. Nesse contexto, um contrato coeso pode reunir as duas operações.
Interfaces também podem nascer próximas ao caso de uso que as consome. Isso evita que um adaptador de infraestrutura dite um contrato enorme para toda a aplicação.
Benefícios
- dependências menores e explícitas;
- implementações sem métodos artificiais;
- menor superfície de permissões;
- testes com objetos simples;
- mudanças com impacto reduzido;
- contratos alinhados aos casos de uso.
Quando aplicar?
Quando projetar interfaces, pergunte-se se todas as classes realmente precisarão de todos os métodos. Se a resposta for "não", é a hora de aplicar o ISP.
Fechamento
Interface Segregation Principle transforma interfaces em descrições honestas de necessidade. Cada caso de uso recebe as capacidades que utiliza e nada além disso.
O resultado é menos código artificial, testes mais claros e uma arquitetura em que permissões e dependências ficam visíveis nos tipos.
Série SOLID
Este artigo faz parte de uma série de artigos: