I have grown into using a principle for designing and consuming interfaces that says basically, “ask for only what you need.”
For instance, if I have a bunch of types that can be deleted, I’ll make a Deletable
interface:
interface Deletable {
void delete();
}
Then I can write a generic class:
class Deleter<T extends Deletable> {
void delete(T t) {
t.delete();
}
}
Elsewhere in code I will always ask for the smallest possible responsibility to fulfill the needs of the client code. So if I only need to delete a File
, I’ll still ask for a Deletable
, not a File
.
Is this principle that is common knowledge and already has an accepted name? Is it controversial? Is it discussed in textbooks?
1
I do believe that this refers to what Robert Martin calls the Interface Segregation Principle. Interfaces are separated into small and concise ones so that the consumers (clients) will only have to know about the methods that are of interest to them. You can check out more on SOLID.
To expand on Vadim’s very good answer, I will answer the “is it controversial” question with “no, not really”.
In general, interface segregation is a good thing, by reducing the overall number of “reasons to change” of the various objects involved. The core principle is, when an interface with multiple methods must be changed, say to add a parameter to one of the interface methods, then all consumers of the interface must at least be recompiled, even if they did not use the method that changed. “But it’s just a recompile!”, I hear you say; that may be true, but keep in mind that typically, anything you recompile must be pushed out as part of a software patch, no matter how significant the change to the binary. These rules were originally conceptualized back in the early 90s, when the average desktop workstation was less powerful than the phone in your pocket, 14.4k baud dial-up was blazin’, and 3.5″ 1.44MB “floppies” were the primary removable media. Even in the current era of 3G/4G, wireless internet users often have data plans with limits, so when releasing an upgrade, the fewer binaries that must be downloaded, the better.
However, like all good ideas, interface segregation can go bad if improperly implemented. First off, there is a possibility that by segregating interfaces while keeping the object that implements those interfaces (fulfilling the dependencies) relatively unchanged, you may end up with a “Hydra”, a relative of the “God Object” anti-pattern where the all-knowing, all-powerful nature of the object is hidden from the dependent by the narrow interfaces. You end up with a many-headed monster that is at least as hard to maintain as the God Object would be, plus the overhead of maintaining all its interfaces. There isn’t a hard number of interfaces that you shouldn’t exceed, but each interface you implement on a single object should be prefaced by answering the question, “Does this interface contribute to the object’s single, narrow core purpose, or goes it give the object a new purpose?”. If it’s the former, implement away; if it’s the latter, split the object in two to maintain the Single Responsibility Principle.
Second, an interface per method may not be necessary, despite what SRP may tell you. You may end up with “ravioli code”; so many bite-size chunks that it’s difficult to trace through to find out exactly where things actually happen. It’s also unnecessary to split an interface with two methods if all current users of that interface need both methods. Even if one of the dependent classes only needs one of the two methods, it’s generally acceptable to not split the interface if its methods conceptually have very high cohesion (good examples are “antonymic methods” which are exact opposites of each other).
Interface segregation should be based on the classes that are dependent on the interface:
-
If there is only one class dependent on the interface, don’t segregate. If the class doesn’t use one or more of the interface methods, and it is the interface’s only consumer, odds are you shouldn’t have exposed those methods in the first place.
-
If there is more than one class that is dependent on the interface, and all dependents use all methods of the interface, don’t segregate; if you must change the interface (to add a method or change a signature), all current consumers will be affected by the change whether you segregate or not (though if you’re adding a method that at least one dependent will not need, consider carefully if the change should instead be implemented as a new interface, possibly inheriting from the existing one).
-
If there is more than one class dependent on the interface, and they do not use all the same methods, it’s a candidate for segregation. Look at the “coherence” of the interface; do all methods further a single, very specific programming goal? If you can identify more than one core purpose for the interface (and its implementors), consider splitting the interfaces along those lines to create smaller interfaces with fewer “reasons to change”.
6