🦉
fps.vogel

My first Ruby app

Lessons learned
2021-05-19

I’ve just released my first Ruby app: Readstat, a CLI app that gives statistics on a plain text (CSV) reading log. This is also my first completed app in any language, so I learned a lot along the way. Here are the highlights.

Lesson 1: Write tests

Back when I started writing the app, I knew about TDD (Test-Driven Development) but I thought setting up tests would be a waste of time for a small project like mine. I couldn’t have been more wrong.

Everything was fine while I was rapidly building the app’s bare-bones functionality. But then I started filling it in, and with each new feature it became more and more difficult to find out whether I had correctly implemented it. Every time, I had to run the app, enter commands, make some calculations to verify the output… not the most efficient process.

And when something didn’t work, I had to spend a good while—hours sometimes—tracking down the bug. (Later I discovered pry and pry-byebug, which would have sped up that debugging, but they don’t solve the problem of mysterious bugs being introduced in the first place.)

Another problem came up every time I took a break from the project, even if only for a few days. On my return I had to spend some time re-orienting myself because all I had to go on was the code and scattered comments, and it was not always clear what a certain chunk of code did, or was supposed to do (which too often were not the same thing). And as the codebase grew, it became harder to keep all those “whats” and “shoulds” in my head. Often I sat staring at an old piece of code, wondering if I could delete it. But deleting it meant I would have to fire up the app again, try out several commands (because I wasn’t sure what this old code affected), make calculations to verify each result… or I could skip all that and just delete the old code and pray that it wasn’t important. Usually I decided on no decision at all and left the code as it was, just to be safe.

And so my app accumulated—I don’t say grew or developed, because it was less like the organic growth of a tree or the planned development of a building, and more like the haphazard accumulation of sediment, each new layer obscuring the one before it, slowly but surely forming fossils out of half-forgotten and poorly-understood code. Except unlike real-life fossils, these were not interesting or valuable, and they weren’t always harmless either.

Eventually I scrapped it all (it was that frustrating) and started over with a better design. I had just finished reading Sandi Metz’s Practical Object-Oriented Design (POODR), and I was ready to unleash my inner OOP and make a lean, mean codebase. There was a chapter on testing in there, but I waved it aside because, well—small project, no need for tests, good design is the main thing.

… A few weeks later, I was in the same abyss of head-scratching code and obscure bugs all over again. There was no escape: I resigned myself to writing tests. By then I had read 99 Bottles of OOP, the other instant classic by Sandi Metz (this one co-authored with Katrina Owen, creator of the amazing Exercism.io. Unlike POODR, this one is about testing from the get-go. So I knew what to do; I had no more excuses.

Looking back, I am baffled at my stubborn reluctance to write tests. By now I have seen the many benefits of having a codebase covered by tests:

In short, I learned to love tests.

Lesson 2: Remove change from the core

I noticed a recurring theme in both OOP and functional programming styles: the further down you go into your code, the less it should use things that might change. If that sounds vague, it’s because this principle manifests itself in a variety of ways. For example:

Lesson 3: Write clear code

In that same post I enthused about my first gem, a DSL for piping data through functions, like this: 'C:\test.txt' >> LoadFromFile >> ProcessData >> OutputResult(STDOUT). I don’t regret working on it, because it taught me new metaprogramming techniques and how to publish a gem. But I soon stopped using it in my own app because I realized that the nifty syntax obscured what is actually going on in the code. For example:


'C:\read_test.csv' >>
  LoadLibrary >>
  EachInput do |input, library|
    +[input, library] >>
      Command >>
      ShowOutput
  end

With this syntax, it’s often hard to tell what arguments (or even how many arguments) one function is passing to another, because the pipe DSL obscures the data. Here is the same code using more natural syntax:


library = Library.load('C:\read_test.csv')
Input.each_line do |input|
  Command.parse(input)
         .result(library)
         .output
end

I’m still not completely satisfied: the chunk after Command seems opaque. (I don’t think it violates the Law of Demeter because there is a Result class, so it can be inferred that Command parses input into a Command object, then we get its Result, which we then output. But if one must so strenuously defend their code’s clarity, maybe that means it is not as clear as it could be.) Still, it is more clear than before because at least now I can see the methods and their arguments.

This is just one of many instances where I noticed that my code, though clear to me now, might be confusing to Future Me. So I have gotten into the habit of critically combing through my code to see where it could be made simpler, whether it is muddled or simply too clever. And I do this not just immediately after I have written it, but also after some time has passed and I can see the code with fresh eyes.

Areas of improvement

Here are my weak spots where I will try to improve in my next project:

Of course, these are in addition to the blind spots that I will only notice in my next project. Onward!