Application of SOLID Principles

The SOLID principles, as laid out by Robert Martin, are a set of five class design principles. This week I was tasked with finishing my implementation of Tic-Tac-Toe in Clojure with these design principles in mind. What follows is an exploration into some of the issues that I had in accomplishing this and the ways I used the solid principles to overcome them.

The Single Responsibility Principle states that a class should have one, and only one, reason to change. Identifying ways in which a class can change is the same as saying we are identifying the number of responsibilities that class has. Following this reason there should only ever be one responsibility for each class.

When I started this week I had one core file that I knew would have to be split up into a number of different files. This was so glaringly against the single responsibility principle that it was the first task I undertook in order to make my code SOLID. In the beginning I wasn’t entirely sure how I should split this file up but by analyzing the responsibility at both the function level and the namespace level I was able to split my core file into a small handful of files. By creating many namespaces I was able to keep the inner logic of each namespace separate from each other. The only class that needed to talk to these other classes, my core class, still only had one responsibility. It may utilize these other namespaces but it’s responsibility is still only to use them to play a game of tic-tac-toe, it’s sole purpose. A wise craftswoman named Sylwia once told me that if you describe what your class does and you have to use the word “and” then it is probably a good indicator that it has more than one responsibility. This is a great way to find places to separate out concerns.

The Open Closed Principle states that “You should be able to extend a class’ behavior without modifying it. In my tic-tac-toe game I have a namespace that takes care of the game-state and coordinates the other namespaces to accomplish the task of playing tic-tac-toe. Once all of the game-state is set up I have a simple call to my play-game function that kicks off playing my game. Because I had the OCP principle fresh in my mind I was able to abstract who is playing the game. For example if I want to play with one computer and one human I should be able to call the same play-game that I would use if I wanted to play with either two humans or two AI. There is more than one way to accomplish this in Clojure. The three that I considered are laid out in the great article, Polymorphism in Clojure. I ended up with the functions as argument approach because it seemed the most Clojure-like. Using this approach my play-game took two functions as arguments called player1 and player2. Inside the play-game function I assumed I had a function that would make a move but didn’t care about how that function accomplished it. By using this modular design I now have two types of players a computer and a human. I could imagine writing different intermediate or alien players or whatever kind of player I wanted to to extend the functionality of my play-game function. I was able to achieve this by applying the Open Closed Principle.

The Dependency Inversion Principle states that we should depend on abstractions, not on concretions. When I first designed my system I had two defs at the top of my file identifying the two pieces that I imagined my tic tac toe game to use: X and O. I used these as if they were global variables and thought nothing of it because I got minimax to work. But when I went to refactor my game and add other components I realized something was wrong with this. This is the perfect example of depending on concretions and the troubles that arise. By depending on this concretion when I wanted to move core functionality out of my first file I couldn’t because it depended on these two defs. I would have to find another way.

My solution was to have these two variables only be available in one of my namespaces, the namespace that actually needed them. By weeding out the times I use these two defs in other files I created a better abstraction. Using the abstraction made my code less entangled and gave me greater flexibility in my future changes. I achieved this by using the Dependency Inversion Principle.

Reading through the SOLID principles made me aware of places that I could make my code more extensible and easier to deal with. Having them in the back of my head creates a sort of guardian coder that steers me in the right direction and away from evil.