ISP - Segregação de Interfaces

TL;DR

Nenhum cliente deveria ser forçado a depender de métodos que ele não usa. Em vez de uma interface gorda que faz tudo, prefira várias interfaces pequenas, cada uma uma capacidade. Em uma linha: ISP é o SRP aplicado ao contrato — uma interface, uma razão para o cliente depender dela.

Imagine que você comprou uma impressora só para imprimir. Aí o manual te obriga a aprender a usar o scanner, o fax e a copiadora — mesmo que sua impressora não tenha nenhuma dessas coisas. Você assina um contrato com promessas que não consegue cumprir. Soa absurdo? É exatamente o que uma interface gorda faz com o código.

Esse é o Interface Segregation Principle (ISP): o “I” do SOLID. A frase canônica de Uncle Bob é direta: “clients should not be forced to depend upon interfaces that they do not use” — clientes não deveriam ser forçados a depender de interfaces (de métodos) que não usam.

A interface gorda e seus métodos zumbi

Vamos ao exemplo clássico. Um escritório tem uma multifuncional moderna que imprime, escaneia e envia fax. Tentado pela conveniência, alguém modela um contrato só:

// VIOLA ISP: interface gorda — promete TUDO a TODO mundo
public interface Multifuncional {
    void imprimir(Documento doc);
    void escanear(Documento doc);
    void enviarFax(Documento doc);
}

Funciona lindamente para a multifuncional, que de fato faz as três coisas. O problema aparece quando chega a impressora simples da recepção — a velha confiável que só imprime:

// O cliente é FORÇADO a implementar o que não tem
public class ImpressoraSimples implements Multifuncional {
    public void imprimir(Documento doc) { /* ...isto funciona... */ }
 
    public void escanear(Documento doc) {
        throw new UnsupportedOperationException("não escaneia");
    }
    public void enviarFax(Documento doc) {
        throw new UnsupportedOperationException("não tem fax");
    }
}

Repara nos dois métodos de baixo. Eles são zumbis: existem no código, mas estão mortos por dentro. Esse cheiro tem dois nomes que você precisa reconhecer na hora — stubs vazios (corpo vazio que finge atender) e UnsupportedOperationException (lança erro porque não há o que fazer). Sempre que você escreve um desses, pare: a interface está mentindo sobre o que o objeto consegue.

E note o veneno escondido: esse stub não é só feio, é uma bomba de LSP. Quem recebe um Multifuncional acredita que pode chamar escanear(...). Passe uma ImpressoraSimples no lugar e o programa explode em runtime. A interface gorda quebrou a substituibilidade — ISP e LSP estão de mãos dadas aqui.

A virada: separe por capacidade

A correção do ISP é fatiar a interface gorda em capacidades coesas. Cada coisa que o objeto pode fazer vira uma interface pequena e independente:

public interface Impressora { void imprimir(Documento doc); }
public interface Scanner    { void escanear(Documento doc); }
public interface Fax        { void enviarFax(Documento doc); }

Agora cada cliente assina só os contratos que consegue honrar. A impressora simples declara que imprime, e ponto:

public class ImpressoraSimples implements Impressora {
    public void imprimir(Documento doc) { /* ...e nada mais... */ }
}
 
// A multifuncional compõe as capacidades que ela realmente tem:
public class MFP implements Impressora, Scanner, Fax {
    public void imprimir(Documento doc)  { /* ... */ }
    public void escanear(Documento doc)  { /* ... */ }
    public void enviarFax(Documento doc) { /* ... */ }
}

Sumiram os stubs. Sumiu o UnsupportedOperationException. E agora um consumidor pode pedir exatamente a capacidade de que precisa: um método que só imprime recebe Impressora, não a multifuncional inteira. Ele depende do mínimo — e portanto só recompila ou quebra quando o que ele usa de verdade muda.

classDiagram
    direction LR
    class Multifuncional {
        <<interface>>
        +imprimir(Documento)
        +escanear(Documento)
        +enviarFax(Documento)
    }
    class ImpressoraSimples_ruim {
        +imprimir() ok
        +escanear() throws!
        +enviarFax() throws!
    }
    ImpressoraSimples_ruim ..|> Multifuncional
    note for ImpressoraSimples_ruim "forcada a stubs zumbi\nUnsupportedOperationException"

    class Impressora {
        <<interface>>
        +imprimir(Documento)
    }
    class Scanner {
        <<interface>>
        +escanear(Documento)
    }
    class Fax {
        <<interface>>
        +enviarFax(Documento)
    }
    class ImpressoraSimples {
        +imprimir()
    }
    class MFP {
        +imprimir()
        +escanear()
        +enviarFax()
    }
    ImpressoraSimples ..|> Impressora
    MFP ..|> Impressora
    MFP ..|> Scanner
    MFP ..|> Fax
    note for MFP "compoe so o que ELA tem"

Leitura do diagrama: em cima, o desenho gordo — ImpressoraSimples é obrigada a realizar (..|>) a Multifuncional inteira e por isso carrega os métodos que lançam exceção. Embaixo, o desenho segregado — três interfaces pequenas. ImpressoraSimples pendura só em Impressora; a MFP compõe as três porque de fato faz as três. Cada seta de realização agora é uma promessa que o objeto consegue cumprir.

Por que isso importa: o acoplamento invisível

“Mas é só um método a mais na interface, qual o drama?” O drama é o acoplamento desnecessário. Quando dez clientes dependem de uma interface gorda, eles ficam amarrados uns aos outros pela interface — mesmo sem se conhecerem.

Pensa numa interface com imprimir(), escanear() e enviarFax(). Você muda a assinatura de enviarFax() por um requisito de fax. Em uma compilação estática como Java, todo mundo que implementa a interface precisa recompilar — inclusive a ImpressoraSimples, que não tem nem ideia do que é fax. Um cliente foi sacudido por uma mudança em um método que ele nunca usou. Isso é acoplamento no pior sabor: invisível e gratuito.

ISP corta esse fio. Interfaces pequenas significam que uma mudança no fax toca só quem usa fax. O raio de impacto encolhe. É por isso que a regra prática é: a forma da interface deve ser ditada pelo cliente que a consome, não pelo objeto que a implementa.

flowchart TD
    A["Olhando uma interface\ne um cliente que a usa"] --> B{"Este cliente usa\nTODOS os metodos\nda interface?"}
    B -->|"Sim: ele chama\ntodos de verdade"| C["Interface coesa\nbom tamanho, deixe assim"]
    B -->|"Nao: ele so usa\num subconjunto"| D["Cheiro de interface gorda"]
    D --> E["Segregue: extraia\numa interface menor\ncom so o que ele usa"]
    E --> F["Cliente depende do minimo\nmenos recompilacao, menos stub"]

Leitura do diagrama: o losango é a pergunta-régua do ISP — este cliente usa todos os métodos da interface? Se sim, a interface está coesa, do tamanho certo: não invente fatias. Se não — se o cliente só toca um pedaço — você tem o cheiro da interface gorda e a saída é segregar: extrair uma interface menor com exatamente o que aquele cliente consome.

ISP é SRP no nível do contrato

Se essa pergunta soou familiar, é porque ela é parente do SRP. O SRP diz que uma classe deveria ter uma única razão para mudar. O ISP diz a mesma coisa sobre a interface: cada interface deveria responder a um grupo de clientes, e portanto ter uma única razão (um único cliente/capacidade) para mudar de forma.

É o mesmo princípio de coesão aplicado a camadas diferentes. SRP olha a coesão da implementação; ISP olha a coesão do contrato. Uma interface gorda é, por definição, uma interface pouco coesa — ela junta capacidades que não pertencem ao mesmo cliente. Segregar é restaurar a coesão.

Entre linguagens: Go já nasce com ISP

Aqui vale uma volta por como o OO difere entre linguagens, porque Go ilustra o ISP de um jeito quase poético. A cultura de Go prega interfaces minúsculas — frequentemente de um único método. As mais famosas da biblioteca padrão são io.Reader e io.Writer:

// A interface mais usada de Go tem UM metodo:
type Reader interface {
    Read(p []byte) (n int, err error)
}
 
type Writer interface {
    Write(p []byte) (n int, err error)
}

Dois fatores empurram Go nessa direção. Primeiro, a tipagem estrutural (duck typing em tempo de compilação): em Go você não declara implements — qualquer tipo que tenha o método Read já é um Reader, automaticamente. Isso barateia muito ter dezenas de interfaces pequenas e compô-las quando precisa de mais (io.ReadWriter é só Reader

  • Writer). Segundo, o ditado da comunidade: “the bigger the interface, the weaker the abstraction” — quanto maior a interface, mais fraca a abstração.

O contraste com Java é instrutivo. Em Java você precisa do implements explícito e tende a interfaces maiores; o ISP é uma disciplina que você aplica conscientemente. Em Go, a linguagem inclina você ao ISP por design. Mesmo princípio, ergonomias diferentes — o detalhe de como interfaces e classes abstratas se declaram em cada linguagem muda o quanto o ISP “dói” ou “flui”.

Em entrevista

ISP é curto de definir, mas o senior se separa do júnior ao (a) nomear o cheiro — stubs e UnsupportedOperationException — e (b) conectar com LSP e coesão.

  • Definição em uma frase: “Clients should not be forced to depend on methods they don’t use. Prefer many small, role-based interfaces over one fat interface.”
  • O cheiro que delata: “The tell-tale sign of an ISP violation is an implementation full of empty stubs or methods that throw UnsupportedOperationException — the object is being forced to honor a contract it can’t fulfill.”
  • A conexão com LSP: “A fat interface often breaks Liskov too: a client expects all the methods to work, but a partial implementer blows up at runtime. ISP prevents that whole class of bug.”
  • A regra de ouro: “Interfaces should be shaped by the client that consumes them, not by the class that implements them. ISP is basically the Single Responsibility Principle applied to interfaces.”
  • Entre linguagens (mostra alcance): “Go bakes this in — its standard library favors tiny, single-method interfaces like io.Reader, and structural typing makes composing them cheap.”
  • Se cobrarem a história: “Uncle Bob coined ISP at Xerox, where one giant Job class forced every client to depend on methods for jobs it never ran.”

Vocabulário PT → EN:

  • interface gorda → fat interface
  • segregar interfaces → to segregate interfaces
  • interface por papel / capacidade → role interface / capability
  • ser forçado a depender de → to be forced to depend on
  • método que não usa → method it doesn’t use
  • stub vazio → empty stub
  • método zumbi / morto → dead method
  • acoplamento desnecessário → needless coupling
  • coesão de interface → interface cohesion
  • recompilar → to recompile
  • tipagem estrutural → structural typing
  • compor interfaces → to compose interfaces

Lastro

O Interface Segregation Principle foi formulado por Robert C. Martin (Uncle Bob), que o articulou no artigo “The Interface Segregation Principle” no C++ Report (1996) e depois nos livros Agile Software Development e Clean Architecture. A origem é real: Martin consultava na Xerox, onde uma única classe Job gigante servia a todos os tipos de tarefa (imprimir, grampear, faxar), forçando cada cliente a depender de métodos de tarefas que não executava — e cada mudança disparava um ciclo de recompilação/redeploy de quase uma hora. O exemplo da multifuncional / impressora-scanner-fax que ensino acima é a didatização moderna mais comum (não o caso Xerox literal), mas captura fielmente a mesma dor. A relação que afirmo entre ISP e coesão de interface (ISP como SRP no nível do contrato) é interpretação amplamente aceita, mas é uma leitura — Martin enfatiza “clientes forçados a depender”, e a coesão é a outra face dessa moeda. Fontes: Interface segregation principle — Wikipedia; “The Interface Segregation Principle” — Robert C. Martin, C++ Report (1996); Go Proverbs — go-proverbs.github.io.

Veja também