The Interface Segregation Principle says:
No client should be forced to depend on methods it does not use. ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.
There are a few unanswered questions here. One is:
How small?
You say:
Currently I deal with this by dividing the module's namespace depending on the requirements of its clients.
I call this manual duck typing. You build interfaces that expose only what a client needs. The interface segregation principle is not simply manual duck typing.
But the ISP is also not simply a call for "coherent" role interfaces that can be reused. No "coherent" role interface design can perfectly guard against the addition of a new client with it's own role needs.
ISP is a way to isolate clients from the impact of changes to the service. It was intended to make the build go faster as you make changes. Sure it has other benefits, like not breaking clients, but that was the main point. If I'm changing the services count()
function signature it's nice if clients that don't use count()
don't need to be edited and recompiled.
This is WHY I care about the Interface Segregation Principle. It's not something I take on faith as important. It solves a real problem.
So the way it should be applied should solve a problem for you. There is no brain dead rote way to apply ISP that can't be defeated with just the right example of a needed change. You are supposed to look at how the system is changing and make choices that will let things quiet down. Let's explore the options.
First ask yourself: is making changes to the service interface difficult right now? If not, go outside and play until you calm down. This isn't an intellectual exercise. Please be sure the cure isn't worse than the disease.
If many clients use the same subset of functions, that argues for "coherent" reusable interfaces. The subset likely focuses around one idea that we can think of as the role the service is providing to the client. It's nice when this works. This doesn't always work.
If many clients use different subsets of functions it's possible that the client is actually using the service through multiple roles. That's OK but it makes the roles hard to see. Find them and try to tease them apart. That may put us back in case 1. The client simply uses the service through more than one interface. Please don't start casting the service. If anything that would mean passing the service into the client more than once. That works but it makes me question if the service isn't a big ball of mud that needs to be broken up.
If many clients use different subsets but you don't see roles even allowing that the clients might use more than one then you have nothing better than duck typing to design your interfaces around. This way of designing the interfaces ensures that the client isn't exposed to even one function it isn't using but it almost guarantees that adding a new client will always involve adding a new interface that while the service implementation doesn't need to know about it the interface that aggregates the role interfaces will. We've simply traded one pain for another.
If many clients use different subsets, overlap, new clients are expected to added that will need unpredictable subsets, and you're unwilling to break up the service then consider a more functional solution. Since the first two options didn't work and you're really in a bad place where nothing is following a pattern and more changes are coming then consider providing each function it's own interface. Ending up here doesn't mean ISP has failed. If anything failed it was the object oriented paradigm. Single method interfaces follow ISP in the extreme. It's a fair bit of keyboard typing but you may find this suddenly makes the interfaces reusable. Again, be sure there isn't a simpler way to solve this problem before you do this but this is a solution that follows ISP and should isolate clients from changes to the service that they don't really care about.
So it turns out they can get very small indeed.
I've taken this question as a challenge to apply ISP in the most extreme cases. But bear in mind that extremes are best avoided. In a well thought out design that applies other SOLID principles these issues usually don't occur or matter, nearly as much.
Another unanswered question is:
Who owns these interfaces?
Over and over I see interfaces designed with what I call a "library" mentality. We all have been guilty of monkey-see-monkey-do coding where you're just doing something because that's how you saw it done. We are guilty of the same thing with interfaces.
When I look at an interface designed for a class in a library I used to think: oh, these guys are pros. This must be the right way to do an interface. What I was failing to understand is that a library boundary has it's own needs and issues. For one thing, a library is completely ignorant of the design of it's clients. Not every boundary is the same. And sometimes even the same boundary has different ways to cross it.
Here are two simple ways to look at interface design:
Service owned interface. Some people design every interface to expose everything a service can do. You can even find refactoring options in IDE's that will write an interface for you using whatever class you feed it.
Client owned interface. ISP seems to argue that this is right and service owned is wrong. You should break up every interface with the clients needs in mind. Since the client owns the interface it should define it.
So who's right?
Consider plugins:

Who owns the interfaces here? The clients? The services?
Turns out both.
The colors here are layers. The red layer (right) isn't supposed to know anything about the green layer (left). The green layer can be changed or replaced without touching the red layer. That way any green layer can be plugged into the red layer.
I like knowing what's supposed to know about what, and what isn't supposed to know. To me, "what knows about what?", is the single most important architectural question.
Let's make some vocabulary clear:
[Client] --> [Interface] <|-- [Service]
----- Flow ----- of ----- control ---->
A client is something that uses.
A service is something that is used.
Interactor
happens to be both.
ISP says break up interfaces for clients. Fine, lets apply that here:
Presenter
(a service) shouldn't dictate to the Output Port <I>
interface. The interface should be narrowed to what Interactor
(here acting as a client) needs. That means the interface KNOWS about the Interactor
and, to follow ISP, must change with it. And this is fine.
Interactor
(here acting as a service) shouldn't dictate to the Input Port <I>
interface. The interface should be narrowed to what Controller
(a client) needs. That means the interface KNOWS about the Controller
and, to follow ISP, must change with it. And this is not fine.
The second one is not fine because the red layer isn't supposed to know about the green layer. So is ISP wrong? Well kinda. No principle is absolute. This is a case where the goofs who like the interface to show everything the service can do turn out to be right.
At least, they're right if the Interactor
doesn't do anything other than this use case needs. If the Interactor
does things for other use cases there is no reason this Input Port <I>
has to know about them. Not sure why Interactor
can't just focus on one Use Case so this is a non issue, but stuff happens.
But the input port <I>
interface simply can't slave itself to the Controller
client and have this be a true plugin. This is a 'library' boundary. A completely different programming shop could be writing the green layer years after the red layer has been published.
If you're crossing a 'library' boundary and you feel the need to apply ISP even though you don't own the interface on the other side you're going to have to find a way to narrow the interface without changing it.
One way to pull that off is an adapter. Put it between clients like Controler
and the Input Port <I>
interface. The adapter accepts Interactor
as an Input Port <I>
and delegates it's work to it. However, it exposes only what clients like Controller
need through a role interface or interfaces owned by the green layer. The adapter doesn't follow ISP it self but allows more complex class like Controller
to enjoy ISP. This is useful if there are fewer adapters than clients like Controller
that use them and when you're in the unusual situation where you're crossing a library boundary and, despite being published, the library won't stop changing. Looking at you Firefox. Now those changes only break your adapters.
So what does this mean? It means honestly you haven't provided enough information for me to tell you what you should do. I don't know if not following ISP is causing you a problem. I don't know if following it wouldn't end up causing you more problems.
I do know you're looking for a simple guiding principle. ISP tries to be that. But it leaves a lot unsaid. I believe in it. Yes, please don't force clients to depend on methods that they don't use, without a good reason!
If you have a good reason, such as your designing something to accept plugins, then be aware of the problems not following ISP causes (it's hard to change without breaking clients), and ways to mitigate them (keep Interactor
or at least Input Port <I>
focused on one stable use case).