7

According to one definition of "interface segregation principle" that states currently in Two contradicting definitions of Interface Segregation Principle – which one is correct?, a client should not depend on the methods that it doesn't call at all, for example:

public interface IData{
    public void create(String name,int age);
    public void delete();
}

public class DeleteUserActivity extends Activity{
    private IData userData;
    .
    .
    .
    onConfirmDeleteButtonPressed(){
        userData.delete();
    }
}

Since DeleteUserActivity only uses "delete" but not "create", so IData is violating "interface segregation principle", instead I should create 2 interfaces:

public interface ICreateData{
    public void create(String name,int age);
}

public interface IDeleteData{
    public void delete();
}

However, if the data is concrete, it is normal to group 2 methods together:

public class UserData{
    public void create(String name,int age){
    }
    public void delete(){
    }
}

public class DeleteUserActivity extends Activity{
    private UserData userData;
    .
    .
    .
    onConfirmDeleteButtonPressed(){
        userData.delete();
    }
}

And it is weird to separate 2 classes for each method:

public class UserData{
}

public class UserDataCreate{
    public void create(UserData userData,String name,int age){
    }
}

public class UserDataDelete{
    public void delete(UserData userData){
    }
}

And in practice, a real example : String, I almost never call getHashCode() on a String even I can do so.

My question is, why is "clients don't call all methods of a interface" a problem, but "clients don't call all methods of a concrete class" isn't? Why just "interface segregation principle" but not "method segregation principle" (introduce the principle to both interface methods and concrete methods)? What is the difference between depending on unused interface methods and depending on unused concrete class methods?

1
  • 9
    "I almost never call getHashCode() on a String" Do you ever put strings into a Map or a Set? HashMap and HashSet are the primary reason getHashCode exists on Object
    – Caleth
    Commented Jan 22 at 11:03

9 Answers 9

32

The principle of Low Coupling/High Cohesion states that functionality that is closely related and interdependent should be in the same class or module, and functionality that is not closely related should be in separate classes or modules.

UserData create() and delete() are conceptually related and probably also share the underlying storage and other logic in the implementation. So they should be in the same class. But if you have methods with unrelated logic like calculateTax() it would be in a separate module.

Principles like Interface Segregation, Single Responsiblity, and Separation of Concerns all put a lot of attention on splitting and separating things. But it is just as important to keep related things together. Splitting everying into atoms and single-method classes does not make for readable code.

1
  • 2
    Adding to this, the fact the OP named those atoms after the methods (ICreateData / IDeleteData) instead of a functionality (IDataAdministration) is a marker of the root design issue.
    – spectras
    Commented Jan 24 at 12:31
16

"clients don't call all methods of a interface" a problem, but "clients don't call all methods of a concrete class" isn't?

Says who?

Why just "interface segregation principle" but not "method segregation principle" (introduce the principle to both interface methods and concrete methods)? What is the difference between depending on unused interface methods and depending on unused concrete class methods?

You're confusing the keyword interface with the concept of interface. Concrete classes that don't implement an interface still have an interface. The ISP applies to what your calling code says it needs. Be it a keyword interface or a concrete class. Which is why it's rude to demand a String when all you need is a far more flexible CharSequence.

ISP is about not over stating your needs. Now sure, if you push ISP to extremes you end up needing an interface with every possible combination of methods. Or at least an interface for each method. Don't do that. Just understand that each unused method you're demanding anyway comes at a cost. But demanding the perfect interface also comes at a cost. If reasonably balancing this bugs you so much then go use a language with duck typing. No interface keyword to contend with at all. You'll find out if the method you wanted to call exists at run time.

4
  • 5
    A thing I see a lot of people forgetting is that the Principles aren't physical laws, they're starting points. With experience, you end up learning their limits and when to break away from them.
    – T. Sar
    Commented Jan 23 at 13:35
  • 11
    @T.Sar I think of them as sign posts warning you of hard to see costs. But if you obsess on one and ignore everything else they’ll run you over a cliff. Commented Jan 23 at 13:47
  • That's a good analogy!
    – T. Sar
    Commented Jan 23 at 14:05
  • 1
    @T.Sar - on writing forums, I used to say "you can break any rule of writing, as long as you understand why the rule exists and can explain why breaking it isn't a problem for your work." The same thing applies with software design principles.
    – occipita
    Commented Jan 24 at 17:14
7

There isn't a difference between "clients don't call all methods of a interface" and "clients don't call all methods of a concrete class". We can't really tell from a tiny example with one call site whether IData is actually worse than ICreateData / IDeleteData, and the same reasoning would apply to Data and CreateData / DeleteData.

This is because the public members of a concrete class also form an interface. The nomenclature around these principles predates Java, and is applicable in languages that don't have a keyword for it. Often when programmers are talking about interfaces, they don't mean only interface the Java keyword.

I wouldn't split here if there is one caller that doesn't use one of the methods. If the callers use overlapping groups of methods then splitting it up is detrimental.

However it is less detrimental in the Java interface case, because you can have class Data implements ICreateData, IDeleteData but not class Data extends CreateData, DeleteData, which would be the equivalent way of splitting with concrete classes.

6

Let's take the following advice:

Surgeons should scrub before they perform surgery.

Well, let's say Bob hears this advice and counters it, asking us if that means that untrained people shouldn't scrub before surgery?

There are two ways to respond to this:

  • Yes, Bob, everyone should scrub before they perform surgery.
  • No, Bob, untrained people shouldn't be performing surgery.

Bob's question implies that untrained people are performing surgery, which is an even bigger problem than the lack of scrubbing. Therefore, we don't answer Bob's direct question, instead redirecting the basis of his question so that it's clear that this situation (an untrained person performing surgery) shouldn't even happen in the first place.


Let's bring it back to your specific question and context.

So, ISP is the following advice:

Interfaces should be separated by their distinct purposes.

You're Bob in this scenario, asking us if that means that concrete classes shouldn't be separated. And there's two possible answers here:

  • Yes, wcminipgasker2023, separating any contract by their distinct purpose is a good idea.
  • No, wcminipgasker2023, we shouldn't be coupling to concrete classes in the first place.

And that is the answer I think you need to hear.

The reason why you're not seeing advice on if and how to separate methods the same way as interfaces is because it's a bad idea to use concrete classes for coupling in the first place, so people sidestep such advice in favor of pointing out that you shouldn't be doing it in the first place.


Some smaller points that I wanted to raise, though I think the above answer is closer to the target that you were asking about:

  • One could argue that SRP is to classes what ISP is to interfaces. There's a nuanced difference between them but from a high level they are very similar.
  • When used as intended, ISP already separates your methods even if the methods live in the same class, because you have no idea of knowing whether ICreate and IDelete are being implemented by the same class or by different classes. For example, I might have an IAccountant and an IOfficeManager, which in a small company/application could be represented by the same person/class and in a larger company/application are represented by different people/classes. What you're seeing here is the power of using interfaces, it removes a lot of concerns that you would otherwise have w.r.t. concrete classes, such as the concern that drove you to ask this question.
4

Because it leads to a million interfaces.

  • ICanDelete
  • ICanAdd
  • ICanAddAndDelete

etc

2
  • Actually, having a common interface with various ICanX methods can often be a good thing for classes which are frequently used with wrapper classes. If IEnumerable had included a count method, as well as an indication saying whether count was cheap, expensive, or meaningless, and whether it could change during the lifetime of an instance, then a class which is supposed to concatenate IEnumerable instances, could implement its count-related methods using those of constituent classes. If a "concat" wrapper combined a 10,000 item List with a 5-item iterable, its count method...
    – supercat
    Commented Jan 24 at 15:54
  • ...could quickly compute the number of items in the 10,000-item List and then iterate through the 5-item iterator, rather than having to individually iterate through all 10,005 items of the combined set.
    – supercat
    Commented Jan 24 at 15:55
3

You do not need it for the concrete case because there is already a different principle for that: program to interfaces

also, following the DIP will often lead you to using the interface (abstraction) instead of the concretion.

In General, these principles exist to protect you from changes in the concrete implementations. The less you know, the fewer things can affect you.

Edit to answer the question: there is no difference between depending on unused stuff in the abstraction or the concrete class. Its even fine to use the concrete classes, but be aware of the implications.

3

The interface segregation aims to avoid that classes implementing interfaces should not have to have to implement something that is not strictly needed.

The single responsibility is another principle, that aims to avoid that classes do not implement unrelated things, which would make their maintenance and use more difficult.

While this seems clear cut, there is of course some overlap: if you're going to extract an interface from a class which is not SRP, there is a high probability that it'll not be ISP either.

You will find a lot of good, but also bad, and even a few ugly articles/blogs on SOLID principles, due to misunderstandings. In case of doubts, the best is then to find an authoritative source. In the case of SOLID, it's R.C.Martin's original book on the subject.

3

These principles are largely language agnostic, so, the word "interface" doesn't mean a Java interface type (although, that's often what it will end up being in Java). The term is used in its more general sense: an interface of a class is the set of its public methods (and other public members). This usage of the term predates Java.

So, strictly speaking, it's not about abstract interface methods vs concrete methods; that's too narrow a picture - you also have to consider the clients who are dependent on the class, and the role this class plays within each of those clients. Also, the principle is more relevant to class hierarchies you control (so a general-purpose standard library class like String is not a good example).

The core issue here is that there are clients (or rather, business needs that drive the design and implementation of those clients) that dictate changes on the interface of their dependency, and if there are several such clients that are all dependent on the same type, those clients become coupled to each other.

One reason is that your service class will tend to have methods that are more general-purpose in nature, at a lower level of abstraction, because they'll try to cater to all those clients - so you'll call those same methods in many or all of the clients. And then a requirement will come in where you'll have to change the signature of some of those methods because of the needs of one client (thus changing the interface), but this change will nevertheless propagate to those other clients and force you to alter their implementations as well - even though it shouldn't, when you think about it (assuming the change was not due to some fundamental oversight in your understanding of the problem domain, that naturally affects the design more broadly).

Even if a client doesn't use the changed method, depending on the language, it may need to be recompiled, but this was more of a problem in the past (due to long compile times in C++) than it is today.

Often, the reason for this is that the clients in question are (or should be), in the context of clean architecture, in an inner layer compared to the service class, meaning that the dependencies flow towards them (and changes propagate in the other direction); the issue is that each client changes because of a different set of reasons, and the shared interface (that everything currently depends on) is affected by all those reasons simultaneously.

So, under those circumstances, what you do, generally speaking, is you approach the design in a bit more top-down way (starting from the clients, rather than from the service class) - you give each client a more narrow interface that each client can own (meaning, they change together, you logically bundle them together, you package them together in the same JAR), so that each interface serves more directly the needs of one specific client (or plays a specific role within that client). Thus, you segregate the interface as a first step; as a good second step, you look for opportunities to raise the level of abstraction of the methods to match more closely that appropriate within the context of the owning client (you may change the signature of some methods, or merge some methods into one, etc, so that you end up with an interface that is less reusable across clients, but that allows you to express the implementation of the owning client in a more straightforward way).

Going further, you can decide if the segregated interfaces should still be implemented by the same class, or if the class should be split. The segregation, and subsequent adjustments to the design (if any), also allow you to write simpler tests (and specifically, simpler test doubles), making your tests clearer and less brittle.

Note also that, sometimes, two of those segregated interfaces may end up playing different roles (as two distinct dependencies) on the same client (again, if they evolve for different reasons) - you may initially pass the same object for both (and distinct test doubles in your tests), but this also allows you to replace each dependency independently by a different implementation in production code - whether to satisfy a future requirement, or to implement several current requirements by providing different combinations of objects (by composing several instances of a client, each initialized with a different set of concrete dependencies).

3

Actually

why is "clients don't call all methods of a interface" a problem, but "clients don't call all methods of a concrete class" isn't?

is not the problem here. There is no problem in DeleteUserActivity in your examples, it can only call delete method from the interface. The actual problem can happen in the UserData, the class that implements the IData interface depending on the use case.

If you have an interface IData with create & delete methods, you're forcing classes that implements IData to have both of the methods. Now assume that you have a implementation that have valid use case only with the creation flow, i.e some sort of data that once created cannot be deleted. Then, that implementation should leave the delete method empty (or better one will be throwing an unsupported operation exception). The problem is then, when you call delete from IData, nothing may happen or you may get an exception.

It depends on your use case. If a situation similar to above exists, then it's better to have separate interfaces ICreateData & IDeleteData. You may also have a concrete class that implements both of the interfaces as well, i.e UserData in your example. You're not forbidding that. You just need to make your interfaces compact so that concrete implementations don't need to deal with flows that they don't care, i.e leaving a method empty or throwing an unsupported operation exception.

Not the answer you're looking for? Browse other questions tagged or ask your own question.