The Art of Writing Object-Oriented Code
Unlike theory, practice gets its hands dirty. It is practice that lays bricks, builds bridges, and writes code. Practice lives in the real world of change, confusion, and uncertainty. It faces competing choices and, grimacing, chooses the lesser evil; it dodges, it hedges, it robs Peter to pay Paul. It makes a living by doing the best it can with what it has.
- Sandi Metz, Practical Object-Oriented Design in Ruby, Chapter 1
Compare two different code bases (exhibit A and exhibit B). Both are tic-tac-toe games that I had written a few months ago. One of them has fewer lines of code. One has tests (29, in fact). One was sloppily done in less than two days; I’m afraid the other took quite some time. And one is much much more flexible, and by tweaking a few lines of code, you can transform the game from a 3x3 to 5x5 board between two AIs. One is objectively better than the other. Can you guess which is which?
You guessed it: exhibit B is far superior, having integrated object-oriented principles, a clear design pattern, and unit testing.
Exhibit B is not just a reiteration of exhibit A, but a complete gut renovation. I wrote exhibit A out of time constraint, thinking ‘Well, this gets the job done’ and having met my objective of building an unbeatable tic-tac-toe game. Here are some of its shortcomings:
It does not treat the game’s various parts as “objects” nor does it honor the interactions between them as “messages.” In exhibit A, I declared certain variables but failed to do much beyond that. In exhibit B, I identified and created the program’s core objects (board, computer player, human player, and game) and gave them their own set of methods and attributes. The `Game` is responsible for various messages between the players and the board.
It fails to manage dependencies. In exhibit A, not only are functions intricately tied to the DOM, but they are heavily dependent on each other and expose too much information. It is a messy entanglement of one function, or several functions, invoking others. On the other hand, exhibit B abstracts as many functions as possible — thereby ensuring that each function is held responsible for a single role and reducing redundant lines of code. Exhibit B even employs a `getCopy` (board.js, line 19) function to retrieve a copy of the board so that other objects (players, game) do not have direct access to the board and its winning moves.
It is not modular, flexible, or reusable in its design. The best application is one that thinks about the future and can endure changes in scope and detail. Exhibit A’s lack of design pattern renders it fitting for a certain kind of tic-tac-toe game. But it cannot be withstand any changes without a overhaul.
After all, each function in exhibit A serves a general purpose, but it is hardcoded with certain “magical” numbers that operate under certain assumptions (e.g. the tic-tac-toe board’s size being 3x3). In exhibit B, you will note a few things, like how the size is flexible and how the corners of the board are mathematically determined rather than set as certain coordinates (see computer_player.js, line 61). Also, you’ll see how reusable functions are employed, such as the `traverseBoard`(board.js, line 25) function which is a simple nested for loop that iterates through the board. The function is executed to return all the empty squares on the board and clear the board when the game is over— talk about versatility!
Furthermore, in exhibit A, enveloping everything into an `app.js` file also haphazardly brings together game logic, UI decisions, and DOM manipulation. Well-organized and modular coding allows for testing. Creating separate classes (and files) for the board, game, computer player, and human player allowed me to define very specific and unique functions of each and subsequently test each of these functions. Other benefits of separation of concerns is bringing clarity into the code base, for your future self and for others who may wish to review or contribute, and ensuring that functions do not lead to any unexpected side effects.
While there is much to fault with exhibit A, it was the perfect learning opportunity to get my hands dirty in object-oriented design. And this is the single-most useful takeaway in learning OO design principles: theory requires practice. It is not enough to superficially understand the construction of OO design. While it is not ideal to build two versions of the same game, as I have done, the process of building and experimenting and failing to get it right was worthwhile. In fact, I learned what not to do and how not to do it. This gave me the tools to think more critically about how to apply “these [OO] theories appropriately, at the right time, and in the right amounts.” And so, onward we go; let’s write better, cleaner, higher-quality code.