Interface Segregation Principle (ISP)
O Interface Segregation Principle diz:
Clientes não devem ser forçados a depender de métodos que não utilizam.
Em termos mais práticos:
É melhor ter várias interfaces pequenas e específicas do que uma interface grande e genérica.
Se o SRP fala sobre responsabilidade de classes, o ISP fala sobre responsabilidade de interfaces.
Fazendo um paralelo com Go, podemos pensar em interfaces como contratos que definem o comportamento esperado. O ISP nos orienta a manter esses contratos enxutos e focados, evitando que clientes sejam obrigados a implementar ou depender de métodos que não fazem sentido para eles.
A interface “faz tudo”
Imagine uma API simples de usuários. Você começa com algo aparentemente conveniente:
type UserRepository interface {
Save(user User) error
FindByID(id string) (User, error)
Delete(id string) error
CountActiveUsers() (int, error)
GenerateReport() ([]byte, error)
}
No começo parece normal. Mas observe o que está acontecendo aqui: essa interface mistura várias responsabilidades:
- Persistência
- Consulta
- Métrica
- Geração de relatório
Agora imagine um caso de uso simples:
type CreateUserUseCase struct {
repo UserRepository
}
Foi determinado anteriormente que o caso de uso só precisa de Save, mas ele depende também de:
DeleteCountActiveUsersGenerateReport
Essas funções não são utilizadas por esse caso de uso, e mesmo assim ele depende que todas sejam implementadas para satisfazer o contrato da interface. Esse é exatamente o comportamento que o ISP tenta evitar.
Quais problemas surgem com essa implementação?
- Interfaces crescem de forma descontrolada.
- Mocks ficam complexos e difíceis de manter.
- Mudanças em um método afetam clientes que não deveriam ser impactados.
- Métodos são implementados apenas para satisfazer o contrato.
- Surge o clássico:
panic("not implemented").
Esses são sinais claros de violação do ISP.
Como aplicar o ISP na prática?
Vamos segregar as responsabilidades:
// Interface para persistência
type UserSaver interface {
Save(user User) error
}
// Interface para consulta
type UserFinder interface {
FindByID(id string) (User, error)
}
// Interface para métricas
type UserCounter interface {
CountActiveUsers() (int, error)
}
Dessa forma, as responsabilidades ficam separadas e cada caso de uso depende apenas do que realmente precisa:
type CreateUserUseCase struct {
saver UserSaver
}
Agora temos:
- Baixo acoplamento
- Testes mais simples
- Mocks menores e mais claros
- Mudanças isoladas
- Código mais coeso
Em Go, o ISP é quase natural
Existe um ponto interessante aqui: em Go, quem consome define a interface.
Isso favorece naturalmente o ISP.
Exemplo clássico da standard library:
type Reader interface {
Read(p []byte) (n int, err error)
}
- Pequena
- Específica
- Fácil de compor
A própria filosofia da linguagem incentiva interfaces pequenas e focadas, tornando a aplicação do ISP algo bastante orgânico no ecossistema Go.
Go Playground
Você pode ver um exemplo completo funcionando no Go Playground:
https://go.dev/play/p/FrRi7U2KwuE
Observe que o CreateUserUseCase não sabe que existem métodos como Delete, GenerateReport ou CountActiveUsers. Ele depende exclusivamente do que precisa.
Isso é ISP na prática.
E quando surge uma nova “entidade”?
Uma dúvida comum ao aplicar ISP é:
“Se agora eu preciso criar uma nova entidade, devo criar outra interface separada?”
A resposta é: depende de quem está consumindo a interface.
O ISP não é sobre quantidade de métodos. É sobre evitar que um cliente dependa de comportamentos que não utiliza.
Cenário 1 — Administrador é apenas um tipo de User
Se “Administrador” é apenas um User com uma role diferente, nada muda estruturalmente.
Você pode continuar usando uma interface pequena:
type UserSaver interface {
Save(user User) error
}
E o caso de uso:
type CreateAdminUseCase struct {
saver UserSaver
}
Aqui o ISP está sendo respeitado, pois o caso de uso depende apenas do que precisa.
Cenário 2 — Administrador possui novos comportamentos
Agora imagine que administradores podem:
- Suspender usuários
- Gerar relatórios
- Listar todos os usuários
Um erro comum seria criar uma interface gigante baseada na entidade:
type AdminRepository interface {
Save(user User) error
FindByID(id string) (User, error)
Delete(id string) error
CountActiveUsers() (int, error)
GenerateReport() ([]byte, error)
SuspendUser(id string) error
}
Isso não é automaticamente errado.
A violação ocorre quando um caso de uso depende dessa interface inteira, mas utiliza apenas um método.
Exemplo:
type SuspendUserUseCase struct {
repo AdminRepository
}
Se ele utiliza apenas SuspendUser, então está dependendo de métodos que não usa.
A abordagem idiomática em Go
Em Go, é mais comum definir interfaces a partir do consumo:
type UserSuspender interface {
SuspendUser(id string) error
}
E o caso de uso:
type SuspendUserUseCase struct {
suspender UserSuspender
}
Agora a interface é pequena, específica e alinhada ao que o cliente realmente precisa.
Conclusão
Não crie outra interface apenas porque surgiu outro “tipo” de usuário.
Crie outra interface quando surgir um novo conjunto de comportamentos necessários para um cliente específico.
O ISP é orientado ao consumidor, não à entidade.
- Interfaces grandes são um cheiro arquitetural.
- Prefira interfaces pequenas e específicas.
- Quem consome define a interface.
- Se você está escrevendo
panic("not implemented"), provavelmente violou o ISP. - Em Go, aplicar ISP é quase o caminho natural.
Referências
https://en.wikipedia.org/wiki/Interface_segregation_principle https://schembri.me/solid-interface-segregation-principle-isp/ https://blog.cleancoder.com/uncle-bob/2020/10/18/Solid-Relevance.html
