Today, let’s revise the I of the SOLID principles: Interface Segregation. The name sounds a little bit confusing, but it’s an easy principle to grasp and the article will be quite short. It states that “A client should not be forced to implement an interface that it doesn’t use.” Interface Segregation Principle definition However, what this definition exactly means? As you probably find with all these principles, it comes down to the knowledge that one object has of another object. Let’s illustrate this principle with some code that is hopefully easily understandable. Let’s imagine we have a Worker struct with two methods and a Captain struct that has a manage(worker: Worker) method:

struct Worker {

  func work() {
    "I'm working"
  }
  
  func sleep() {
    "Ok, a bit tired, let me have some rest 🥱"
  }

}


struct Captain {
  func manage(worker: Worker) {
    worker.work()
    worker.sleep()
  }
}

So at this point, everything looks good, right. We have our captain that can manage a worker. But what about the captain recruits a Robot to do the same job. Well, our program is going to change a little bit. You might think to do something like this, for instance:

protocol Worker {
  func work()
  func sleep()
}


struct Human: Worker {
  func work() {
    "I'm working"
  }
  
  func sleep() {
    "Ok, a bit tired, let me have some rest 🥱"
  }
}


struct Robot: Worker {
  func work() {
    "🤖 working"
  }
  
  func sleep() {
  }
}


struct Captain {
  func manage(worker: Worker) {
    worker.work()
    worker.sleep()
  }
}

We set up a Worker protocol, and because a worker should work and sleep, we put those methods in the protocol. Then we changed our Worker struct by a Human struct and implemented the Worker protocol. A human can work and sleep, so we have no problem putting logic on those two methods. Now we have a Robot struct implementing the same Worker protocol. Yes, a robot can definitely work, so it’s totally fine; it knows how to do it, but what about the sleep method? Robots don’t sleep so we cannot implement this method. And this is what the Interface Segregation is all about: clients should never implement interfaces that they don’t use and, right now, the Robot struct is being forced to implement the sleep method. You might say, ok, if it doesn’t use it, so simply delete the sleep method then. Easy said than done. Because we’re implementing the Worker protocol, we’ll have a compiler error saying Type 'Robot' does not conform to protocol 'Worker', so we have to implement it even though its functionality is not required. Interface Segregation Principle compilation error

Doing things right

So how to fix this. The first solution is to break down the Worker protocol into smaller chunks. Remember, a protocol with only a single method is perfectly fine; the thing we should be worried about, in fact is the opposite, a protocol containing too many methods because it is susceptible to break the Single Responsibility Principle (all these principles are linked somehow). Instead let’s have two protocols, a Workable and Sleepable protocols with the respective work and sleep methods.

protocol Workable {
  func work()
}

protocol Sleepable {
  func sleep()
}

Now let’s implement those protocols on our structs. For the Human struct, we’ll have something like this:

struct Human: Workable, Sleepable {
  func work() {
    "I'm working"
  }
  
  func sleep() {
    "Ok, a bit tired, let me have some rest 🥱"
  }
}

And for the Robot struct, since it cannot sleep, we’ll just implement the Workable protocol:

struct Robot: Workable {
  func work() {
    "🤖 working"
  }
}

So now that the Robot struct is no longer conforming to the Working protocol, it is not being forced to implement a method it doesn’t use; therefore we remove the sleep method, and we are now conforming to the Interface Segregation Principle. Let’s come back to our Captain struct now.

struct Captain {

  func manage(worker: Worker) {
    worker.work()
    worker.sleep()
  }

}

So before, we had a dependency on Worker. Now we say it wants something that conforms to Workable protocol.

struct Captain {
  
  func manage(worker: Workable) {
    worker.work()
  }
  
}

However, let’s make a quick call back to the Open-Close Principle and decide we want to sleep in the manage method as well. We might be tempted to do something like this:

struct Captain {
  func manage(worker: Workable) {
    if worker is Human {
      let human = worker as! Human
      human.work()
      human.sleep()
    } else {
      worker.work()
    }
  }
}

And if you remember, as we discussed in earlier articles, doing type checks like this should break the OCP. Instead, why do we not create another protocol called Manageable with a single method?

protocol Manageable {
  func beManaged() 
}

Now we go back to our Captain struct and say the manage method accepts a worker of type Manageable.

struct Captain {
  func manage(worker: Manageable) {
    worker.beManaged()
  } 
}

What left to be done is to implement the Manageable to the Human and Robot structs:

struct Human: Workable, Sleepable, Manageable {
  func work() {
    "I'm working"
  }
  
  func sleep() {
    "Ok, a bit tired, let me have some rest 🥱"
  }
  
  func beManaged() {
    self.work()
    self.sleep()
  }
}


struct Robot: Workable, Manageable {
  func work() {
    "🤖 working"
  }
  
  func beManaged() {
    self.work()
  }
}


struct Captain {
  func manage(worker: Manageable) {
    worker.beManaged()
  } 
}

Now because the manage method does not depend upon a worker object itself, we have improved the design and reduced coupling or, in another words, now our client, which is the manage method, can use any object that conforms to the Manageable protocol without needing to depend upon any particular implementation of worker.

Final Thought:

As I told you, this principle is pretty easy to grasp. The main thing to remember is to not implement a contract that won't be used on clients. For our final article about the SOLID principle, we'll see the Dependency Inversion Principle. Until then, have a nice week 👊