Understanding SOLID: Interface Segregation Principle

Understanding SOLID: Interface Segregation Principle

Meet Raizo, (Kon'nichiwa 🙏🏼), Raizo is a ninja, Ozunu taught him everything about the clan’s dirty work, on one of his missions, he was caught by Ryan 👮🏻, and because he knows too much, Ryan is able to find out all about the clan👊🏼,

The clan learned a lesson the hard way, never let a single ninja knows everything, just tell each one his Role in the mission.

As developers we have a similar problem, let’s code🧑🏻‍💻

💡 I’m using client, consumer, and dependence interchangeably to describe the class that depends on another class.

What’s a Fat class or Header interface?

when a class or an Interface tries to serve multiple use cases for a client at once.

let’s see a Header Interface:

interface AppRepository {
    fun getUserProfile(userId: Long): UserProfile
    fun updateUserEmail(newEmail: String)
    fun getBlogs(topic: Topic): List<Blog>
    fun publishBlog(blog: Blog)
    fun subscribeTo(topic: Topic)
    fun unSubscribe(from: Topic)
    fun deleteAccount(feedBack: FeedBack)
}

So these functionalities are needed in the app, we need to get the user’s profile, publish blogs, get others’ blogs, and can subscribe/unsubscribe from a blogging topic, so what’s wrong?!

let’s see..

ninja_assassin29.jpg

class UserProfileRepository: AppRepository {
    // profile related use cases
    override fun getUserProfile(userId: Long): UserProfile {...}
    override fun updateUserEmail(newEmail: String) {...}
    override fun deleteAccount(feedBack: FeedBack) {...}

    // why do we know about these functions!!
    override fun getBlogs(topic: Topic): List<Blog> {...}
    override fun publishBlog(blog: Blog) {...}
    override fun subscribeTo(topic: Topic) {...}
    override fun unSubscribe(from: Topic) {...}
}

why does UserProfileRepository know about publishBlog, deleteAccount functionalities?, it's not his Role, and in some situations, it’s critical to have accessibility to multiple use cases, we force UserProfileRepository to depend on methods it does not use.

you can imagine the number of use cases that can be added to this header interface as the app scales, it’s going to be a mess!

To the rescue 🌟…

Interface Segregation Principle ISP:

Segregation: setting something apart, isolating, or dividing something into groups.

by splitting an interface into more client-specific use cases, now each client can refer to the interface that serves its use case and not knows about other use cases.

Role Interface:

A consumer-driven contract, which means you can divide (segregate) your logic into multiple interfaces, each one has a role and each consumer can depend on the role(s) it needs, and avoid unneeded methods.

Advantages of ISP:

  • Readability: smaller interfaces/classes are easier to understand their role.
  • maintainability: avoiding fat classes, smaller classes, easier maintenance.

Refactoring 🚧:

in a real-world application, interfaces happened to declare lots of methods, you should include only highly related methods together in one interface.

// profile role
interface UserProfileRepositoryInterface {
    fun getUserProfile(userId: Long): UserProfile
    fun updateUserEmail(newEmail: String)
    fun deleteAccount(feedBack: FeedBack)
}
// blogging role
interface UserBloggingRepositoryInterface {
    fun getBlogs(topic: Topic): List<Blog>
    fun publishBlog(blog: Blog)
}
// subscription role
interface UserSubscriptionRepository {
    fun subscribeTo(topic: Topic)
    fun unSubscribe(from: Topic)
}

Now, UserProfileRepository just needs to implement UserProfileRepositoryInterface and only know about the profile role.

class UserProfileRepository: UserProfileRepositoryInterface {
    override fun getUserProfile(userId: Long): UserProfile {...}
    override fun updateUserEmail(newEmail: String) {...}
    override fun deleteAccount(feedBack: FeedBack) {...}
}

the consumer of UserProfileRepository is scoped to only profile use cases.

class UserProfileViewModel (private val userProfileRepository: UserProfileRepositoryInterface): ViewModel() {
    val userProfileState = MutableLiveData<UserProfile>()
    fun getUserProfile(userId: Long) {
        val userProfile = userProfileRepository.getUserProfile(userId)
        userProfileState.value = userProfile

        //Unresolved reference: getBlogs
        userProfileRepository.getBlogs()
    }
}

Implicit Interface

A class's public methods act as an implicit interface.

Segregation can be done by scoping the class’s methods, so when other classes depend on your package, for example, you can provide a client-specific use case by public APIs and the rest will remain protected or private for internal logic.

package com.mylib.internal

class ServiceLocator {
    //public api
    fun getService(serviceName: String): Service
    fun isExist(service: Service): Boolean
    // scoped api
    protected fun deleteService(service: Service)
    protected fun flush()
}

Then Utility is limited to the ServiceLocator’s implicit interface.

package com.xyz.base

class Utility (private val serviceLocator: ServiceLocator){
    val resourceResolver = serviceLocator.getService(ResourceResolver::class.java.name)
    fun clearResources() {
        //error: Cannot access 'deleteService': its protected in 'ServiceLocator'
        serviceLocator.deleteService(resourceResolver)
    }
}

What’s next?

  • Try to read open source projects, and address violations of this principle.
  • Read about this principle and discuss it with your teammate to see if you can enhance your projects, at the same time you can practice this principle.

Conclusion:

  • ISP is about isolating highly related functions into an interface.
  • Adhering to ISP makes it easy to read, maintain and refactor your code.
  • Implicit interface is the public APIs provided by a class to the outer world.
  • It seems to be an extra effort to write more code, but it serves a long-term goal.

Peace ✌🏼