The S.O.L.I.D principles are the five fundamental principles of object-oriented programming and design. In this post, I want to discuss two of these principles and the scenarios I encountered during the development of my tic-tac-toe game project. The two are the Liskov Substitution Principle (LSP) and the Dependency Inversion Principle (DIP).
LSP says that “functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it”. In other words, a derived class can substitute a base class without any modifications. LSP helps with hiding information and providing flexibility to introduce new codes and objects without creating an impact to the existing code. This means it improves encapsulation and cohesion in the designing process, forcing the model to be created with high awareness of the objects’ behavior and relationship to each other.
DIP recommends that high-level modules should not depend upon low-level modules. Instead, both modules should depend upon a layer of abstractions Additionally, it emphasizes that abstractions should not depend upon details. It should be the other way around in which details should depend upon abstractions. Following the Dependency Inversion Principle will help modules to be independent from each other so we can reuse the code freely and make changes without breaking the system.
First, I want to show an example of how I violated DIP and how I resolved the problem. In my tic-tac-toe implementation, I created a Player Class that was thought to support the functional behaviors of a human player. The Player Class had a “make move” function that always depended on human’s inputs from the local console. It greatly violated DIP because the Player Class is a concrete class and is not an interface. I realized this violation when I tried to create a Computer Player Class. The Player Class I had implemented was a concrete class that was not flexible enough to be derived from. In other words, it could not handle the new Computer Player Class. I was missing an abstract layer in between.
I applied DIP by adding an abstraction layer between the Player Class and the client modules that use it. Here, the client modules would be the Human Player and the Computer Player classes. I created the Player Interface. This is not the same as the concrete Player Class that I had. This Player Interface must be general and be able to have different derived classes. In my case, I can now have two derived classes: Human Player and Computer Player. This way, I can implement different behaviors for Human Player and Computer Player without making any modifications to the Player Interface or the modules that use the Player Class. Human Player and Computer Player are not derived from each other since their behaviors are inconsistent with each other. However, they can be derived from a more simple and general base class such as the Player Interface.
Another example I want to show is my Game Scanner Class and how it violated the LSP principle. When I first implemented the game, I had a simple scanner initialized using scanner.in. The behavior of this scanner was to scan the input from the console. I violated the LSP principle when I converted the game to a library to use it in a simple socket server, because now, the scanner needs to scan the input stream instead of the console. My Game Scanner Class broke because I cannot derive a scanner to scan the input stream. Scanning from the console and scanning from the input stream are two different behaviors.
I know that a more general and flexible Scanner Class is needed for the two different behaviors to be derived from. I solved this problem by creating a Game Scanner Interface and an Input Stream Game Scanner Class that derived from that Game Scanner Interface. By doing this, I can easily create more different scanners to associate with different behaviors without breaking the other classes that use the scanners.
In conclusion, LSP requires the derived class to be completely substitutable for its base class without knowing it. It sounds obvious, but it is easy to violate. Behaviors of the objects play an important role in how objects can be derived from the other. From the above scenario, it is clear that scanning an input stream cannot be completely substitutable for Game Scanner because Game Scanner can only scan from a system.in. Game Scanner and Input Stream Game Scanner’s behaviors are not consistent with each other. If we force it, we will need to modify the behavior of the Game Scanner. LSP helps us to be conscious of this, to focus on the behaviors of the objects and to carefully evaluate how the clients are using the objects. With a solid understand of the object’s behavior, we can design a consistent and flexible model with the correct abstraction so that the new code can be introduced without breaking the old implementation. Similarly, DIP helps to create abstract layers between all the objects and its client, forcing independence between modules so the codes are flexible and independent to change.