SOLID Principles With Simple Examples — Part 2

In my first blog post on SOLID, I examined the Single Responsibility Principle and Open-Closed Principle and related them to examples from my own coding background. Here I will cover the remaining 3 principles.

3 — Liskov Substitution Principle

Subclasses should be substitutable for the classes from which they were derived — so you should always be able to replace MyClass with MySubClass.

In my first blog post, I explained that my Java TicTacToe application had an abstract Player superclass with two subclasses: HumanPlayer and ComputerPlayer. The TTT Game object does not care what type of player it actually is fed, it simply calls the getInput method on such Player to be returned an input to play the next move on its board.

According to the Liskov Substitution Principle, any of the Player subclasses should be substitutable for the abstract Player class itself everywhere that it is used without causing any bugs in the application. This is because each should be able to implement or use all of its parent class’s state and behaviour. No matter which Player subclass is actually passed to the Game at runtime, the Game should not break when it calls the getInput method because every subclass should be able to make sense of that method call. If you had a Player subclass that did not have or implement the getInput method then it is not really a type of Player and should not be a subclass of it.

At first glance, this idea seems obvious. However, a traditional example of how this principle can become complicated is when you try to fit a square within a rectangle. In Battleships, you traditionally have a square grid — however the game would still work if the grid was rectangular. So let’s imagine that in our first iteration of our Battleships game, we ask the user for a width and a length to set a rectangular board, in a method called setSize in a GridRectangle class.

In our next iteration, we are asked to add a new game mode where the user is able to select whether they would like a square grid or a rectangle grid. After selecting the square version, the user should be asked for only one integer to determine both width and length — if they were still able to provide two integers, they might very well provide different ones, which obviously does not make sense for a square grid. Imagining that the setSize method was required on instantiation of the grid, it would not be possible to make GridSquare a subclass of GridRectangle — because its setSize method would ask for only one integer not two. The Game object would try to pass the grid two integers and this would fail at runtime (although this error would probably never get to runtime in a real scenario).

In this situation, a square is not a type of rectangle — because you could not pass in a GridSquare subclass in place of its GridRectangle superclass perfectly. The Liskov Substitution Principle is therefore violated, which will cause errors at runtime and elsewhere.

To solve this, you could utilise a similar solution to the Player abstract class — by having an abstract superclass Grid with the GridSquare and GridRectangle both subclasses, which implement the setSize method in different ways. The OCP would also be satisfied were this solution adopted.

4 — Interface Segregation Principle

Clients should not be forced to depend on methods they don’t use — so you should make many fine-grained interfaces that are client specific.

Source code should be modular and every interface (and class) should contain only the minimum necessary logic to achieve the desired behaviour — so that entities (clients) which implement them do not depend on methods that they do not need. Again this will help to ensure that code is robust so that when changes are required, unexpected breaks do not occur elsewhere, for example due to using the interface in too many places. Smaller interfaces are also more flexible as they are more easily reusable without modification.

You are creating an adventure game with characters of different races — human, dwarf and elf — which are all implemented as classes. Each character can also select a battle archetype as a warrior, rogue or mage. Instead of creating one ‘fat’ BattleArchetype interface, where any rogue character which could perform the steal action would also have to ignore the strike and cast methods, you should have three separate interfaces.

5 — Dependency Inversion Principle

A: High level modules should not depend on low level modules. Both should depend upon abstractions.
B: Abstractions should not depend upon details. Details should depend upon abstractions.

In this context ‘modules’ are code pieces of functionality, such as classes and interfaces. This principle boils down to the idea that high-level ‘policy’ modules should not be directly coupled with lower level implementations and should instead be connected via abstractions. This has the consequence of making the high-level modules reusable and not prone to breaking as they are decoupled from low-level details. For example, an Encryption class should rely on a FileReader interface and a FileWriter interface, rather than specific implementing classes — those can be fed in at runtime from elsewhere (such as a FileReader class which specifically is able to read files of type ‘txt’ or a FileWriter class which is able to write files to the web). The Encryption class itself does not need to know about the specifics, which means it is not tied to details and therefore is more portable.

This idea can apply at various levels of your program. In my earlier example above, the Game object in my TicTacToe program does not need to know the specifics of the Player classes — it seems asks that it receives some kind of Player and is able to call its getInput method to receive an input. This means that Player subclasses can change or be added to without affecting the higher-level policy module of Game. In other words, the dependency has been inverted — Game does not rely on the Player subclasses implementations of the getInput method. In fact, they rely on Game to tell them what data type they should return by their getInput methods.

Another example is the isGridFull method in my Grid object. This returns false immediately if any square in the grid equals a string of a single space — because this signals that the square hasn’t yet been ‘marked’ and so the Grid cannot be full.

This does not satisfy the DIP because isGridFull relies directly on the detail of a String space. If I decide instead that an empty grid square should be represented by a smiley face I will have to modify this method as well as every other method which analyses empty grid squares through my program. The abstraction isGridFull depends directly on a detail.

To solve this issue, I have created a Java Enum class called Marks which includes a type called unmarkedSquare. The unmarkedSquare type, which is an abstraction, can then be referenced throughout the rest of the program including in the isGridFull method. If the string representation needs to be changed, I only need to change this in one place — in the implementation of unmarkedSquare itself. isGridFull now depends upon an abstraction rather than a detail.

This concludes my two-part series on the SOLID principles. After researching and reflecting on these, I undertook a significant restructuring of my TicTacToe application. I was able to spot with more confidence where classes had more than one responsibility and where modules were too tightly coupled, where abstractions should be introduced and so on. However, do bear in mind that the principles are guidelines which help to improve your code’s structure — they should not be treated as unbreakable rules. It is not correct to take the principles to the extreme and have, for example, a different class for every method!

In the future I would like to revisit the Interface Segregation Principle as I have not yet included an interface in any of my projects so was not able to relate this directly to work I have done.

--

--