The interesting thing about the problems we encountered with Liskov is that they lead directly to the next principle. The basic problem we found was that there are a lot of implicit assumptions that can and often are made by the usage of an interface. We also discussed the use of contracts to make some of those assumption explicit, and the use of unit tests to help verify those assumptions. However, we ignored the simplest solution of the lot: make fewer assumptions. That’s what the Interface Segregation Principle is about.
Let’s go back to my naive understanding of object-orientation. By now, I’d learned what the interface keyword did, so when I created a ChessGame class, I knew that I needed an IChessGame interface. I was still wrong. Let’s think about it for a second: imagine I write a tournament class which plays a certain number of games and returns the winner. There’s nothing chess-specific here. By using IChessGame, I’m still requiring anyone using this functionality to implement chess. Which is a pity, because when you look at it, a draughts tournament works in exactly the same way.
Get to the principle already
Here’s the basic statement of the principle:
Make fine grained interfaces that are client specific.
Read that last bit again “client specific”. Let’s say that I look at my previous code and say
- Well, ChessGame inherits from Game
- Game implements IGame
- So I’ll just change my code to use IGame.
Well, I’ve satisfied dependency inversion there, but I’ve completely missed the point when it comes to interface segregation. Let’s talk a look at the IGame interface:
public interface IGame { IPlayer PlayGame(); // returns the winner IEnumerable<IMove> PreviousMoves { get; } }
The tournament doesn’t need to know about PreviousMoves. He actually wants a smaller interface: one that just plays the game and returns the winner. Call it ITournamentGame if you like. Does the Game class or the ChessGame class implement this interface? Doesn’t matter. What matters is that we’ve reduced the coupling of our code.