Last week I got to write my very first Ruby application with a command-line interface from scratch. No guided lesson with specific outputs or a framework already lined out. Just a simple prompt to have at a minimum three tables in our database with one many-to-many relationship. The what and the how was up to us (we ended up with 7 tables and three many-to-many relationships which helped make the below lessons much more clear in hindsight). My partner and I decided that we should build an employee management system that tracked the skills of each employee, what projects they were assigned to and what skills those projects required. From there we generated our user stories and mapped out our data structure.
Lesson 1, continuous delivery or MVP or however you want to think about your early product, but prioritize a functioning application over a full feature set. Our project was entirely contained in a 4-day sprint, but instead of remembering this, we built out all of the user stories to develop a useful application with lots of features and answers to questions we might ask. From there we immediately started working without really pausing to think about how to USE the application or really anything except methods and the data structure. Now in our defense, we were only 3 weeks into learning Ruby, and building methods that manipulate data, store and retrieve data is what we knew how to do. Regardless, we had about 20 different stories that we were working through, and it was not until the last one that we even bothered to think about the command-line interface. We built the backend and then tried to slap a front end on at the very last second, and while it worked, a lot of sleep was lost when crunch time came. We were about as far from continuous delivery as we could be, and it nearly resulted in having nothing to show. So to reiterate and rephrase, the lesson that we learned was the feature is not done until the user can actually use it (we even wrote it in our stories, “As a user, I want to…”!). Even if it means the functionality of your app sucks and it looks like crap, having something that the user can interact with is better than having a beautifully refactored backend with no user interface. UI and backend need to be developed hand in hand.
While that is very nice, it is not the topic of this post, using objects in object-oriented languages is, so why did I go through our work planning methodology? Turns out there was another impact of not figuring out anything about our UI until the end. In building our methods we ASSUMED that the user would interact through string inputs. When they wanted to select an employee they would type their name, “Milton” or search by skill would be “Filing TPS Reports”. So, naturally, every method we wrote took in arguments of strings, then searched for the matching object, we also had to incorporate error checking everywhere because “milton” is not “Milton” is not “millton”. What a nightmare, and our instance on passing strings to each method was resulting in some ugly code that was anything but DRY, but still not easy to refactor because of the custom messages and actions based on where you were. Finally, our testing process had us converting the values in our objects to strings, then using the method to convert it back to an object, silly and redundant (especially since we had not yet learned how to efficiently write our own automated tests, and given the short timeframe of this project, we did not have the time to teach ourselves).
As you can imagine, this process resulted in some painful code. Below is one of our methods that was designed to improve an employee's competency in a given skill by 1. The skill argument in this method was a string, that had to be checked against the database and the employee’s skills list. Note that some parts of this method were already refactored into new_skill_via_method. If your keeping count, there are three nested If statements, not too problematic, until you realize that there are 20–30 other methods just like this one (and in the real world, there would be hundreds or thousands).
Finally came the icing on the cake, when we finally implemented our UI at 8 pm the night before it was due (by the way TTY Prompt is fantastic for Ruby command line), we found out that the gem we were using could select from a list. Suddenly our string issue evaporated because we just used a list of objects and could pass the selected object to the method. From that moment on, we had a mad scramble to purge our string matching and error checking code from our methods and actually use the objects in our object-oriented language. The result was shorter, easier to read, easier to debug, and easier to refactor.
Proof, the below code does exactly the same thing as the method above, but since training_skill is an object, we don’t have to convert it and we don’t have to check if it is in the system. Much easier to read, debug, and much DRYer.
So, Lesson 2, use the objects! Ruby and other OO languages give you a fantastic way to pass information that already exists around. Can you match the string for a name to the name string stored in an object, absolutely, but think first, do I really have to do this or can we just grab the object upfront. If you do need to do the matching, write a method to do just that, then pass the resulting object to the rest of your program. Now this is software engineering and every rule is actually just a guideline, but I am certainly going to think twice about passing values to my method if there is an object I can use instead.
At the end of the day, we did get our application to work as intended and completed every user story. We even had a spare hour to gold plate it (who needs to refactor anyway…). Still, if we had paid a little more attention to what the helpful people on stack-overflow were saying instead of the code snippets, or our teacher’s lecture on OOP, or even the authors of several agile books in my collection, we could have gotten some proper sleep and taken lunch or two if we had not had to learn some lessons the hard way.
P.S. If you decide to clone and use my app, whatever you do, don’t fire Milton, let's just say it ends poorly.