Most developers are aware of SOLID at this point, and "I" stands for "Interface Segregation Principle". And like a lot of the other principles in SOLID, it has been wildly misunderstood.
Recently, I've seen the following pattern:
interface InsuranceService {
fun addPolicy(policy: InsurancePolicy)
fun getPolicyDetails(policyNumber: String): InsurancePolicy
fun calculatePremium(policy: InsurancePolicy)
fun processClaim(policy: InsurancePolicy)
fun cancelPolicy(policyNumber: String)
}
@Service
class InsuranceServiceImpl(private val policyRepository: PolicyRepository) : InsuranceService {
override fun addPolicy(policy: InsurancePolicy) {
// Implementation
}
override fun getPolicyDetails(policyNumber: String): InsurancePolicy {
// Implementation
}
override fun calculatePremium(policy: InsurancePolicy) {
// Implementation
}
override fun processClaim(policy: InsurancePolicy) {
// Implementation
}
override fun cancelPolicy(policyNumber: String) {
// Implementation
}
}
The code above is some fictional Kotlin code, but the pattern is the same. Let's go through what is happening:
So, what's the problem here? After all, it is said that we should code against interfaces and not implementations. Doesn't this pretty much guarantee that we end up coding against the interface? Isn't that a good thing?
Ask yourself: What value does the interface above provide? It provides no benefits while being annoying to work with.
The Interface Segregation Principle says the following:
Clients should not be forced to depend upon interfaces that they do not use.
There are two things we need to unpack in that statement. Who are the clients? And what does it mean by "interfaces"?
If we apply the principle to the InsuranceService
, we see that it is clearly violated as all clients must know about all methods because they are present in the interface. If we are going to adhere to the Interface Segregation Principle, then we'd end up with interfaces looking something like this:
interface PolicyManager {
fun addPolicy(policy: InsurancePolicy)
fun getPolicyDetails(policyNumber: String): InsurancePolicy
fun cancelPolicy(policyNumber: String)
}
interface PremiumCalculator {
fun calculatePremium(policy: InsurancePolicy)
}
interface ClaimProcessor {
fun processClaim(policy: InsurancePolicy)
}
While not perfect, we have split the interface into more concrete (and often more helpful) interfaces. This is how we should think about interfaces - small and concrete. But context matters, and in the context of the scenario in the first code example, this doesn't provide much value. An interface needs to have a purpose. It needs to have a reason to exist. We shouldn't write interfaces for the sake of having them - they should provide some value beyond just existing.
NOTE
Let's assume we're working on some closed-source application where the interfaces will never have a client we don't directly control. IF we were to have external clients, the interfaces above might yield some benefits just by existing.
Let's look at some good reasons why we would want to use an interface:
Decoupling can be great - in fact, it often is great! Does that mean we should strive to decouple everything everywhere? No, I don't think so.
Developers have a habit of going to extremes. If we look at the example in this post, the team has decided to interface everything - which is extreme. So, they have decoupled usage from implementation. Now what?
There are really two reasons you want to decouple:
If you aim to reverse a dependency, then again, you have to ask why. The reason is that you want to get some architecture. For example, if we look at Hexagonal architecture, we see that the database "layer" exists outside the application code, yet the application code must somehow get data from the database. How can we do that? We put the interfaces for getting, deleting, persisting, querying, etc, inside the application layer! Then, we have the database layer implement those. That way, the application layer doesn't need to know about the database layer, not even as a build-system dependency. The application layer doesn't need to know about any implementors of these interfaces.
The effect here is that the implementations of the interfaces are found in a different module from where the interfaces are defined. By doing so, we can control the direction of the dependency as we like.
There might be other reasons we want to reverse a dependency (not just module dependency). It could achieve some pattern or flow. However, it is always because we want to achieve something. We reverse the dependency because it helps us. We do it because it helps us in some way.
Whenever I bring up the pointlessness of many of the interfaces, I see the response often is, "But what about testing?". Yeah, what about it?
If I were a betting man, I'd say this question comes from advice from the past when dependencies were complex to replace. These days, we have powerful mocking tools* to replace the dependency, regardless of whether it is an interface or an implementation. It is, for the most part, a non-issue.
*Whether you'd want to mock stuff is a post for another day
Another common argument is that we can't predict the future. We have yet to determine how the codebase will evolve, so it is better to use interfaces and not need them while hoping that they come into use at some point. This is a flawed argument on multiple levels.
For one, if we don't know how the application can evolve and we want to "future-proof" it, then we can't make good interfaces either. We don't know how they'll end up looking.
The second flaw is that it is so easy to create an interface when the need shows up. We have powerful refactoring tools that can both extract the interface and replace the usages of the concrete implementation.
The third issue is that we write code for the need that exists right now - not some theoretical future. Sometimes, we need to stop and consider the trajectory of our system so that we don't design ourselves into a corner, but for the most part, we focus on building the right thing for what is needed right now.
In other words, I don't buy this argument whatsoever.
There are plenty of articles titled "Code against interfaces, not implementations", and it has become a slogan for some developers. Most of the articles communicate that the title doesn't automatically mean "let's interface everything", which is great. Unfortunately, if there exists a clear and actionable title, then some developers will just do what the title says. Developers sometimes struggle with nuances.
In the future, if you're going to make an interface, ask yourself why. What value does it give you? How does it enable you to do something? Don't create the interface if the answer is "nothing" or "don't know".
/Rant