Some time ago I attended to a coding interview for the position of Data Scientist at one start-up. I felt myself well-prepared and confident, practicing lots of programming puzzles, coding various Machine Learning techniques from scratch, and having several years of programming experience under the belt. What can go wrong?
Unfortunately, I failed at the thing that doesn’t have any relation to the Gradient Descent methods or Time Complexity analysis. No, the failure was related to something very different and much more complicated. It was a Tic-Tac-Toe game!
I bet at this moment, some of you just close this story with thoughts: What, a Tic-Tac-Toe game? How can one fail to implement such a simple thing? You should be a total newbie, man, not being able to solve such a trivial exercise!
The rest of you, who decided to give me a chance, I invite you to read my story and probably this experience help you to escape similar mistakes in your software development practice and be more careful.
Now let me explain the interview task more thoroughly to give you enough context and understanding of the project’s scope. The following list enumerates the minimal requirements.
Sounds very straightforward, right? The greatest twist here is a time limit. You’re only given a 1 hour to build the MVP, i.e., working terminal-based game for two players. Also, there is an additional list of priorities that clarify and extend the design requirements. Note that they list in the order of decreasing importance.
Ok, great, the requirements are clear. Everything looks pretty simple and its time to start building the code! It shouldn’t be too tricky…
Photo by Ant Rozetsky on Unsplash
As soon as I’d read the document with requirements, my thoughts were: Hey, the basic gameplay of a Tic-Tac-Toe game is very trivial so let’s focus on the UX and handling the input at the beginning, and then build the rest of the game logic. I wanted to build something fancy and interactive. Up to this moment, I’ve developed many CLI tools, and simple terminal experience wasn’t enough. So I chose the curses library to make things interactive and convenient. (I know, it could sound strange to talk about “convenience” of a terminal-based interface, but still…)
There is one small question here: How would you debug a program that controls the terminal? Here is a small snippet to explain what I am talking about. The code below shows the simplest curses program to interactively with a breakpoint.
<a href="https://medium.com/media/c89a5ae7ab304b81e4cbfede35ef09b0/href">https://medium.com/media/c89a5ae7ab304b81e4cbfede35ef09b0/href</a>
Looks nice, right? Until you try to run it.
Having an interactive UI wasn’t a strict requirement, as you see from the list shown above. Still, this thing drained my attention and a considerable part of the allocated time.
When you run this snippet, it not shows you what you could expect. As you have probably already guessed, the curses take control over the terminal session, and when a debugger tries to render its interface, things are totally broken.
Not a something you would like to see when running a debugger
I’d spent a whole lot of time trying to set things up and deal with a broken debugger. Sure enough, the precious minutes wasted without bringing any value to the developed program. The most interesting thing here is that having an interactive UI wasn’t a strict requirement, as you see from the list shown above. Still, this thing drained my attention and a considerable part of the allocated time.
After I’d gave up my attempts to tame the terminal, I started building the game logic. The main class called Game (yes, a pretty unusual name for a class that manages the game) was responsible for storing the board state, switching the active player, and managing the whole game process.
As you already know, I was running out of time, so I’d tried to focus on the building working solution and put off the refactoring for the very end of the process. After some time, the Game was responsible not only for the gameplay but also rendered the board, including a few hard-coded constants and so on. Without notice, my Game class became responsible for everything.
Ad-hoc solutions and quick patches had led to the avalanche of bad code without time to fix it
Expectation vs Reality from the world of Computer Programming
So as you can guess, I didn’t have enough time to factor out this AllmightyGame class into something more manageable and light-weighted. It becomes a great illustration of God-class anti-pattern instead. Ad-hoc solutions and quick patches had led to the avalanche of bad code without time to fix it. It seems that distinguishing the clean code from the ugly one is a more simple thing than spotting how it slowly slides from one category into another.
An ugly game logic class, a fragile UI, and the time given to complete the task is almost over. Not bad for such a “simple” thing! How can we safe a bit of time to fix all this mess? Which part of the software development process seems to be the one we can easily drop without much consequences?
The answer is unit tests! The code is so compact that you can see every class and function; we can safely jump straight into application logic coding instead of writing these extra functions, right?
Wrong. Even with the simplistic and trivial programs, you need somehow verify that your input generates a valid output. You still need to write some entry point to run your program, and you are going to execute it very often during the development process. These entry points are precisely the thing tests give you.
The tests not only don’t add too much overhead to your code, but they also bring many benefits to the development process, even for pretty simple, toy programs
Moreover, writing tests has a significant effect on your API building process: it forces you to decouple modules and untangle pieces of your system. See the snippet below. You need to verify that game logic works, and the most straightforward way to do so is to pass the game state from outside. Then you only need to call your winner checking function and see if it works as expected. Otherwise, you would need to run the game UI, and type turns manually. Even for small game states, it would steal a few seconds of your time on each run, not talking about testing bigger boards.
<a href="https://medium.com/media/6a660c160bcede64a4715182d2e8d0cc/href">https://medium.com/media/6a660c160bcede64a4715182d2e8d0cc/href</a>
Therefore, the tests not only don’t add too much overhead to your code, but they also bring many benefits to the development process, even for pretty simple, toy programs. I believe there is an opposite tendency actually: the tests could even speed up the development process and help to make it better. One should think twice before shrinking testing to save time.
Sure enough, this little story was written not to tell you how to deal with terminal rendering troubles or write puzzle games. It is musing about Software Development in general.
One could find this “parable” quite shallow with lots of obvious mistakes I’d made. Well, good for you! You are a much better developer than I am. Nevertheless, I decided to share this failure with the rest of us who feel confident and experienced but sometimes forget about such “mundane things” like deadlines, KISS, and MVP. Tight time limits and perceived task’s simplicity could play a bad joke of you, and I am talking not only about test tasks but the daily development process as well. So let’s go through the lessons we can learn from this story.
Many of these things could seem obvious and many times repeated. (Not talking about plenty of great books). Nevertheless, I believe that every of us has a chance to fall into the same trap, and it is a good idea to remind yourself about these simple observations every once in a while to be in a good fit and make better decisions.
Have you ever had similar experience during your developer’s carrier? Or if you’re an employer, which kind of tasks you prefer to give to your candidates? What do you think about such type of “stress-testing”? Would be glad to hear your thoughts!