This is one of a number of things that's been bugging me for a while and for which quibbling over the correct interpretation of this has been leading me in a number of attempted coding projects to more fussing around with design, than it has with making steady forward progress, because I want to make sure I've gotten it "right".
In this case, I have a question about the "interface segregation principle" (ISP) of the five "SOLID" principles of basic object oriented design. Its "canonical" statement is this:
Clients should not be forced to depend on methods they do not use.
However, the question here is, is what is the "client" - because this leads to very different interpretations, and also leads to a seeming dilemma when applied in practice, which is what I'd like to understand if first actually is one, and second, how to best solve it.
One interpretation of this is that it is a "single responsibility principle" for interfaces instead of just classes: i.e. if you have an interface IMessageReceiver
, it better not include a Send
method. Another possible interpretation is that it means that the implementer of an interface should not be required to implement empty methods, while a third interpretation is that the caller should not be able to see any more methods than it actually needs. And the thing is, I can see merit in all three of these interpretations, yet when you apply them all, it seems that in any suitably large project, highly counterintuitive and seemingly problematic things result. In particular, it's that third one that seems to be the rub.
For one, if something gets used in enough places, it is particularly that last one - the "caller" one - which generally tends to "bite" in that it results naturally in your interfaces being atomized down to single methods only. For example, consider a simple interface to a backend storage or database, which may have a load
, save
, and update
method. Some callers of that, though, may not want to save anything. They may just want to peek at the data. Hence to avoid violating the caller interpretation, we must split off the load method. Add a few more use cases and now it's atomized into a IDataLoader
, IDataSaver
, and IDataUpdater
which all have 1 method each.
And I can't help but feel this almost seems like an anti-pattern, particularly if it happens with enough objects owing to them being used in a suitably wide variety of places. Is it? Or am I misinterpreting something here? After all, nobody makes a generic container class (like the "list", "map", etc. things in a lot of languages) with single-method interfaces to be piecemealed out to whatever it gets passed to.
CodePudding user response:
One interpretation of this is that it is a "single responsibility principle" for interfaces...
Robert Martin has addressed this question directly, and his answer is that ISP separates things that SRP does not, thus the two principles differ.
Another possible interpretation is that it means that the implementer of an interface...
We also see in his answer that the client is the caller of the interface, not the implementer. I've touched on that in another answer regarding default methods. The implementer should be implementing exactly as many methods as the caller needs: no more, no less.
a third interpretation is that the caller should not be able to see any more methods than it actually needs.
This is correct, and it is also correct to note that a single-method interface cannot violate the ISP. While this may result in a "lowest-common-denominator" approach where every interface is a single method, they should be highly composable in that case; i.e. implementations can select several interfaces to implement, making them highly customizable to client needs.
The database example of separating read from write is so common that is has its own pattern: CQRS. There are arguments both ways. Likewise, generic collections may choose to avoid creating too many interfaces, which is a design counter to the ISP.
I would suggest that in most applications (particularly service applications as opposed to libraries) if rigorously applying the ISP results exclusively in single-method interfaces, it indicates the clients are not cohesive and not complex. If this is an accurate description of the clients, then single-method interfaces are probably appropriate. You may also consider a different architecture such as a separate service for each client if their requirements have so little overlap.
CodePudding user response:
SRP means that a class is responsible just for one feature. E.g. if you want to edit logging in Person class, then it is violation of SRP.
public class Person
{
public string FirstName { get; set; }
public void Say(string message)
{
File.WriteAllText("path", message); // violation of SRP
}
}
How can we avoid violation of SRP? We should create an abstraction of logging:
public interface ILogging
{
void Write(string address, string content);
}
and use it:
public class PersonWithSRP
{
private readonly ILogging _logging;
public string FirstName { get; set; }
public PersonWithSRP(ILogging logging)
{
_logging = logging;
}
public void Say(string message)
{
_logging.Write("path", message);
}
}
public class Logging : ILogging
{
public void Write(string address, string content)
{
File.WriteAllText(address, content);
}
}
By extracting logic of logging in separate class, we moved logic of logging in special, single class or place where we can edit only logic of logging on one place.
What is about ISP?
nobody makes a generic container class
yeah, you are right. Interface should have only necessary methods which client class needs.
An example with HDD that uses ISP:
public interface IReadable
{
string Read();
}
public interface IWriteable
{
void Write();
}
public class HDD : IReadable, IWriteable
{
public string Read() { }
public void Write() { }
}
By creating one interface for Read()
and Write()
methods, it would obligate class to implement both methods in class. But some classes only want to read data, others want to write data, and some to do both. So in this case it is better to create separate interfaces.
So let's look another example with CardReader. CardReader just reads data, it does not write data. So, if we inherit one interface with Read()
and Write()
methods, then we would violate ISP principle. An example of violation of ISP:
public interface IWriteReadable
{
string Read();
void Write();
}
public class CardReader : IWriteReadable
{
// this is a necessary method
public string Read() { }
// here we are obligating to implement unnecessary method of interface
public void Write() { }
}
So by applying ISP, you only puts methods in interface that are necessary for the client class. If your class/client just wants to read data, then you need to use IReadable
interface, not IReadableWriteable
.