When asked to what he attributes his success in life, Winston Churchill purportedly said, “Economy of effort. Never stand up when you can sit down, and never sit down when you can lie down.”
My philosophy with programming is basically the same. Here’s why.
Finite mental energy
Sometimes people say they wish there were more hours in the day. I see it a little differently. It seems to me that the scarce resource isn’t time but energy. I personally run out of energy (or willpower or however you want to put it) well before I run out of time in the day.
Most days for me there comes a certain time where I’m basically spent for the day and I don’t have much more work in me. (When I say “work” I mean work of all kinds, not just “work work”.) Sometimes that used-up point comes before the end of the workday. Sometimes it comes after. But that point almost always arrives before I’m ready to go to bed.
The way I see it, I get a finite supply of mental energy in the morning. The harder I think during the day, the faster the energy gets depleted, and the sooner it runs out. It would really be a pity if I were to waste my mental energy on trivialities and run out of energy after 3 hours of working instead of 8 hours of working. So I try to conserve brainpower as much as possible.
The ways I conserve brainpower
Below are some examples of wasteful ways of working alongside the more economical version.
Wasteful way
Economical way
Keep all your to-dos in your head
Keep a written to-do list
Perform work in units of large, fuzzily-defined tasks
Perform work in units of small, crisply-defined tasks
Perform work (and deployments) in large batches
Perform work serially, deploying each small change as soon as it’s finished
Many programmers make Git commits in a haphazard way that makes it easy to make mistakes and commit things they didn’t mean to.
Here’s a six-step process that I use every time I make a Git commit.
1. Make sure I have a clean working state
In Git terminology, “working state” refers to what you get when you run git status.
I always run git status before I start working on a feature. Otherwise I might start working, only to discover later that the work I’ve done is mixed in with some other, unrelated changes from earlier. Then I’ll have to fix my mistake. It’s cheaper just to check my working state in the beginning to make sure it’s clean.
2. Make the change
This step is of course actually the biggest, most complicated, and most time-consuming, but the content of this step is outside the scope of this post. What I will say is that I perform this step using feedback loops.
3. Run git status
When I think I’m finished with my change, I’ll run a git status. This will help me compare what I think I’m about to commit with what I’m actually about to commit. Those two things aren’t always the same thing.
4. Run git add .
Running git add . will stage all the current changes (including untracked files) to be committed.
5. Run git diff –staged
Running git diff --staged will show a line-by-line diff of everything that’s staged for commit. Just like the step where I ran git status, this step is to help compare what I think I’m about to commit with what I’m actually about to commit—this time at a line-by-line level rather than a file-by-file level.
6. Commit
Finally, I make the commit, using an appropriately descriptive commit message.
The reason I say appropriately descriptive commit message is because, in my opinion, different types of changes call for different types of commit messages. If the content of the commit makes the idea behind the commit blindingly obvious, then a vague commit message is totally fine. If the idea behind the commit can’t easily be inferred from the code that was changed, a more descriptive commit is called for. No sense in wasting brainpower on writing a highly descriptive commit message when none is called for.
Conclusion
By using this Git commit process, you can code faster and with fewer mistakes, while using up less brainpower.
It’s easy to find information online regarding how to use ViewComponent. What’s not as easy to find is an explanation of why a person might want to use ViewComponent or what problem ViewComponent solves.
Here’s the problem that ViewComponent solves for me.
Cohesion
If you just stuff all your domain logic into Active Record models then the Active Record models grow too large and lose cohesion.
A model loses cohesion when its contents no longer relate to the same end purpose. Maybe there are a few methods that support feature A, a few methods that support feature B, and so on. The question “what idea does this model represent?” can’t be answered. The reason the question can’t be answered is because the model doesn’t represent just one idea, it represents a heterogeneous mix of ideas.
Because cohesive things are easier to understand than incohesive things, I try to organize my code into objects (and other structures) that have cohesion.
Achieving cohesion
There are two main ways that I try to achieve cohesion in my Rails apps.
POROs
The first way, the way that I use the most, is by organizing my code into plain old Ruby objects (POROs). For example, in the application I maintain at work, I have objects called AppointmentBalance, ChargeBalance, and InsuranceBalance which are responsible for the jobs of calculating the balances for various amounts that are owed.
I’m not using any fancy or new-fangled techniques in my POROs. I’m just using the principles of object-oriented programming. (If you’re new to OOP, I might recommend Steve McConnell’s Code Complete as a decent starting point.)
Regarding where I put my POROs, I just put them in app/models. As far as I’m concerned, PORO models are models every bit as much as Active Record models are.
Concerns/mixins
Sometimes I have a piece of code which doesn’t quite fit in with any existing model, but it also doesn’t quite make sense as its own standalone model.
In these cases I’ll often use a concerns or a mixin.
But even though POROs and concerns/mixins can go a really long way to give structure to my Rails apps, they can’t adequately cover everything.
Homeless code
I’ve found that I’m able to keep the vast majority of my code out of controllers and views. Most of my Rails apps’ code lives in the model.
But there’s still a good amount of code for which I can’t find a good home in the model. That tends to be view-related code. View-related code is often very fine-grained and detailed. It’s also often tightly coupled (at least from a conceptual standpoint) to the DOM or to the HTML or in some other way.
There are certain places where this code could go. None of them is great. Here are the options, as I see them, and why each is less than perfect.
The view
Perhaps the most obvious place to try to put view-related code is in the view itself. Most of the time this works out great. But when the view-related code is sufficiently complicated or voluminous, it creates a distraction. It creates a mixture of levels of abstraction, which makes the code harder to understand.
The controller
The controller is also not a great home for this view-related code. The problem of mixing levels of abstraction is still there. In addition, putting view-related code in a controller mixes concerns, which makes the controller code harder to understand.
The model
Another poorly-suited home for this view-related code is the model. There are two options, both not great.
The first option is to put the view-related code into some existing model. This option isn’t great because it pollutes the model with peripheral details, creates a potential mixture of concerns and mixture of levels of abstraction, and makes the model lose cohesion.
The other option is to create a new, standalone model just for the view-related code. This is usually better than stuffing it into an existing model but it’s still not great. Now the view-related code and the view itself are at a distance from each other. Plus it creates a mixture of abstractions at a macro level because now the code in app/models contains view-related code.
Helpers
Lastly, one possible home for non-trivial view-related code is a helper. This can actually be a perfectly good solution sometimes. I use helpers a fair amount. But sometimes there are still problems.
Sometimes the view-related code is sufficiently complicated to require multiple methods. If I put these methods into a helper which is also home to other concerns, then we have a cohesion problem, and things get confusing. In those cases maybe I can put the view-related code into its own new helper, and maybe that’s fine. But sometimes that’s a lost opportunity because what I really want is a concept with meaning, and helpers (with their -Helper suffix) aren’t great for creating concepts with meaning.
No good home
The result is that when I have non-trivial view-related code, it doesn’t have a good home. Instead, my view-related code has to “stay with friends”. It’s an uncomfortable arrangement. The “friends” (controllers, models, etc.) wish that the view-related code would move out and get a place of its own, but it doesn’t have a place to go.
How ViewComponent provides a home for view-related code
A ViewComponent consists of two entities: 1) an ERB file and 2) a Ruby object. These two files share a name (e.g. save_button_component.html.erb and save_button_component.rb and sit at a sibling level to each other in the filesystem. This makes it easy to see that they’re closely related to one another.
Ever since I started using ViewComponent I’ve had a much easier time working with views that have non-trivial logic. In those cases I just create a ViewComponent and put the logic in the ViewComponent.
Now my poor homeless view-related code can move into a nice, spacious, tidy new house that it gets all to its own. And just as important, it can get out of its friends’ hair.
And just in case you think this sounds like a “silver bullet” situation, it’s not. The reason is because ViewComponents are a specific solution to a specific problem. I don’t use ViewComponent for everything, I only use ViewComponent when a view has non-trivial logic associated with it that doesn’t have any other good place to live.
Takeaways
If you just stuff all your domain logic into Active Record models, your Active Record models will soon lose cohesion.
In my Rails apps, I mainly achieve cohesion through a mix of POROs and concerns/mixins (but mostly POROs).
Among the available options (views, controllers, models and helpers) it’s hard to find a good place to put non-trivial view-related code.
ViewComponent provides (in my mind) a reasonable place to put non-trivial view-related code.
Side note: if you’re interested in learning more about ViewComponent, you can listen to my podcast conversation with Joel Hawksley who created the tool. I also did a talk on ViewComponent which you can see below.
Let’s say I’m making a pot of tortilla soup. I’ve made tortilla soup before and it has always turned out good. This is a low-risk operation.
But for some reason, this time, I decide to get experimental (high risk). I add some cayenne pepper to the soup. Unfortunately, this makes the soup too spicy, and no one in my family except me will eat it. The soup is now basically garbage and I have to throw it out.
What just happened was bad. I mixed a small high-risk operation (cayenne) into a large low-risk operation (tortilla soup). Since cayenne is impossible to remove from soup, I’ve turned the entire operation into a high-risk operation. And, due to my poor choices, the whole operation got ruined.
Ruined bread
Now let’s say I’m making a different kind of soup, an experimental one. Let’s say I’m making another Mexican soup, pozole. I’ve never even eaten pozole before. In fact, I’m not even sure what pozole is. Who knows if my family will like it. This is a high-risk operation.
On the day I make pozole, I happen to have some homemade bread around. A lot of work went into making the bread. Unlike the pozole, the bread is a low-risk operation. In fact, it’s a no-risk operation because the bread is already made.
Since I know that bread usually goes good with soup, I decide to cut up the loaf of bread and serve it in bowls with the soup.
Unfortunately, when I serve the pozole for dinner, it’s discovered that my pozole is not good. No one, including me, eats very much. What’s more, I’ve wasted a whole loaf of homemade bread in addition to wasting the pozole ingredients.
In this case I mixed a small low-risk operation (the bread) into a large high-risk operation (the pozole). Now not only do I have to throw out the pozole but I have to throw out the bread with it.
What atomic commits are
The literal meaning of “atomic” is “cannot be cut”, from Greek. (The “a” part means “not” and the “tomic” part comes from a word that means “cut”.)
A commit that’s atomic is a commit that’s only “about” one thing. The commit can’t be cut into smaller logical pieces.
Why atomic commits are helpful
One of my programming principles is “keep everything working all the time”. It’s much easier to keep everything in a working state all the time then to allow things to slip into chaos and then try to recover.
But slipping into chaos once in a while is unavoidable. When that happens, the easiest way to get back to “order” usually isn’t to solve the problem, but rather to just blow away the bad work and revert back to your last known good state.
If your commits are non-atomic, then reverting back to your last known good state can be problematic. You’ll be forced to throw the baby out with the bathwater.
If you make my “tortilla soup mistake” and mix a small high-risk change in with a large low-risk change, then you’ll end up reverting a large amount of good work just because of a small amount of bad work. (You might be able to escape this problem if the two changes are sufficiently separate from each other but that’s often not the case.)
If you make my “pozole mistake” and mix a low-risk change in with a high-risk change, then you’ll have to revert the good change along with the bad one as well.
How to decide what constitutes an atomic commit
Unless you’re committing a one-character change, pretty much any commit could conceivably be cut into smaller pieces. So how do you decide what constitutes an atomic commit and what doesn’t?
The answer is that “atomicity” is subjective, and it’s up to you do decide. The heuristic I use to decide what to put in my commits is: the smallest thing that can be considered complete.
Sometimes when I code in front of people, they’re astonished by how frequently I make commits. But I figure since it’s super cheap to make a commit, why not turn the “commit frequency dial” up to the max?
Takeaways
An atomic commit is a commit that’s only “about” one thing.
Atomicity is subjective.
It’s easier to keep everything working all the time than to allow things to slip into a state of chaos and then try to recover.
When things do slip into a state of chaos, it’s usually cheaper to just revert to the last known working state and start over than to try to fix the actual problem.
Atomic commits ensure that, when you do have to revert, you don’t have to throw out the baby with the bathwater.
Imagine you’re building a house out of wooden blocks.
Wooden blocks are easy to build with but they make a somewhat fragile structure. As you’re putting a new block in place, you accidentally bump the house and the whole thing comes tumbling down. The various shapes of blocks get all mixed together when the house crashes. It takes you several minutes to get the house rebuilt to the point of progress you were at before.
Once you’re almost done rebuilding the house, you accidentally bump it again and you send it crashing down a second time. You have to spend another several minutes rebuilding it. This process repeats several times before you finally succeed in building the house to completion.
In certain ways, writing a computer program is like building a house out of blocks. If you make a big enough mistake, you can throw the whole area you’re working on into disorder. Often, the degree of disorder is so great that it’s cheaper just to revert to where you were at some point in the past and start over than it is to recover from where you are now.
Order and chaos
As I’ve observed programmers working over the years (including observing myself), I’ve noticed two possible states a programmer can be in: order and chaos.
In a state of order, you’re in control. You know what you’re doing. Every step you take is a step that gets you closer to your objective.
In a state of chaos, you’re flailing. You’re confused. Your program is broken and you don’t know how to fix it. Chaos is a very expensive state to be in.
The cost of chaos
It costs drastically more to enter a state of chaos and then get back to order than it does to just stay in a state of order the whole time.
I think of order and chaos like a thin trail through thick, dark woods. Order is the trail and chaos is the woods. If you allow yourself to stray off the trail, you might be screwed. It can be so hard to find the trail again that you might never find the trail again. Even though it costs a little extra effort to pay close attention to the trail as you follow it, the cost is a tiny one, and that cost is obviously much lower than the cost of losing the trail and dying in the woods.
What causes programmers to enter a state of chaos, and how can they avoid allowing that to happen?
What causes chaos
Entering a state of chaos while programming might seem like an unavoidable fact of life. And to a certain extent, it is. But some programmers spend almost all their time in a state of chaos while others spend almost all their time in a state of order. It’s not just luck. They’re doing something different.
As I see it, the main risk factor for chaos is length of feedback cycle. The longer the feedback cycle, the higher the chances that something is going to go wrong. And the bigger a change, the harder it is to determine what went wrong, because there’s a greater amount of stuff to dig through in order to find the source of the problem.
How to stay in a state of order
The way to minimize your chances of slipping into a state of chaos is to work in tiny changes and check your work after each change.
As I described in my post about programming in feedback loops, this type of working process can look something like this:
Decide what you want to accomplish
Devise a test you can perform to see if #1 is done (manual or automated)
Perform the test from step 2
Write a line of code
Repeat test from step 2 until it passes
Repeat from step 1 with a new goal
Working in feedback loops of tiny changes is like hiking a trail in the woods and looking down every few steps to make sure you’re still on the trail.
Takeaways
When you’re programming, you’re always in either a state of order or a state of chaos.
Recovering from a state of chaos is expensive. Never entering a state of chaos in the first place in cheap.
The way to avoid entering a state of chaos is to work in feedback loops.
When I’m programming, I have a saying that I repeat to myself: “Never underestimate your ability to screw stuff up.”
It never ceases to amaze me how capable I am of messing up seemingly simple tasks.
Sometimes I get overconfident and, for example, I try to make a big change all at once without checking my work periodically. Usually I’m humbled. I end up creating a mess and I have to blow away my work and start over. I end up creating a lot more work than if I had just stayed humble and careful in the first place.
The advice that I have to frequently remind myself of, and which I give to you, is this. Recognize that your brain is weak with respect to the intellectually demanding task of programming. Work in small, careful steps as part of a feedback loop cycle. Always assume you made a mistake somewhere. Treat your work as guilty until proven innocent. Don’t trust yourself. You’re just setting yourself up for pain and frustration if you do.
The job of a programmer involves building new programs and changing existing programs.
In order to successfully work with a program, a programmer has to do two fairly difficult things. One is that the programmer has to learn and retain some number of facts about the program. The other is that, in order to understand existing behaviors and predict the effects of new changes, the programmer has to play out chains of logic in his or her head.
These two tasks involve what you could metaphorically call a person’s RAM (short-term memory), hard disk (long-term memory) and CPU (raw cognition ability).
The limitations of our mental hardware
RAM
When I poke around in a program before making a change in order to understand the substrate I’m working with, I might make mental notes which get stored in my short-term memory. Short-term memory has the obvious limitation that it’s impermanent. Six months from now I’ll no longer remember most of the stuff that’s in my short-term memory right now.
Hard disk
Other aspects of the program might enter my consciousness sufficiently frequently that they go into long-term memory. Long-term memory has the benefit of being more permanent, but the trade-off is that it costs more to get something into long-term memory than short-term memory. The cases where we can learn something easily and then remember it permanently are rare.
CPU
The other demanding mental task in programming is to look at a piece of code and run it in our minds. When I try to mentally run a piece of code, it’s like I’m combining my mental RAM with my CPU. I not only have to load some pieces of information into my short-term memory but I have to make a series of inferences about how those pieces of information will get transformed after they’ve had a number of operations performed on them. This is a harder task, kind of like trying to multiple two three-digit numbers in one’s head and having to remember each intermediate product to be summed up at the end. People tend not to be great at it.
If human memory were perfect and if our mental processing power were infinite, programming would be easy. But unfortunately our hardware is very limited. The best we can do is to make accommodations.
What if we had infinitely powerful brains?
If humans had infinitely powerful brains, programming would look a lot different.
We could, for example, read a program’s entire source code in one sitting, memorize it all the first time around, then instantly infer how the program would behave given any particular inputs.
It wouldn’t particularly matter if the code was “good” or “bad”. For all we cared, you could name the variables a, b, c, and so on. We would of course have to be told somehow that a really means customer, b really means order and so on, but thanks to our perfect memories, we would only have to be told once and we would remember forever. And thanks to our infinitely powerful mental CPUs, it wouldn’t cost anything to map all the cryptic variable names to their real meanings over and over again.
But our brains are weak, so…
So we make accommodations. Here are a few examples of weaknesses our brains have and solutions (or at least mitigations) we’ve come up with to accommodate.
We can’t read and memorize an entire codebase
The best we can do is to understand one small part of a system, often at great cost. The solution? Try to organize our code in a modular, loosely-coupled way, so that it’s not necessary to understand an entire system in order to successfully work with it.
We easily can’t map fake names to real names
When a codebase is full of variables, functions and classes whose names are lies, we get confused very easily, even if we know the real meanings behind the lies. We can neither easily remember the mappings from the lies to the real names, nor can we easily perform the fake-name-to-real-name translations. So the solution (which seems obvious but is often not followed in practice) is to call things what they are.
We’re not good at keeping track of state
Imagine two people, John and Randy, standing at the same spot on a map. John moves 10 feet north. Randy moves 200 feet west. John moves 10 feet west. A third person, Sally, shows up 11 feet to the west of Randy. Now they all move 3 feet south. Where did they all end up relative to John and Randy’s starting point? I’m guessing you probably had to read that sequence of events more than once in order to come up with the right answer. That’s because keeping track of state is hard. (Or, more precisely, it’s hard for us because we’re not computers.) One solution to this problem is to code in a declarative rather than imperative style.
Broadly speaking, our mental weaknesses are the whole reason we bother to write good code at all. We don’t write good code because we’re smart, we write good code because we’re stupid.
Takeaways
Our mental RAM, hard disk and CPU are all of very limited power.
Many of the techniques we use in programming are just accommodations for our mental weaknesses.
The more we acknowledge and embrace our intellectual limitations, the easier and more enjoyable time we’ll have with programming.
It’s popular to be critical of live-coding interviews. But like many issues that people reduce to “X is good” or “X is bad”, I think the question of live coding interviews is actually more complicated than that. Here’s my take.
The criticisms of live coding interviews
The content isn’t realistic
This can obviously be true. If a candidate is asked in an interview to, for example, live-code an implementation of a linked list or a binary search, then the candidate is being asked to do something that’s not really representative of what they’d actually do in a typical programming job.
Programmers who pass these tests are demonstrating that they’re capable of passing computer-sciencey type tests, but they’re not necessarily demonstrating that they have skills that match the job they’re interviewing for. So these types of tests aren’t very smart ones.
But to use these as a criticism of live coding tests is a confusion. If many live-coding tests contain unrealistic content, that doesn’t prove that live-coding tests are bad, it only proves that live-coding tests with unrealistic content are bad.
The spotlight and pressure changes the outcome
Sometimes developers choke on live-coding exercises not because they’re not capable of completing the task but because the interview process made them too nervous to think clearly enough.
I do think this is a valid criticism of live-coding tests. Having said that, there is at least one thing that can be done to mitigate the nervousness factor. That is: don’t make it a race. A live-coding test doesn’t have to be “complete X in Y amount of time or you fail”. When I’ve run live-coding tests myself in the past, I limit the time we spend, but I don’t require candidates to finish the exercise as a condition to passing. Rather than “Did you complete the entire exercise?” the question is “Did you get anything to work at all?” Candidates who fail to get anything to work don’t pass the test.
Why I’m in favor of live-coding interviews
End products don’t tell the whole story
Given a particular challenge, two different developers may arrive at roughly the same solution. But one developer may have struggled with the challenge for two hours while the other developer solved it in five minutes. I contend that this difference matters.
If I give developers a take-home exercise, then all I can see is the end product. I don’t get to see how they arrived at the end product. Maybe developer A has 20 years of experience but they approached the task in a slapdash and thoughtless manner and struggled for hours before finally piecing together the right answers. Maybe developer B has only 3 years of experience but methodically homed in on the answer in just a few minutes because they’re smarter.
Selecting for intellectual character over experience
Because end products don’t tell the whole story, judging by end products alone may select for experience over raw brainpower. If the candidate submits a solution that works but looks sloppy, how do you know if that mistake is due to lack of intelligence or just lack of experience?
It would be a real shame IMO to reject someone who’s really smart and has great potential just because at the current moment they’re not very experienced. I’d much rather take an inexperienced but smart person who will be 10X better a year from now than a very experienced but not-very-smart person who is not only slow but also will never get much better.
And as a side note, I think “smart” is way too vague of a word to use when evaluating programmer potential. Plenty of people are “smart” in the sense that they can solve hard logic puzzles or write complicated algorithms, but many of those “smart” people also make shockingly incompetent developers because their intelligence is of a very narrow kind. Other traits that I think make a good programmer include things like intellectual honesty, intellectual rigor, inquisitiveness and discipline. These qualities make a world of difference. And I don’t think they’re very evident in the end result of a take-home coding test.
When scrutiny isn’t necessary
The only reason to do something like a live-coding interview, or any interview process at all, is if you don’t yet know how good the person is and if they’re the kind of person you’d like to work with. If other, better ways are available, then certain aspects of the interview process becomes less necessary. For example, if someone has a large body of publicly available work, or if you’ve already worked with them before, then you can potentially use that to judge the person’s abilities and you don’t have to contrive tests for them. But most programming candidates don’t have much to be judged by, and so unfortunately in most cases we have to do this painful song and dance we call the interview process.
Summary
I think live-coding tests can either be done well or poorly. Live-coding tests have some unavoidable drawbacks, but so does everything. The value of live-coding tests, when done in a smart way, is that they reveal how a person thinks and behaves while coding. To me this is the most important thing to learn about a programming candidate. I don’t see how this information can be gained any way other than actually watching them code.
If we wanted to, we could, of course, write web applications in assembly code. Computers can understand assembly code just as well as Ruby or Python or any other language.
The reason we write programs in higher-level languages like Ruby or Python is that while assembly language is easy for computers to understand, it’s of course not easy for humans to understand.
High-level languages (Ruby, Python, Java, C++, etc.) provide a layer of abstraction. Instead of having to think about a bunch of low-level details that we don’t care about most of the time, we can specify the behavior of our programs at a higher, more abstracted level. Instead of having to expend mental energy on things like memory locations, we can focus on what our program actually does.
In addition to using the abstractions provided by high-level languages, we can also add our own abstractions. A function, for example, is an abstraction that hides low-level details. An object can serve this purpose as well.
We’ll come back to some technical details regarding what abstraction is. First let’s gain a deeper understanding of what an abstraction is using an analogy.
Abstraction at McDonald’s
Let’s say I go to McDonald’s and decide that I want a Quarter Pounder Meal. The way I express my wishes to the cashier is by saying “Quarter Pounder Meal”. I don’t specify the details: that I want a fried beef patty between two buns with cheese, pickles, onions, ketchup and mustard, along with a side of potatoes peeled and cut into strips and deep-fried. Neither me nor the cashier cares about most of those details most of the time. It’s easier and more efficient for us to use a shorthand idea called a “Quarter Pounder Meal”.
The benefit of abstraction
As a customer, I care about a Quarter Pounder Meal at a certain level of abstraction. I don’t particularly care whether the ketchup goes on before the mustard or if the mustard goes on before the ketchup. In fact, I don’t even really think about ketchup and mustard at all most of the time, I just know that I like Quarter Pounders and that’s what I usually get at McDonald’s, so that’s what I’ll get. For me to delve any further into the details would be for me to needlessly waste brainpower. To me, that’s the benefit of abstraction: abstraction lets me go about my business without having to give or receive information that’s more detailed than I need or want. And of course the benefit of not having to work with low-level details is that it’s easier.
Levels of abstraction
Even though neither the customer nor the cashier want to think about most of the low-level details of a Quarter Pounder Meal most of the time, it’s true that sometimes they do want to think about those details. If somebody doesn’t like onions for example, they can drop down a level of abstraction and specify the detail that they would like their Quarter Pounder without onions. Another reason to drop down a level of abstraction may be that you don’t know what toppings come on a Quarter Pounder, and you want to know. So you can ask the cashier what comes on it and they can tell you. (Pickles, onions, ketchup and mustard.)
The cook cares about the Quarter Pounder Meal at a level of abstraction lower. When a cook gets an order for a Quarter Pounder, they have to physically assemble the ingredients, so they of course can’t not care about those details. But there are still lower-level details present that the cook doesn’t think about most of the time. For example, the cook probably usually doesn’t think about the process of pickling a cucumber and then slicing it because those steps are already done by the time the cook is preparing the hamburger.
What would of course be wildly inappropriate is if me as the customer specified to the cashier how thick I wanted the pickles sliced, or that I wanted dijon mustard instead of yellow mustard, or that I wanted my burger cooked medium-rare. Those are details that I’m not even allowed to care about. (At least I assume so. I’ve never tried to order a Quarter Pounder with dijon mustard.)
Consistency in levels
Things tend to be easiest when people don’t jump willy-nilly from one level of abstraction to another. When I’m placing an order at McDonald’s, everything I tell the cashier is more or less a pre-defined menu item or some pre-agreed variation on that item (e.g. no onion). It would probably make things weird if I were to order a Quarter Pounder Meal and also ask the cashier to tell me the expiration dates on their containers of ketchup and mustard. The cashier is used to taking food orders and not answering low-level questions about ingredients. If we jump among levels of abstraction, it’s easy for the question to arise of “Hang on, what are we even talking about right now?” The exchange is clearer and easier to understand if we stick to one level of abstraction the whole time.
Abstraction in Rails
In the same way that abstraction can ease the cognitive burden when ordering a Quarter Pounder, abstraction can ease the cognitive burden when working with Rails apps.
Sadly, many Rails apps have a near-total lack of abstraction. Everything that has anything to do with a user gets shoved into app/models/user.rb, everything that has anything to do with an order gets shoved into app/models/order.rb, and the result is that every model file is a mixed bag of wildly varying levels of abstraction.
Soon we’ll discuss how to fix this. First let’s look at an anti-example.
Abstraction anti-example
Forem, the organization behind dev.to, makes its code publicly available on GitHub. At the risk of being impolite, I’m going to use a piece of their code as an example of a failure to take advantage of the benefits of abstraction.
Below is a small snippet from a file called app/models/article.rb. Take a scroll through this snippet, and I’ll meet you at the bottom.
class Article < ApplicationRecord
# The trigger `update_reading_list_document` is used to keep the `articles.reading_list_document` column updated.
#
# Its body is inserted in a PostgreSQL trigger function and that joins the columns values
# needed to search documents in the context of a "reading list".
#
# Please refer to https://github.com/jenseng/hair_trigger#usage in case you want to change or update the trigger.
#
# Additional information on how triggers work can be found in
# => https://www.postgresql.org/docs/11/trigger-definition.html
# => https://www.cybertec-postgresql.com/en/postgresql-how-to-write-a-trigger/
#
# Adapted from https://dba.stackexchange.com/a/289361/226575
trigger
.name(:update_reading_list_document).before(:insert, :update).for_each(:row)
.declare("l_org_vector tsvector; l_user_vector tsvector") do
<<~SQL
NEW.reading_list_document :=
setweight(to_tsvector('simple'::regconfig, unaccent(coalesce(NEW.title, ''))), 'A') ||
setweight(to_tsvector('simple'::regconfig, unaccent(coalesce(NEW.cached_tag_list, ''))), 'B') ||
setweight(to_tsvector('simple'::regconfig, unaccent(coalesce(NEW.body_markdown, ''))), 'C') ||
setweight(to_tsvector('simple'::regconfig, unaccent(coalesce(NEW.cached_user_name, ''))), 'D') ||
setweight(to_tsvector('simple'::regconfig, unaccent(coalesce(NEW.cached_user_username, ''))), 'D') ||
setweight(to_tsvector('simple'::regconfig,
unaccent(
coalesce(
array_to_string(
-- cached_organization is serialized to the DB as a YAML string, we extract only the name attribute
regexp_match(NEW.cached_organization, 'name: (.*)$', 'n'),
' '
),
''
)
)
), 'D');
SQL
end
end
Given that dev.to is largely a blogging site, the concept of an article must be one of the most central concepts in the application. I would imagine that the Article would have a lot of concerns, and the 800-plus-line article.rb file, which contains a huge mix of apparently unrelated stuff, shows that the Article surely in fact does have a lot of concerns connected to it.
Among these concerns, whatever this trigger thing does is obviously a very peripheral one. If you were unfamiliar with the Article model and wanted to see what it was all about, this database trigger code wouldn’t help you get the gist of the Article at all. It’s too peripheral and too low-level. The presence of the trigger code is not only not helpful, it’s distracting.
The trigger code is at a much lower level of abstraction than you would expect to see in the Article model.
The fix to this particular problem could be a very simple one: just move the trigger code out of article.rb and put it in a module somewhere.
class Article < ApplicationRecord
include ArticleTriggers
end
The trigger code itself is not that voluminous, and I imagine it probably doesn’t need to be touched that often, so it’s probably most economical to just move that code as-is into ArticleTriggers without trying to improve it.
Another anti-example
Here’s a different example which we’ll address in a little bit of a different way.
There are a couple methods inside article.rb, evaluate_markdown and evaluate_front_matter.
class Article < ApplicationRecord
def evaluate_markdown
fixed_body_markdown = MarkdownProcessor::Fixer::FixAll.call(body_markdown || "")
parsed = FrontMatterParser::Parser.new(:md).call(fixed_body_markdown)
parsed_markdown = MarkdownProcessor::Parser.new(parsed.content, source: self, user: user)
self.reading_time = parsed_markdown.calculate_reading_time
self.processed_html = parsed_markdown.finalize
if parsed.front_matter.any?
evaluate_front_matter(parsed.front_matter)
elsif tag_list.any?
set_tag_list(tag_list)
end
self.description = processed_description if description.blank?
rescue StandardError => e
errors.add(:base, ErrorMessages::Clean.call(e.message))
end
def evaluate_front_matter(front_matter)
self.title = front_matter["title"] if front_matter["title"].present?
set_tag_list(front_matter["tags"]) if front_matter["tags"].present?
self.published = front_matter["published"] if %w[true false].include?(front_matter["published"].to_s)
self.published_at = parse_date(front_matter["date"]) if published
set_main_image(front_matter)
self.canonical_url = front_matter["canonical_url"] if front_matter["canonical_url"].present?
update_description = front_matter["description"].present? || front_matter["title"].present?
self.description = front_matter["description"] if update_description
self.collection_id = nil if front_matter["title"].present?
self.collection_id = Collection.find_series(front_matter["series"], user).id if front_matter["series"].present?
end
end
These methods seem peripheral from the perspective of the Article model. They also seem related to each other, but not very related to anything else in Article.
These qualities to me suggest that this pair of methods are a good candidate for extraction out of Article in order to help keep Article at a consistent, high level of abstraction.
“Evaluate markdown” is pretty vague. Evaluate how? It’s not clear exactly what’s supposed to happen. That’s fine though. We can operate under the presumption that the job of evaluate_markdown is to clean up the article’s body. Here’s how we could change the code under that presumption.
class Article < ApplicationRecord
def evaluate_markdown
body_markdown = ArticleBody.new(body_markdown).cleaned
end
end
With this new, finer-grained abstraction called ArticleBody, Article no longer has to be directly concerned with cleaning up the article’s body. Cleaning up the article’s body is a peripheral concern to Article. Understanding the detail of cleaning up the article’s body is neither necessary nor helpful to the task of trying to understand the essence of the Article model.
Further abstraction
If we wanted to, we could conceivably take the contents of evaluate_markdown and evaluate_front_matter change them to be at a higher level of abstraction.
Right now the bodies of those methods seem to deal at a very low level of abstraction. They deal with how to do the work rather than what the end product should be. In order to understand what evaluate_markdown does, we have to understand every detail of what evaluate_markdown does, because it’s just a mixed bag of low-level details.
If evaluate_markdown had abstraction, then we could take a glance at it and easily understand what it does because everything that happens would be expressed in the high-level terms of what rather than the low-level terms of how. I’m not up to the task of trying to refactor evaluate_markdown in this blog post, though, because I suspect what’s actually needed is a much deeper change and a different approach altogether, rather than just a superficial polish. Changes of that depth that require time and tests.
How I maintain a consistent level of abstraction in my Rails apps
I try not to let my Active Record models get cluttered up with peripheral concerns. When I add a new piece of behavior to my app, I usually put that behavior in one or more PORO models rather than an Active Record model. Or, sometimes, I put that behavior in a concern or mixin.
The point about PORO models is significant. In the Rails application that I maintain at my job, about two-thirds of my models are POROs. Don’t make the mistake of thinking that a Rails model has to be backed by Active Record.
Takeaways
Abstraction is the ability to engage with an idea without having to be encumbered by its low-level details.
The benefit of abstraction is that it’s easier on the brain.
Active Record models can be made easier to understand by keeping peripheral concerns out of the Active Record models and instead putting them in concerns, mixins or finer-grained PORO models.