Category Archives: Programming

Atomic commits

Ruined soup

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.

Order and chaos

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:

  1. Decide what you want to accomplish
  2. Devise a test you can perform to see if #1 is done (manual or automated)
  3. Perform the test from step 2
  4. Write a line of code
  5. Repeat test from step 2 until it passes
  6. 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.

Don’t trust yourself

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.

Human brains are no match for programming

Why programming is hard for brains

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.

Why I think live-coding interviews are a good idea

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.

Abstraction in Rails

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.

When I do TDD and when I don’t

Some developers advocate doing test-driven development 100% of the time. Other developers think TDD is for the birds and don’t do it at all. Still other developers go in the middle and practice TDD more than 0% of the time but less than 100% of the time.

I personally am in the camp of practicing TDD some of the time but not all. Here’s my reasoning.

When TDD makes sense to me

It’s not the case that I use TDD, or even write tests at all, for every single project I work on. But I do pretty much always program in feedback loops.

Feedback loops

The “feedback loop method” is where work as follows. First, I think of a tiny goal that I want to accomplish (e.g. make “hello world” appear on the screen). Then I decide on a manual test I can perform in order to see if that goal is accomplished (e.g. refresh the page and observe). Then I perform the test, write some code to try to make the test pass, perform the test again, and repeat the process with a new goal.

TDD == automated feedback loops

The way I view TDD is that it’s just the automated version of the manual work I was going to do anyway. Instead of making a to-do note that says “make ‘hello world’ appear on the screen” and then manually refreshing the page to see if it’s there, I write a test that expects “hello world” to appear on the screen. All the other steps are the exact same.

I’ve found that TDD works great for me when I’m working on what you might call “crisply-defined” work. In other words, the requirements I’m working to fulfill are known and specified. I find that I’ve found that there are other scenarios where TDD doesn’t work so great for me.

When TDD doesn’t make sense to me

Coding as production vs. coding as thinking

It’s easy to think that the reason to write code is to create a work product. But that’s certainly not the only reason to write code. Code isn’t just a medium for producing a product. It’s also a medium for thinking.

This is the idea behind a “spike” in Agile programming. When you’re doing a spike, you have no necessary intention to actually keep any of the code you’re writing. You’re just exploring. You’re seeing what it looks like when you do this or how it feels when you do that.

You can think of coding kind of like playing a piano. Sometimes you have some pieces of music already in your head and you’re trying to record an album. Other times you’re just messing around to see you can come up with any music worth recording. These are two very different modes of engaging with your instrument. Both are very necessary in order to ultimately record some music.

TDD doesn’t mix great with spikes

I often find that a spike phase is necessary when I’m coding, for example, a feature with known big-picture requirements but unknown UI specifics. In that case my test would be so full of guesses and placeholders that it would be kind of a joke of a test, and it wouldn’t help me much. In these cases I give myself permission to forego the testing during the spike period. I come back after I have some working code and backfill the tests.

Takeaways

  • I don’t practice TDD 100% of the time. (I believe I do practice TDD the vast majority of the time though.)
  • I view TDD as the automated version of the coding workflow that I already use anyway.
  • Producing a work product is not the only reason to write code. Code can also be a medium for thinking.
  • When I’m in the mode of using coding as a way to think, I find that the benefits of TDD don’t really apply.

How to apply a bugfix

Any bugfix job has three distinct stages: reproduction, diagnosis, and fix. This post is about the third phase, the fix.

It might seem like the “fix” phase of a bugfix is simple: just fix it. But it’s not that simple. Many people screw it up. I certainly have.

They key thing with a bugfix is that you have to have a way to be sure your bugfix actually fixed the bug. So before you apply a bugfix, devise a test (which can be a manual test or an automated test) which has the following qualities:

  • The test fails when the bug is present
  • The test passes when the bug is absent

And, crucially, perform the test both before and after the fix has been applied. If you don’t perform the test before the fix is applied, you don’t know if the test is passing afterward because your fix worked or because your test gives a false positive.

And not only is it important to make sure your test fails when the bug is present, but that it fails in the precise way you expect. If the test fails in a way other than what you expect, then it might be failing for a reason that’s irrelevant to the bug, and you can’t be sure that your test is a valid one.

By following this process, you can have a higher chance of fixing your bug just once rather than having one of more false fixes before you finally succeed.

Don’t mix jobs

Many programmers spend most of their working time in a very wasteful manner.

One of the biggest sources of waste is the mistake of mixing jobs. In this post I’ll explain what I mean by mixing jobs, why it’s bad, and how to avoid it.

Mixing the “what” with the “how”

When you write a line of code, you’re actually carrying out two jobs. One job is to decide what you want the line of code to do. The other job is to figure out the exact syntax to accomplish your goal. You could call these two jobs “what to do” and “how to do it”.

Mixing a “main job” with a “side job”

Another form of mixing jobs is to start working on a certain job, then notice some semi-relevant problem or opportunity along the way, and immediately start addressing the new job in a way that’s not separate from the original job.

Why mixing jobs is bad

Almost everything we do in programming is to accommodate the fact that human brains are weak and frail in the face of the staggeringly complex task of building and maintaining software systems. If humans were infinitely smart, we could just write everything in assembly code directly on production servers. But we aren’t, so we need accommodations.

One of these accommodations is to carry out our work serially. It’s much easier to work on exactly one thing at a time, and bring it fully to completion before starting the next task, than to juggle multiple tasks at once.

Working serially might intuitively seem slower. It feels like it would be more efficient to batch things up. But humans are so susceptible to mistakes and oversights and confusions and such, and software systems are so complicated, that batching work costs more time than it saves.

Tips for not mixing jobs

When you’re working on something, always be conscious of the answer to the question “What job am I working on right now? What exactly am I trying to accomplish?” Once you’ve determined that, ask yourself, “Is this actually just one job or are there multiple jobs inside it?” If it makes sense, separate the “what” jobs from the “how” jobs.

It may take some experimentation to find the right granularity. I wouldn’t advocate planning the entire “what” for a program (e.g. writing out the whole thing in pseudocode) before starting any of the “how”. I personally like to decide a few steps’ worth of “what”, then figure out the “how”, then switch back to “what”, and repeat.

Avoiding mixing a main job with a side job is conceptually pretty easy, although it might take some discipline. When you’re working on a job and you notice some other job that needs doing, don’t just switch focus and do that other job. Instead, make a note to do that other job. I personally like to keep a daily to-do list where I keep track of such things. Before the day ends I’ll usually either complete those “side jobs” or, if the side jobs are big enough, I’ll capture them in a way that they can be remembered for later when they can be given the level of care and attention that they call for.

Takeaways

  • Don’t mix the “what” with the “how”.
  • Don’t mix a “main job” with a “side job”.
  • It’s generally faster to carry out work serially than to try to do it in batches or in parallel.

If you want to learn testing, first learn how to program in feedback loops

Most beginner programmers (and even many experienced programmers) take a slow, painful, wasteful approach to programming.

The wasteful way

The way many programmers code is to spend a bunch of time writing code without checking to see that it works, then finally run the program once they’ve accumulated many lines of code. The program inevitably fails.

Next, the programmer sits and puzzles over what might have gone wrong. Since the programmer wrote a lot of code without checking it, there’s a lot of stuff that could possibly be the culprit, and therefore the debugging process is slow and painful.

The debugging process is usually not a systematic one but rather a guessing game. “Maybe it’s this. Nope it’s not that. Maybe it’s this other thing. Nope, it’s not that either. Hmm.” As the clock ticks, frustration mounts, and maybe a little desperation sets in. It’s not fast and it’s not fun.

The smarter way: feedback loops

Instead of working in the slow, painful, wasteful way described above, you can work in feedback loops. As I described in my other post about feedback loops, the feedback loop process goes like this:

  1. Decide what you want to accomplish
  2. Devise a manual test you can perform to see if #1 is done
  3. Perform the test from step 2
  4. Write a line of code
  5. Repeat test from step 2 until it passes
  6. Repeat from step 1 with a new goal

When you use the feedback loop method, it’s hard to run too far astray. If you only write a little bit of code at a time and you keep everything working at all times, then you’re guaranteed to always have a program that’s either fully working or very close to fully working.

Feedback loops and automated testing

Automated testing is just the practice of coding using feedback loops, but with the testing step automated.

Here’s how the feedback loop would go with automated tests involved. The automated test parts are included in bold.

  1. Decide what you want to accomplish
  2. Devise a manual test you can perform to see if #1 is done (write a test)
  3. Perform the test from step 2 (run the test)
  4. Write a line of code
  5. Repeat test from step 2 until it passes (run the test again)
  6. Repeat from step 1 with a new goal

Obviously there’s also a lot of technical knowledge that’s needed in order to write automated tests. For example, there are test frameworks that enable automated testing, there are libraries that help your tests interact with a browser, and there are libraries that help with generating test data. But more important than any particular tool are the principles behind automated testing.

Perhaps the most important idea behind automated testing is the feedback loop. And luckily for you if you’re a beginner, you can learn how to program in feedback loops without having to learn anything to do with automated testing yet. And once you do, writing automated tests will feel much more natural.