0

So basically the ISP states we should break big interfaces with members that are not cohesive with each other to smaller and more cohesive interfaces, which is very close (if not the same) to what is stated in the SRP.

Assume we have a Printer class that implements the IPrinter interface and assume for a moment the premise that IPrinter is representing much more than it should. Does this mean that our Printer class is by consequence representing too much and should be broken into smaller classes as well? I.e. in our example, we should never have something like Printer : SmallerInterface1, SmallerInterface2 (little bit of C# syntax here), otherwise we would be violating the SRP, right?

So if we want to follow both the ISP and the SRP, by segregating an interface we should always segregate the classes that implement that interface?

0

2 Answers 2

3

Splitting the interface does not necessarily mean that you should split the class. You can think of interfaces as of a roles an object of a class is playing in the context of some client code. An object can play more than one role - e.g. picture two different clients "seeing" two different aspects of the same concept. The aspects themselves could be of a more general nature. E.g., consider IComparable: it allows a sorting algorithm to be written in terms of that interface, without the developer having to worry about anything else; at the same time, a concrete class implementing IComparable may have a more complex nature, and may implement other interfaces.

To deal with SRP, you can try to split the class into two (or more) separate concepts, but you can also extract parts of it into separate classes to delegate responsibilities to (composition). I.e. the source class can stay a single overall concept, but "relinquish" some of its original responsibilities to its constituent objects; it would essentially only orchestrate them, implementing a high-level policy.

Basically, if splitting the class would still require the objects to be very "chatty" and rely on each other's internals, and there's no real way around that, then splitting would hurt cohesion, and it's probably best not to do it. Such classes would still be coupled, and furthermore might couple other code that uses them.

So basically the ISP states we should break big interfaces with members that are not cohesive with each other to smaller and more cohesive interfaces

That's a partial picture; what's missing is that this is to be judged with respect to, or from the perspective of, clients (code that uses objects through these interfaces, code written against these interfaces). ISP states that clients shouldn't depend on stuff they don't use, even though it may make sense to bundle that stuff together into a single object.

I think it helps to contrast SRP and ISP in the following way. SRP is more focused on what the objects themselves are doing, and is about (1) splitting things that aren't closely related (and change for different reasons and with different rates), but also about (2) bringing together things that are closely connected, but are scattered throughout the code. ISP is more about controlling coupling by limiting the surface area exposed to client code. Splitting a large interface into more focused, smaller interfaces provides flexibility for the clients. It can even be useful if two interfaces segregated from the same source class appear in the same client, because this lets you plug something else for the particular "role" embodied by each interface. This lets you reuse that client code for some other feature that has a similar "shape", and it also allows for testing.

That said, there's always some judgement involved. If everything was taken to the extreme, then everything would be too granular, too separated to be usable, and so thoroughly decoupled that the system wouldn't be able to do anything. Sometimes you can't satisfy both principles to your liking for various reasons. And it's not always worth it; some parts of the codebase will work fine and won't change much, so expending design effort there would bring limited benefit.

3
  • Very well explained, thanks! However something caught my attention. When you say "It can even be useful if two interfaces segregated from the same source class appear in the same client" does this mean that we could have the same object being passed more than once for different parameters in the same call to work (from the client's perspective) on different purposes? Jul 3, 2021 at 12:40
  • @underthevoid - yes, exactly; it looks strange at first, but it's a known technique. Most classes probably won't be like that, but it's useful to remember that it's something you can do. E.g. suppose you have some processing function that takes an ICommandSource and an IOutput, which are fairly small, focused interfaces you came up with. If the app is started in GUI mode, you can pass in an instance of a window for both, but if it's started as a service, one could be a command stream, and the other some object that writes to stdout and/or the log file. Jul 3, 2021 at 15:51
  • @underthevoid - Or, you could use the same object for both, pass one "as is", but wrap the other in a decorator first to alter or augment its behavior (Decorator Pattern). Sometimes, decorators can be a bit of a pain to implement. But here, assuming the client-facing interface is small, it's easier because there's no need for the decorator to provide every public method or property on the object, it can just implement the narrow interface. Jul 3, 2021 at 15:51
2

Having a single responsibility does not mean "to do one thing". It means "to be one thing".

You want your printer class to be a printer only and not also an alarm clock and toaster just because it can make some noise and generate heat (how convenient).

SRP is about identity, ISP is about behaviors.

Indeed in many environments you too often see a class SomeClass implementing an interface ISomeClass. This is often a smell, interfaces are implemented just for the sake of it. With the mere purpose of using abstractions, because it says somewhere that using abstractions and decoupling is good. Those interfaces have the exact same set of methods as their class counterpart and get updated as often as the class itself. They are not helpful at all from a modeling point of view. But they allow you to build your test framework more easily. Still you should be testing behaviors separately, not objects, so it is hardly ever a good thing.

The number of methods in an interface should typically be small, like one or too. It can be more but if you go beyond this it is probably a good idea to stop and wonder whether you are still modeling a single behavior.

Let's do your printer. Its responsibility is to produce a hard copy of a document. That is one responsibility. To accomplish this it needs a number of behaviors. Like act as a network node that can receive a document. Page distribution settings. Drawer selection. Error notification.

The class declaration could look like this:

Printer : IIpNode, IPageSettings, IPrintErrors

This is helpful. It tells you immediately what the printer can do. IPrintErrors for example would have a subscription method for notifications.

My favorite example is a cat:

Cat : IScratcher, IPurrer, IJumper, IEater, IPooper

The interfaces tell you what kind of animal you are dealing with in terms of capabilities. It is one coherent meaningful animal which has a number of behaviors.

4
  • The cat example is great indeed.
    – Rik D
    Jul 3, 2021 at 10:26
  • Ok, I finally got it, thanks! Just a side question: what if a client class need the behaviors described by IEater and IPooper of the same object, how would that be done following those principles? How could this client class be assured that the same Cat (or another unique object) is being passed? Jul 3, 2021 at 12:50
  • 1
    @underthevoid - "How could this client class be assured that the same Cat [...] is being passed?" - basically, a client that takes an IEater and IPooper as separate dependencies is deliberately designed to not know if these are the same object or not. It would be the responsibility of the code that creates the client instance to make sure that the supplied dependencies are sensible. Jul 3, 2021 at 16:14
  • 1
    @underthevoid - If you deem necessary for these to be the same object, then you'd either make the client dependent on the Cat class, or perhaps on IMetabolizer: IEater, IPooper (you can have interfaces inheriting other interfaces). This can then be implemented by Cat or some other class (but thread carefully, we might be entering the borders of the overengineering land here; do this when you have reason to, but don't go off constructing these elaborate hierarchies preemptively). Jul 3, 2021 at 16:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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