I'm proud to say I published my first Ruby Gem the other day, which I created from scratch! Reflecting on the project as a whole, I realized I learned a lot about the process of creating a working program (albeit a simple one), so I'd like to share. I think these ideas could be especially helpful other beginners like me.
The gem, by the way, is a CLI app that lets you find recipes by scraping data from an outside source (link to source code, etc. at end of post). Not revolutionary, but it was still fun :)
Here are five points that I learned which helped me think through the coding process:
1. Write out a plan before writing any code
Because I was used to writing code along with a tutorial or programs limited to a few lines of code, I had really taken the value of writing out a plan for granted.
In my head, I knew what I wanted to build, so why would I need to waste time writing it down? It turns out that my written plan was one of my most valuable resources. It helped me figure out exactly where to get started, what kind of classes and methods I might need, and how I needed to let the users interact with my program. Without a plan, I’m pretty sure I would have been staring at an empty screen scratching my head.
I don’t think the plan needs to be super detailed, but just enough so you imagine the entire flow of the program (at least this is what I did for a small program like mine). I wrote my plan by imagining how a user would be interacting with the program and exactly what the program would do based on the user’s actions (e.g. Choosing “search for a recipe” would prompt the user to input a keyword, which would then put out a list of recipes that they could choose from). I didn’t include anything technical like “data would be fetched by the scraper,” rather I let the plan guide me as I wrote the code.
Of course, if there are specific specs that need to be fulfilled, I think think they could also be included in a coding plan or perhaps in a separate spec sheet.
2. Writing in pseudocode first can help
I had read a lot about pseudocode in Code Complete. The author is a big advocate of using pseudocode and it definitely helped me think out the more complex parts of my code. The basic idea is to write out exactly what you want a certain section of code to do in plain English before writing any actual code. I guess it’s like a mini-plan that you write out inside each method, class, etc. That will then guide you through any complicated logic and the technicalities of the exact lines of code you need to write.
I’d definitely say it’s much easier to think about the code in plain English opposed to jumping straight into the technical details and logic of, say, a complicated loop. Each line of pseudocode can be translated into the respective working code and furthermore, pseudocode generally makes excellent comments, which make your code easier to understand and manage in the future.
Another thing I want to try in the future is drawing out diagrams when I need to visualize how classes, methods, and data are interconnected with each other. I found myself getting frustrated trying to figure out why the data I was trying to pass around in my program wasn’t getting handled properly. Instead blindly trying to fix this-and-that, I think it would have helped to actually draw it out so I could see with my eyes what was actually going on.
3. Work in small steps and use “fake” data as needed
This really helped me move through my gem project without getting stalled or caught up in code I didn’t need until later. Obviously you can’t write out your entire program in one step, but you still need something to work with, right? If you don’t know where to start or get stuck, try breaking up things up into smaller pieces.
For example, work on one method at a time and get it to work and output exactly what you need before trying to weave it into the rest of the code. Or, for a complicated nested loop, start from the inside and work yourself out, getting each portion to work before moving one layer out.
Fake data can used if you need/want to get a certain method or class working, but the other part of your program that handles, creates, or passes in the data isn’t finished yet.
For my CLI gem, I decided to get the command line interface logic working before coding my class (a Recipe class and a Scraper class) so I could at least run the program in the command line. The CLI needed to deal with user input and output data accordingly, such as lists of recipes and the recipe details. To make sure the CLI logic was working properly first, I just faked the recipe data by writing it out by hand and used that while testing the CLI.
Afterwards when I was working on getting the Recipe class set up, I did the same thing: I didn’t have real data because the scraper wasn’t finished, but I wanted to make sure the Recipe class could actually process the data first, so I wrote some recipe data by hand and used it build out the Recipe class’s methods. Once that was working, all I needed to do was get real data with the Scraper class and then the rest of the program would just work.
4. Make sure each class and method only has one job or purpose
Separating each “job” into different methods and classes is one of the fundamentals of OOP and programming in general, but I also find that it makes it easier to code, organize your program, and goes hand-in-hand with working in small steps. Because each method supposedly only handles one job, it keeps the code inside each method less complicated and shorter, packaging it up into a neat little box. These “boxes” can then be easily used within other methods or classes without needing to repeat code (stay DRY!).
I also love to use this technique for abstracting out code when the code inside of a single method is getting too complicated or ugly. For example, moving a complex calculation or data processing loop into its own method. Or placing often-used operations into a well-named method that would make your code easier to read—These methods don’t really need to involve complicated code, but I feel it's often worth separating out certain parts to improve the readability of the code.
5. Write about the coding process in your blog or notes
Last but not least, I found that writing about my coding process has greatly helped me with learning retention and deeper understanding. Writing about what you’re doing requires you to really think about why you did what you did and look up things you don’t completely understand in order to explain them. I also love the idea that my blog posts may one day help someone else going through the same experience as me. Another huge advantage is that your blogs (or notes) become your very own reference manual for later on down the road!
Bonus: Things would have probably been easier with by writing tests first (TDD!)
I've worked with applications that use test-driven development, usually Rspec, and I think writing tests first would also go hand-in-hand with planning out a program and working in smalls steps/fake data. I actually don't know how to write tests quite yet, but it's on my list of things to learn.
About the Gem
You can check out the Git repository or see it on Rubygems.org.
More Info
For in-depth reading on how I worked through creating a CLI gem, you can see my "Creating My First CLI Gem" series on my blog:
Top comments (6)
Congrats on your gem!
#1 I 1000% agree! I use different words, I usually call what you did a "goal" or an "interface", but I'm known to have emotional issues with the word "plan". This sentence is the one one where I knew we were on the same page, this is definitely how I start most things:
#2 This can be super useful, especially as you work in a language you're not as familiar with. When I hit a hard JS problem that had me stuck for several days, I was exasperated and thinking "is this problem really so hard?!?" and then wrote it in Ruby, a language I know well, and it only took an hour! Then I translated my solution into JavaScrpit, and it worked. Pseudocode is a great way to remove the distracting details and let you think through the problem. I will say that the better you get at a language, the easier it becomes to think in that language.
#3 Okay, I love this one too. I initially liked this one more than #1, but after reading #1 more thoroughly, I'll place this one second. The less confident you are in a language/domain, the more important this is (both small steps and fake data). The harder your problem, the more important the "fake data" idea is. I wrote an entire gem to make it easy to explore small ideas from problems I'm working on, in small isolated contexts with simple fake data. My gists are full of conclusions I've reached from this step. IMO, this is the one step that most devs don't do enough, even seniors.
#4 There's a thresh hold this one, it's definitely a way to address a problematic end of a pendulum, but if you follow it too far, it can have its own issues. The pain from the other end of this pendulum might be felt when you apply it to a simple script and find it just ballooning.
#5 Aye, reflection is so valuable!
Bonus Lol, I have too many opinions on this one >.< Given you've got the aspiration to do it, and your likelihood to reflect (ie b/c of #5), let me suggest that when you decide to try it out, you try it in two flavours. The first flavour is to only test at the highest level of abstraction, ie whatever you wrote down in #1. People might call this "integration testing" or "acceptance testing" or "behaviour driven development". It will show you one end of the pendulum of testing, the constraint to meet is that you only depend on the highest level interface. And the second flavour is to only do what people call "unit testing" or "test driven development", and the idea there is that you follow your #4, and then its easy to verify that thing works. Some people would call that second flavour "test driven design", and what they mean by that is that sometimes you'll feel a kind of pain when writing that test, where it's deviating from your #4, and so interpret this pain as an indication that you need to modify the design. I don't think either flavour is better, but I think constraining yourself to just one or the other will help identify where they work well and where they work poorly, which will help you know how to adjust your real-world testing, based on situation you're in (and I could talk about this for an absurdly long time, so I'll trust your #5 to reveal the insights there).
Anyway, great article, congrats on the gem!
Oh, looked ~10s at the git repo, here's something I do: instead of committing the
.gem
file, use git to tag the commit you built it from. Then you can can go back and rebuild it from there, if you want to (or DL it from rubygems withgem fetch find_recipe -v 0.2.0
) It's usually (not always) best to avoid committing generated files, esp if they're binary, b/c: they can bloat the repo, git isn't smart about diffs in binary data, and they're generated from another more canonical representation, which is in the repo. In this case, they're also going to accrue in that directory and then they'll start feeling spammy.Hey Josh,
Thank you so much for your awesome feedback and comments!
1 I was skeptical at first, but planning really helped. I usually get stuck wondering what to do next but the plan solved. I'm sure you can do it in many ways like you said, though, for example with "goals" or interfaces. I think the "user story" method would be excellent too! We used to use user stories at my previous job for every single feature addition, no matter how big or small, and it really forced you to think about things from the user perspective, plus of course keep you on track with your code.
2 I loved the image of you writing your pseudocode for JavaScript in Ruby code, lol. I agree that it becomes less necessary as you get better and better at coding though. If I remember correctly, in the Code Complete book, the author actually advocates for ALWAYS writing pseudocode first before any code because it helps majorly reduce errors. I'm sure that's true, but sometimes it does seem like overkill.
3 I'm trying to be more careful with taking things in small steps (especially as a beginner!). I've been guilty of jumping into things, bashing out a bunch of code, just to learn that I broke everything and not knowing at what point I went wrong. Your gem looks super helpful! I'm going to check that out!
4 True!
Bonus: Yeah... testing I'm sure has it's time and place. But it would have been helpful to have a few simple tests that I could test out my code on instead of having to run the program every single time! heh. I haven't gotten too much into it yet, but I'm sure I'll form more in-depth opinions on testing later ;)
Thanks for checking out my repo too! Actually, I tried to find documentation on what exactly to do with those .gem files once you pushed them, but no luck. Are you supposed to keep them in your own file base? If so, should you delete each .gem version as you make new ones? I removed them from my git repo for now at least.
My approach is to build them into my project root and add
*.gem
to my gitignore (eg) so that git won't see it. Sometimes, when I get annoyed by them, I justrm *.gem
to clear them out.I've also seen people create a directory they build it into, eg
gems
orpkg
, and add that directory to the gitignore, but I don't see much value in keeping the old.gem
files around.I looked at it a little bit more, and there's a few issues you're going to wind up hitting. In the gemspec, it defines the bin dir as
exe
, but thefind-recipe
executable is inbin
. As a consequence, users who install the gem won't be able to runfind-recipe
(RubyGems won't know to add it to the$PATH
).The
require
statement in the binary is'./lib/find_recipe'
, the leading dot means "the directory I am currently in", which will work when you're running it from your gem's root directory (b/c from there, lib/find_recipe is the file you want), but it will break when you run it from a different directory (eg people who install the gem are unlikely to be in the gem's source code when they run it). The easy immediate solution here is to userequire_relative
and go up a dir, but the most correct solution is to add your lib dir to an array stored in the global variable$LOAD_PATH
. This variable stores the set of places Ruby will require code from... (Rubygems will add paths to this array, as you require gems). Try runningruby -r pp -e 'pp $LOAD_PATH'
, to see what that array looks like. After you add your lib dir, you can require everything relative to lib. In this caserequire 'find_recipe'
. Here is where I do that in my gem.You might find value in a walkthrough I made while teaching a class. It hits on all the most important things while building a gem, including things like the
$PATH
and$LOAD_PATH
, executability, permissions, and so forth. It should touch on all the really nuanced things like that, which took me forever to learn! Seriously, the$LOAD_PATH
is simple, but it's implicit, so I was confused by it for years! Right now, it will also be difficult for you to test the gem, b/c it callsgets
andputs
, which talk directly to the global input and output streams. The third day's material shows how to deal with that for the gem we build, which is similar to yours in many ways :)Actually, looking @ my walkthrough, the stuff that's relevant to you probably begins halfway through the second day, here. If you want to code along with the walkthrough, you can start on the first day, so that you'll have the code, but I don't know that there's anything super new in it (maybe the stuff about how to get the absolute file path). If you don't want to code along with it, the code is here, and the commits follow along with the walkthrough, so you can go back and see what it looked like at that time.
Thanks for explaining the .gem file stuff.
And you hit right on the other thing I was confused about: how to get the 'find-recipe' command to run no matter what directory you're in! I read through your walkthroughs and the $PATH is starting to make a little more sense.
The original $PATH code and gemspec were generated by bundler's
gem
command, so I had just used it as is. I used your examples and it seems to work now :) thanks again!Thank you! Can't wait to make more ;)