In this post, I continue with the fourth of the S.O.L.I.D. Object Oriented Design Principles, the Interface Segregation principle (ISP). As we could notice, all the principles overlap in many aspects and this one is not the exception. In the Single Responsibility I mentioned that it is very often that a programmer comes across classes whose public interfaces do "too much." This common problem is a major concern of the ISP as well; we can say that this principle targets Interface Pollution.
We detect the Interface Pollution smell when we see client classes that use just a little of the functionality exposed by other ones. This is a good time to do some refactoring and improve our design. Refactoring is a practice that every OOP programmer should do.
When object oriented applications are maintained, the interfaces to existing classes and components often change. When changes have a big impact like recompilation and redeployment, this impact can be mitigated by adding new interfaces to present objects, rather than modifying the current interface. This means, we can extend our published interface by defining a new one that contains these modifications without changing the published one; therefore, no client objects are affected as they would never notice this update.
The Interface Segregation principle defines the following:
- Clients should not be forced to depend upon Interfaces that they do not use.
- Many client specific Interfaces are better than one general purpose Interface (god class).
What does Client Specific Mean?
Clients should be categorized by their type, and interfaces for each type of client should be created. If two or more different client types need the same method, the method should be added to both of their interfaces. Also, to avoid method repetition, we can abstract the common behavior from these interfaces and define a base interface from which they can inherit (GoF Template Method pattern).
The following UML class diagrams picture the ISP:
The classes A, B and C are clients of the Service class who defines methods to be used by them. Each one of the client classes uses small and different portions of these methods.
Now, we create client specific interfaces, each interface is more granular and provides more cohesive definition for each client.
Code Sample:
Despite the following code is in C# it can be easily ported to Java or other OOP language.
Wrong implementation of this principle:
We define the class Animal, this class provides three methods with some animal capabilities; Swim, Fly and Run.
1: public class Animal
2: {
3: public virtual void Swim()
4: {
5: Console.WriteLine("I swim");
6: }
7:
8: public virtual void Fly()
9: {
10: Console.WriteLine("I fly");
11: }
12:
13: public virtual void Run()
14: {
15: Console.WriteLine("I run");
16: }
17: }
Now, we define three classes that inherit from Animal; Dog, Shark and Duck. Our abstraction is that these are animals, therefore it is safe to sub-class from the Animal class. As not all animals can support some of the capabilities, we implement an Exception when such methods are called to notify the programmer that such behaviors are not valid.
1: public class Dog : Animal
2: {
3: public override void Fly()
4: {
5: throw new InvalidOperationException("A dog cannot fly");
6: }
7: }
8:
9: public class Shark : Animal
10: {
11: public override void Fly()
12: {
13: throw new InvalidOperationException("A shark cannot fly");
14: }
15:
16: public override void Run()
17: {
18: throw new InvalidOperationException("A shark cannot run");
19: }
20: }
21:
22: public class Duck : Animal
23: {
24: }
For any client who uses a Dog object, it may be cumbersome to see that by calling the Fly method, an exception will be thrown regardless. The same client would perceive the Shark object even worse. For the client's point of view, the Dog and Shark classes public interface are "polluted."
A class ProgramClient is defined to make this problem clear to see.
1: class ProgramClient
2: {
3: static void Main(string[] args)
4: {
5: Animal animal = null;
6:
7: Console.WriteLine("A Duck says: ");
8: animal = new Duck();
9: animal.Fly();
10: animal.Run();
11: animal.Swim();
12:
13: Console.WriteLine(Environment.NewLine + "A Dog says: ");
14: animal = new Dog();
15: animal.Fly();// Will throw an Exception
16: animal.Run();
17: animal.Swim();
18:
19: Console.WriteLine(Environment.NewLine + "A Shark says: ");
20: animal = new Shark();
21: animal.Fly();// Will throw an Exception
22: animal.Run();// Will throw an Exception
23: animal.Swim();
24: Console.ReadLine();
25: }
26: }
We will need to remove the method calls that throw Exception as per their implementation. Alongside our programming experience, we may have ended up with sub-classes where we left many of their derived methods with either, an empty body or throwing flavors of "Not Implemented Exceptions." This is a sign that we need to segregate the class interfaces, specially if we are the owners or have access to the source code.
Good implementation of this principle:
We follow the name of this principle by segregating our previous interface Animal. Now, we define ISwim, IFly and IRun interfaces as follows:
1: public interface ISwim
2: {
3: void Swim();
4: }
5:
6: public interface IFly
7: {
8: void Fly();
9: }
10:
11: public interface IRun
12: {
13: void Run();
14: }
We define our new Dog class only with the behaviors that such animal can do.
1: public class Dog : IRun, ISwim
2: {
3: public void Run()
4: {
5: Console.WriteLine("I run very fast");
6: }
7:
8: public void Swim()
9: {
10: Console.WriteLine("I can barely swim");
11: }
12: }
We do the same with the Shark and Duck classes:
1: public class Shark : ISwim
2: {
3: public void Swim()
4: {
5: Console.WriteLine("I swim all day long");
6: }
7: }
8:
9: public class Duck : IRun, ISwim, IFly
10: {
11: public void Run()
12: {
13: Console.WriteLine("I can barely run");
14: }
15:
16: public void Swim()
17: {
18: Console.WriteLine("I swim with no problem");
19: }
20:
21: public void Fly()
22: {
23: Console.WriteLine("I fly short distances");
24: }
25: }
Now, our ProgramClient class uses these objects intuitively without any problem. We have improved our design by abstracting a better way to expose animals behaviors. A client of Dog objects will never be given the choice to call the Fly method.
1: class ProgramClient
2: {
3: static void Main(string[] args)
4: {
5: Console.WriteLine("A Duck says: ");
6: Duck duck = new Duck();
7: duck.Fly();
8: duck.Run();
9: duck.Swim();
10:
11: Console.WriteLine(Environment.NewLine + "A Dog says: ");
12: Dog dog = new Dog();
13: dog.Run();
14: dog.Swim();
15:
16: Console.WriteLine(Environment.NewLine + "A Shark says: ");
17: Shark shark = new Shark();
18: shark.Swim();
19: Console.ReadLine();
20: }
21: }
It is clear that we can provide better interfaces for our classes. We might have a new requirement for a SuperDog class, this would be simple to implement with the following definition.
1: public class SuperDog : Dog, IFly
2: {
3: public void Run()
4: {
5: Console.WriteLine("I can run super fast");
6: }
7:
8: public void Swim()
9: {
10: Console.WriteLine("I swim with no problem");
11: }
12:
13: public void Fly()
14: {
15: Console.WriteLine("I fly long distances");
16: }
17: }
We may consider to implement directly the IRun, ISwim and the IFly interfaces for this new SuperDog class instead of inheriting from Dog, otherwise our design is in risk to break the Liskov Substitution principle. This is because clients using Dog classes only, are not expecting "super" behaviors.
Also, for backward compatibility with our previous Animal class, we can make it implement our three new interfaces.
Conslusions:
We have seen how the ISP helps us to provide more granular and cohesive interfaces that our clients can understand and use. A good way of doing this is by dividing an interface into single responsibilities for each one of the interfaces to define; this is a clear sign that all the S.O.L.I.D. principles overlap. When we see a polluted interface doing "too much," we can apply the Interface Segregation principle to solve this problem.
I will cover the Dependency Inversion principle on my next post, the last of the S.O.L.I.D. Object Oriented Design Principles series. See you soon!
[RobertMartin96] SRP: The Interface Segregation Principle, Robert Martin, 1996