Last month, I had the experience of building out an app on an extremely tight time schedule. The rapid-fire development, while exciting, left me with technical debt to pay. Luckily, I’ve had the extremely good fortune to work with a master of refactoring and code maintenance, Coraline Ada Ehmke, as I learn how to clean up my controllers, detect problems, and write a tight test harness. I wanted to pay it forward and share some of the lessons that I am learning from her in our process of building code together.
I’ll go over some things that I learned in our last session, with examples:
- controller methods: form and function
- code smells: method names and variable names
- Hash methods: zip and inject
- An array method: map
- “Or Equals”: an introduction to caching and an elegant alternative to instantiating variables outside of loops
- Sugary Ruby syntax: &:, ?, and ranges (they’re not just for integers!)
Controller Methods: Form and Function
Here is a controller method that facilitates the main data visualizations page of my app:
We got to this, believe it or not, from a probably-forty-line controller method that did everything from set up data hashes to reverse the order of data arrays for display on the main page. Of course, most of that was obvious technical debt: the logic needed to be moved to a model. But which logic should I move to the model, and which logic belongs in the controller?
Mystery Solved: A clean, functional controller method assigns instance variables to be used in the view…and no more.
The above method is a good representation of that: every line is an assignment of an instance variable for the view. And those assignments each use a method from inside a model: there is no logic for how to assign that instance variable inside the actual controller.
That’s not to say that this method is perfect: it’s not, and as a matter of fact it has two very distinctive code smells.
Code Smells: Method Names and Variable Names
Coraline explained this most succinctly: “If you have a noun as a method name, that’s often an indication that the noun should have a controller of its own.” Such is the case with ‘dashboard:’ it really needs its own model and controller to allow programmers to easily set up dashboards by date, by country, by actor, or even by other data currently stored in the event resource for this app. That change is upcoming, but this was a good opportunity to point it out.
There’s another code smell here: name-spaced variables. We have ‘types_in_past_days’ and ‘actors_in_past_days’ rather than the ability to assign those data hashes to their charts, say, with parameters. They’re a byproduct of insufficient model architecture. This is another upcoming change to the app.
So we know that creating a controller for the dashboard remains to be done. But, during this session, Coraline and I made a model for the dashboard. In so doing, we used a tool: Poro Plus.
Poro plus adds some useful functionality to plain old Ruby objects, or POROs. In this app, we used its ability to take variables defined in attr_accessor and set them in the constructor upon instantiating the object. So the code above lets me say:
and set the variable, just like that. We also used it on a tiny class that we placed below the Dashboard class inside the Dashboard file: Statistic, an object that is only to be used by the Dashboard model, at least for now:
Hash Methods: zip and inject
Inside the class, we made a method that would obtain an array of dates corresponding to the number_of_days (ago, or between dates) that the dashboard visualizations should represent. We have a separate method for determining which types of events were committed by which actors on those days. How do we make a hash out of these two, with the dates as keys and the corresponding type counts as values? Easy. We take the two arrays of the same length (dates and type counts for each date), and we zip them together:
So what do we do when we don’t have the values in an array yet? That can be a job for the inject method, which gets called on an array of soon-to-be-keys, takes in an empty hash as an argument, as seen below, and assigns values to each key in syntax consistent with the way we usually use hashes.
This is much clearer, both for expressing intent and for being easy on the eyeballs, compared to running an each loop on the array and doing
hash.merge!() every time with a new key-value pair.
Next, let’s discuss a useful array method: map.
What does map do? It is called on one array and, for each element in that array, it does something to that element (in this case converting to string for time, which is how you pronounce it, evidently) and then stores the result of the doing-something in a separate array, leaving the first array untouched. So now you’ll have two arrays of equal length, with one containing the results of doing something to the elements in the other one. Bonus: zip them together to get a hash of the two. In fact, you’ll actually notice that that’s what we did two screenshots ago.
Now let’s talk about ‘or equals.’
‘Or equals’ is represented with
||=, and it means “If this variable isn’t already assigned to something, assign it to the following.” You see it used in the above screenshot to create an array of dates ending today, provided dates is not already set (say, with an ending date we passed in that isn’t today).
Let’s look at some more examples of its use, and why we used it:
Here it is inside of the statistics method in the Statistics class. Look at that constructor. There is a lot of information in the statistics object, including two database hits. In this method, it’s three in all. We don’t want to have to make those database calls every single time we need our statistics. So, what can we do instead? Cache it, or store it in the short-term memory of the program—that being its memory upon one page-load of the dashboard—so we can call on it after having made those hits just once. We can use
||= for this: we know that “when @statistics is not already assigned, do the following.” In this pageload, of course, statistics was already assigned. So now we can use it! No more database calls.
There’s also this, which may be my favorite thing about ‘or equals’:
What is this? This is “create a hash called ‘hash’ with the keys being the elements in the events array. Each time we come across an element in the events array, if it is already a key in the hash, we add one to its value. If not, we put it in the hash as a key and then add one to its value.”
No if loops. No for loops. No each loops. In this particular case, the syntactic sugariness of this approach was especially attractive because we had the unenviable situation in which the data we needed got spread across two columns (actor1 and actor2) inside the database. This looked downright ugly before ‘or equals’ saved the day.
Even without that caveat, syntactic sugar is lovely.
Image copyright by Hasbro.
Let’s talk about some more syntactically sugary Ruby.
&: — admittedly, this syntax looks weird. But, it allows you to call a method on each object in a group of objects by passing in the method you want to call as an argument for whatever you’re doing.
In this case, mapping. The alternative is this:
Event.all.map do |event|
=? — This one is best explained, I think, by an example:
This means “the key should point to the count of all events where the attribute in question is the same as the key.
Ranges: .. — This one you may already be familiar with. 1..6 produces an array of all the integers between 1 and 6. But you can actually use ranges for things other than integers, too.
Here, it is used for honest-to-goodness dates. And, in so doing, it avoids creating an each loop to stick all the events in question into an array.
I confess, before this pairing session I knew in passing that Coraline didn’t care much for if loops and for loops, but I couldn’t imagine how one would manage to get around them. My programs often contained many of them—not infrequently, several nested ones. But with a little new vocabulary, it’s becoming much clearer to me how one can replace these loops with clearer, more succinct code.
More on our progress later!