Author Archives: Jason Swett

What should I NOT write tests for?

One of the most common questions about testing, including what to write tests for, is what NOT to write tests for.

When people ask me what to write tests for, my honest but maybe not very helpful answer is “basically everything”. But I don’t test literally absolutely everything. There are some cases when I choose to skip tests.

My criteria for skipping a test

My habit of writing tests for everything leads me to write tests for my code by default. For most of the code I write, I actually finder it harder to write the code without tests than with tests.

But sometimes I get lazy or my instincts tell me that a test would be a waste of time. When I’m feeling like this, I ask myself three questions:

  1. If this code were to misbehave, would it fail silently?
  2. If this code were to misbehave, how bad would the consequences be?
  3. If this code were to misbehave, how frequently would it fail?
  4. How costly would it be to write a test?

If the code I’m working on would fail in a very obvious way, and the consequences are minor, and the failure would only happen once a year, and the test would be very costly to write, then that’s a case where I would probably skip the test.

On the other hand, if the code would fail silently, OR if the consequences would be bad, OR if the failure would be frequent, OR if it’s super easy to write the test, then I would just write the test.

You could think of these questions as a boolean expression where all the items get OR’d together:

“Should I write a test?” formula (boolean OR)

  1. Might fail silently?
  2. Consequences might be bad?
  3. Might fail frequently?
  4. Test is easy to write?

If any one of the items is true, then the whole boolean expression is true and I go ahead and write the test. Otherwise I skip the test without guilt or worry.

Behavior vs. implementation

There are also entire types of tests I avoid.

I don’t test for things like the presence of associations, the presence of methods, etc. Such tests are pointless. Rather, I test the behavior that these things enable. Testing the behavior is the only way you can really be sure anything works.

Takeaways

  • I’ll skip a test if and only if the feature won’t fail silently, the consequences won’t be bad, the failure won’t occur frequently, and the test is expensive to write.
  • I don’t test implementation details, but rather I test the behaviors that the implementations enable.

How to get familiar with a new codebase

The question

Someone submitted the following question to me on my ask me a question page:

One of the things I learned from my RoR bootcamp (Le Wagon) was test driven dev. Using that knowledge, when I joined my first job, I wasn’t given much guidance to onboard (start up and I was first internal dev), so I started by reading and learning the system from the rspec files.

What would your advice be for new to industry, junior devs as to the most effective way to learn a new monolith / code base?

Here’s my advice for how to get familiar with a new codebase.

Having the right objective

The first step in getting familiar with a new codebase is to realize that it’s an impossible goal!

Unless the codebase is really tiny, no one, no matter how smart or experienced, can understand the whole thing. And even if you could understand the whole thing, I think it would be a waste of effort.

Rather than trying to understand an entire codebase, I think it’s more useful to try to understand the area of the code where you need to make a change. After all, the only reason for needing to understand an area of code is in order to safely make a change to it.

Now let’s talk about how to understand a piece of code. I find it helpful to list the obstacles to understanding a piece of code.

The obstacles to understanding code

The following things can stand in the way of understanding a piece of code:

  • Unfamiliar technologies
  • Unfamiliar domain concepts
  • Sheer complexity
  • Poor-quality code

Let’s discuss each, including how to address it.

Unfamiliar technologies

The answer to this one is straightforward although not easy: get familiar with those technologies.

When I have to learn a new technology, I personally like to spin up a scratch project to teach myself about it in isolation. Learning a new technology is easier when it’s not mixed with other stuff like unfamiliar domain concepts and somebody else’s code.

Sometimes I like to bounce between a scratch project and a production project. Too much scratch coding and the learning can get too detached from what’s relevant to the production project. To much production coding and it can be hard to separate the difficulties presented by the unfamiliar technology from the difficulties presented by everything else.

Unfamiliar domain concepts

Domain knowledge can be hard to acquire. Software technologies often have documentation you can read, but domain knowledge often has to be acquired through experience or just by having someone tell you.

The unfortunate truth about domain knowledge is that, quite often, you just have to ask your co-workers to tell you. If you’re lucky you may be able to supplement your learning with things like Wikipedia and books.

Sheer complexity

Complex things are obviously harder to understand than simple things. Sometimes it’s helpful to acknowledge that in addition to unfamiliar technologies, unfamiliar domain concepts and hard-to-understand code, some things are just complex. To me, articulating precisely why something is hard to understand is half the battle toward understanding it.

When I want to try to understand something complex, I try to break it down into parts. For example, I started to understand cars a lot better when I understood that a car consists of several somewhat separate systems including the engine, the braking system, the steering system, the heating and cooling system, etc. Cars became a little easier for me to understand once I realized that these separate systems were present and that I could understand each system more or less in isolation.

Poor-quality code

My definition of bad code is code that’s hard to understand and change. Sadly, quite a lot of code in the world, perhaps the vast majority of it, is pretty bad, and therefore hard to work with.

One of the highest-yield techniques I’ve encountered for understanding bad code (which I learned from Working Effectively with Legacy Code) is to do a “scratch refactoring”. With this technique, I take a piece of code and freely rename variables, move things around, etc., with no intention of ever committing my changes. Sometimes this act can lead to a useful burst of insight.

Honestly, the most helpful thing I can say about dealing with legacy code is that you should buy Working Effectively with Legacy Code and read it. There’s too much to say about legacy code for me to repeat it all here, and most of what I could say would be redundant to the book anyway.

Takeaways

  • Getting familiar with an entire codebase is impossible. Instead, focus on getting familiar with the parts you need to change.
  • There can be several different reasons why an area of code can be hard to understand. When trying to understand a piece of code, try to identify the reason(s) the code is hard to understand and then address each reason individually.

How to defend good code

Why good code needs defending

Good code quite frequently comes under fire. Managers explicitly or implicitly pressure developers to cut corners in order to “move fast”.

And sadly, even programmers sometimes argue against writing good code. They say things like “it doesn’t always need to be perfect, because after all we need do to ship”.

These arguments sound reasonable on the surface but, as we’ll see, they contain subtle lies.

The biggest lie in many arguments against good code is that programmers spend too much time gold-polishing their code. To me, cautioning programmers against gold-polishing is kind of like, for example, cautioning Americans not to starve themselves and become too skinny. Sure, it’s a theoretical danger, but our in reality our problem is overwhelmingly the opposite one. Similarly, the problem in the software industry is not that we spend too much time writing good code, but that we spend too much time wrestling with bad code.

If you ever find yourself pressured to write sloppy code, my goal with this post is to arm you with some arguments you can use to push back.

Here’s what I’ll go over:

  • What “good code” means
  • Weak arguments for writing good code
  • Weak arguments for writing bad code
  • My argument for writing good code
  • Let’s start with what “good code” means to me.

    What “good code” means

    Good code is code that’s fast to work with. Good code is easy to understand and change. To me it’s nothing more than that.

    If the code can be changed quickly and easily, then it’s good, by definition. If the code is slow and difficult to change then it’s bad. Any specific coding practices like short methods, clear names or anything else are just incidental. The only thing that matters is whether the code can be changed quickly and easily.

    One reason I like this definition of good code is that it also ought to be appealing to everyone. Developers like code that’s quick and easy to change. Non-technical stakeholders also ought to like the idea of code that’s quick and easy to change.

    People might not understand exactly what it means for code to be “high quality” or “good”, but they can certainly understand what it means to be able to work quickly.

    Anytime you have to make a defense for writing good code, it seems smart to remind your “opponent” (who will hopefully become your ally) that your goal is to move fast.

    Before we address some of the bad arguments for writing bad code in order to refute them, let’s first talk about some bad arguments for writing good code. It’s good to be aware of the bad arguments for your case so you can avoid trying to use them.

    Weak arguments for writing good code

    I think if we’re going to write good code, we should have a clear understanding of why we’re doing it. We should also be able to articulate to others exactly why we’re doing it.

    The bad arguments I’ve heard for writing good code include things like “craftsmanship”, “professionalism” and “integrity”.

    Saying something like “I write good code because it’s more professional to write good code” is a little bit of a copout. It doesn’t explain why it’s more professional to write good code.

    Same with craftsmanship. You can say “I write good code because I believe in craftsmanship”. But that doesn’t explain what the benefits of craftsmanship supposedly are.

    Such appeals are also selfish. They speak to what makes the programmer feel good, not to what benefits the business. These types of arguments are unlikely to be persuasive except perhaps to other programmers.

    So when people pressure me to cut corners to get a job done quickly, I don’t ever push back with talk about craftsmanship or professionalism.

    Weak arguments for writing bad code

    Finally, here are some bad arguments for doing sloppy work and why I think each one is flawed.

    “Perfect is the enemy of the good”

    This is a good and useful saying for the cases to which it actually applies. For example, when you’re talking about project scope, “perfect is the enemy of the good” is a good saying to keep in mind. A bent toward perfectionism can eat up all your time and keep you from shipping something that’s good.

    But with respect to code quality, “perfect is the enemy of the good” is almost always a false premise. Comically so, in fact.

    The typical spectrum of possibilities for a coding change usually doesn’t range from “perfect” to merely “good”. Usually it ranges from “acceptable” to “nightmarish”. A more honest version of this saying would be “acceptable is the enemy of the nightmarish”.

    Refutation: If someone tries to pull “perfect is the enemy of the good” on you, you can say, “Oh, don’t worry, I’m not trying to make it perfect, I’m just trying to make the code understandable enough so I can work with it.” This statement is hard to refute because it appears as though you’re agreeing with the other person. Plus no reasonable person would argue against making the code understandable enough to work with. What you’re saying is also true: you’re not trying to make the code perfect. You’re just trying to make it not nightmarish.

    “Users don’t care about code”

    This idea reflects a shallow, elementary level of thinking. Yes, obviously users don’t directly care about code. But bad code has negative consequences that eventually become obvious to users.

    Bad code (again, by definition) is slower to work with than good code. When bad code is piled on top of other bad code, the slowdowns become exponential. Changes that should take a day take a week. Changes that should take a week take a month. Users definitely notice and care about this.

    Bad code is also harder to keep bugs out of than good code. Code that’s hard to understand gives bugs safe places to hide. Users are obviously going to notice and care about bugs.

    Refutation: If someone uses “users don’t care about code” on you, you can point out that users don’t care directly about bad code, but users do care about the effects of bad code, like slow delivery and buggy software.

    “Your company might go out of business”

    Multiple times I’ve heard something along the lines of this: “If your company goes out of business, it doesn’t matter if the code was perfect.” This might sound on the surface like a slam-dunk argument against geekishly polishing code rather than maturely considering the larger business realities. But it’s not.

    All that’s needed to destroy this argument is a reminder that good code is called good because it’s faster to work with. That’s why we call it “good”.

    Refutation: Good code is called good because it’s faster to work with. Cutting corners only saves time in the very very short term.

    “There’s no time” or “my manager made me do it” or “they did the best they could with the time they had”

    These aren’t arguments for writing bad code but rather excuses for writing bad code.

    No one is holding a gun to your head and making you write shitty code. You’re the steward of your codebase. It’s your responsibility, and no one else’s, to protect the quality of the codebase so that the codebase can continue to be fast to work with.

    If you consciously choose to take on technical debt, you’ll almost certainly never be granted time to pay back that technical debt. Instead you’ll have to pay interest on that technical debt for the rest of your time with that codebase.

    It’s easy for your boss to tell you to cut corners. Your boss doesn’t have to (directly) live with the consequences of poor coding choices. But eventually when the poor coding choices accumulate and bring development to a crawl, your boss will blame you, not himself.

    Obviously it’s not always easy to fight back against pressure to cut corners. But I think developers could stand to fight back a little more than they do (even if it means being quietly insubordinate and writing good code anyway), and I think developers would benefit greatly from doing so. And so would their bosses and the organizations they work for.

    My argument for writing good code

    My argument for writing good code is very simple: code that’s easy to understand and change is faster to work with. Obviously that’s better.

    I’ll also point out something that might not be obvious. Coding choices are multiplicative. The coding choices you make today have an influence over how easy the code will be to work with tomorrow, the next day, and every day after that. Same with the coding choices you make tomorrow. Each day’s choices multiply against every previous day’s choices.

    The result is exponential. Poor coding choices every day lead to an exponential slowdown in productivity. Good coding choices unfortunately don’t lead to an exponential speedup, but they do at least avoid the exponential slowdown.

    You can think of each day’s code additions as having a score. If you add code that has an “easy-to-change score” of 90%, and you do that three days in a row, then your cumulative score is 0.9^3 = 72.9%. If you add code that has an “easy-to-change score” of 40% three days in a row, then your cumulative score is 0.4^3 = 6.4% (!). This is why programmer productivity doesn’t vary by a factor of just 10X but more like infinityX. Bad code can eventually drive productivity down to something close to 0%.

    Takeaways

    • Our industry has a much bigger sloppy-code problem than gold-plating problem.
    • Good code is code that’s fast to work with.
    • The popular arguments for writing poor-quality code, although they sound mature and reasonable on the surface, are a result of sloppy and confused thinking.
    • Whether you choose to write good or bad code is your responsibility, and you’re the one who will have to live with the consequences of your decisions.

How I approach software estimation

Software estimation is really hard. I’ve never encountered a programmer who’s good at it. I don’t think such a programmer exists. I myself am not good at estimation nor do I expect ever to be.

Here are two tactics that I use to deal with the challenge of estimation.

  1. Try to make estimation as irrelevant as possible
  2. Try to use historical data to make future estimates

Try to make estimation as irrelevant as possible

I figure if I’ll never be able to estimate accurately, at least I can try to make estimates a less important part of the picture. Probably my most successful tactic in this area is to try to only take on projects that last a matter of days. If a project will take more than a matter of days, then I try to break that project up and identify a sub-project that will only take a matter of days. That way, if I estimate that the project will take 2 days and it really takes 4, no one has “lost” very much and nobody’s too upset (especially if I let my stakeholder know that I have very low confidence in my estimate).

Try to use historical data to make future estimates

Obviously all estimates are based to an extent on historical data because you’re basing your estimates on past programming experiences, but there’s a little more to it than that. When I work on a project, I break the work into individual features. I have a sense for how long a feature will take on average. Even though some features take an hour and some features take three days, a feature takes some certain amount of time on average. If I think the average amount of time I take to build a feature is half a day and my I can see that my small project has 5 features, then I can estimate that my project will take 5 * 0.5 = 2.5 days. Obviously there’s a lot of room for inaccuracy in this methodology, hence tactic #1.

If you really want to go deep on estimation, I recommend Software Estimation: Demystifying the Black Art by Steve McConnell.

Don’t mix refactorings with behavior changes

Why it’s bad to mix refactorings with behavior changes

It adds risk

Probably the biggest reason not to mix refactorings with behavior changes is that it makes it too easy to make a mistake.

When you look at the diff between the before and after versions of a piece of code, it’s not always obvious what the implications of that change are going to be. The less obvious the implications are, the more opportunity there is for a bug to slip through.

When you mix behavior changes with refactorings, the behavior change and the refactoring obscure each other, often making the change substantially harder to understand and allowing for a much greater opportunity for bugs to slip through.

Mixing refactorings with behavior changes also requires you to make your deployment deltas (i.e. the amount of change being deployed) bigger. The bigger the delta, the greater the risk.

It makes bug attribution harder

If I deploy a behavior change that was mixed with a refactoring, and then I discover that the deployment introduced a bug, I won’t know whether it was my refactoring or my behavior change that was responsible because the two were mixed together.

And then potentially I’m forced to do something painful in order to remove the bug, which is to roll back both my behavior change and my refactoring, even though only one of those two things was the culprit and the other one was innocent. If I had committed and deployed these changes separately, there’s a higher chance that I would be able to attribute the bug to either the refactoring or the behavior change and not have to roll back both.

It makes code review harder

When you mix refactoring with behavior changes, it’s hard or impossible for a reviewer to tell which is which. It makes a discussion about a code change harder because now the conversation is about two things, not just one thing. This makes for a potentially slow and painful PR review process.

How to approach refactorings instead

When I’m working on a behavior change and I discover that my work would also benefit from some refactoring, here’s what I do:

  1. Set aside my current feature branch
  2. Create a new branch off of master on which to perform my refactoring
  3. Merge my refactoring branch to master (and preferably deploy master to production as well)
  4. Merge or rebase master into my feature branch
  5. Resume work on my feature branch

This allows me to work in a way that reduces risk, allows for easier bug attribution, makes code review easier, and generally saves a lot of time and headache.

10X programmers

You can find discussions online regarding the idea of a “10X programmer”. Much of what you’ll find is ridicule of the idea that 10X programmers exist.

I’ve always thought it’s fairly obvious that 10X programmers exist. In fact, I think programmers vary by a factor of way more than 10X.

Months and years vs. days and hours

When I think of a 10X programmer, I don’t think of something who can finish a job in one hour what would have taken an “average” programmer ten hours. Rather, I think of someone who can accomplish ten times as much in a year than an average programmer. I think it’s a very reasonable proposition that one programmer could accomplish 10X as much in a year than another programmer.

When programmers vary beyond 10X

To me it seems clear that programmers can vary not just by 10X or 100X but by infinityX. This is because some programmers are so good that they can solve problems that weaker programmers would never be able to solve. In this case the better programmer hasn’t produced 10X as much value as the worse programmer, but infinitely more value. (I know that you can’t divide by zero, but humor me.)

My infinite variance claim doesn’t require the weaker programmer to be very bad or even below average. Some programming projects are really hard and require a really good programmer for the project not to fail.

Cumulative work vs. non-cumulative work

It’s not possible to be a 10X worker in every type of work. For example, the best dishwasher in the world is probably not 10X more productive than the average dishwasher. The world’s fastest ditch digger probably can’t dig 10X as much as the average ditch digger. That’s because a dishwasher or ditch digger starts each day with a clean slate. They can move their body parts a little faster but that’s all and then they hit a ceiling.

With programming, each change you make to a codebase influences how easily you’ll be able to make future changes to the codebase. The work you do today is helped or burdened by the choices you made yesterday, the day before that, and so on, all the way back to the first day of the project. Each coding decision in the codebase multiplies with some number of other decisions in the codebase, producing an exponential effect. Good coding choices can’t make your work get much faster, but bad coding choices can make your work slow to a crawl or possibly even a halt. When hundreds or thousands of changes interact with each other multiplicatively, it’s not hard for codebases to vary by a factor of much more than 10X.

How to become a 10X programmer

I think being a 10X programmer is mainly a result of four skills: communication, critical thinking, process, and writing good code.

Being skilled at communication helps reduce the chances of building the wrong thing, which wastes time. It also reduces the chances of experiencing interpersonal problems with colleagues which can slow down work.

Being skilled at critical thinking helps you arrive at right answers and helps you, for example, to avoid spending time barking up the wrong tree when debugging.

Following efficient development processes (small tasks, frequent deployment, automated tests, version control, etc.) helps you avoid wasting time on the overhead of programming.

Finally, writing good code (that is, code that’s easy to understand and change) can help make future changes to the codebase faster.

All four of those areas are very broad and many books have been written on each. It’s obviously not realistic for me to go very deep into those areas here. But if you want to become a 10X programmer, I think those are the areas in which to build your skills.

My take on the Single Responsibility Principle

The Single Responsibility Principle (SRP) is an object-oriented programming principle that says (more or less) that each object should only have one responsibility.

The main difficulty that I’ve seen others have with the SRP, which I’ve also had myself, is: what exactly constitutes a single responsibility?

The answer I’ve arrived at is that it’s a subjective judgment what constitutes a single responsibility. If I write a class that I claim has just one responsibility, someone else could conceivably look at my class and credibly argue that it has eight responsibilities. There’s no way to look at a class and objectively count the number of responsibilities it has.

Here’s how I would characterize the gist of the Single Responsibility Principle: things that are small and that are focused on one idea are easier to understand than things that are big and contain a large number of things. Understanding this principle is much more helpful than understanding exactly what a “single” responsibility is.

A Docker “hello world” app

What we’re going to do

In this tutorial we’re going to illustrate what I consider to be Docker’s central and most magical ability: to let you run software on your computer that you don’t actually have installed on your computer.

The specific software we’re going to run is the Lisp REPL (read-evaluate-print loop). The reason I chose Lisp is because you’re unlikely to happen to have Lisp already installed. If I had chosen to use a language that you might already have installed on your computer, like Ruby or Python, the illustration would lose much of its sizzle.

Here are the steps we’re going to carry out. Don’t worry if you don’t understand each step right now because we’re going to be looking at each step in detail as we go.

1. Add a Dockerfile
2. Build an image from our Dockerfile
3. Run our image, which will create a container
4. Shell into that container
5. Start a Lisp REPL inside the container
6. Run a Lisp “hello world” inside the REPL
7. Exit the container
8. Delete the image

Prerequisites

Before you follow this tutorial you’ll of course have to have Docker installed. You might also like to get familiar with basic Docker concepts and terminology.

Adding a Dockerfile

A Dockerfileis a specification for building a Docker image. We’re going to write a Dockerfile, use the Dockerfileto build a custom image, create a container using the image, and finally shell into the container and start the Lisp REPL.

First I’m going to show you the Dockerfileand then I’ll explain each individual part of it.

# Dockerfile

FROM ubuntu:20.04

RUN apt update && apt install -y sbcl

WORKDIR /usr/src

FROM

FROM ubuntu:20.04

The FROMdirective tells Docker what image we want to use as our base image. A base image, as the name implies, is the image that we want to use as a starting point for our custom image. In this case our base image is just providing us with an operating system to work with.

The FROMdirective takes the form of <image>:<tag>. In this case our image is ubuntuand our tag is 20.04. When Docker sees ubuntu:20.04, it will look on Docker Hub for an image called ubuntuthat’s tagged with 20.04.

RUN

RUN apt update && apt install -y sbcl

The RUNcommand in a Dockerfilesimply takes whatever it’s given and runs it on the command line.

In this case the command we’re running is apt update && apt install -y sbcl. The &&in between the two commands means “execute the first command, then execute the second command if and only if the first command was successful”. Let’s deal with each of these commands individually.

The apt updatecommand is a command that downloads package information. If we were to skip the apt updatecommand, we would get an error that says Unable to locate package sbclwhen we try to install sbcl. So in other words, running apt updatemakes our package manager aware of what packages are available to be installed.

The apt install -y sbclcommand installs a package called sbcl. SBCL stands for Steel Bank Common Lisp which is a Common Lisp compiler. (Common Lisp itself is a popular dialect of the Lisp language.)

The -ypart of apt install -y sbclmeans “don’t give me a yes/no prompt”. If we were to leave off the -ywe’d get an “are you sure?” prompt which would be no good because the Dockerfileisn’t executed in an interactive way that would actually allow us to respond to the prompt.

WORKDIR

WORKDIR /usr/src

The WORKDIR /usr/srcdirective specifies which directory to use as the working directory inside the container. Imagine being logged into a Linux machine and running cd /usr/src. After running that command, you’re “in” /usr/srcand /usr/srcis your working directory. Similar idea here.

Listing our existing images

Before we use our Dockerfileto build our image, let’s list our existing Docker images. If this is your first time doing anything with Docker then the list of existing images will of course be empty.

In any case, let’s run the docker image lscommand:

$ docker image ls

Listing our existing containers

In addition to the docker image lscommand which lets us list any images we have, there’s an analogous command that lets us list our containers.

$ docker container ls

Building our image

We can use the docker buildcommand to build our image. The --tag lisppart says “give the resulting image a tag of lisp“. The .part says “when you look for the Dockerfileto build the image with, look in the current directory”.

$ docker build --tag lisp .

After you run this command you’ll see some entertaining output fly across the screen while Docker is building your image for you.

Confirming that our image was created

Assuming that the build was successful, we can now use the docker image lscommand once again to list all of our existing images, which should now include the image we just built.

$ docker image ls

You should see something like the following:

REPOSITORY   TAG      IMAGE ID       CREATED          SIZE  
lisp         latest   91f4fa2a754a   11 minutes ago   140MB

Creating and shelling into a container

Run the following command, which will place you on the command line inside a container based on your image. You should of course replace <image id>with the image id that you see when you run docker image ls.

$ docker run --interactive --tty <image id> /bin/bash

The docker runcommand is what creates a container from the image. The /bin/bashargument says “the thing you should run on this container is the /bin/bashprogram”.

Viewing our new container

Now that we’ve invoked the docker runcommand, we have a new container. Open a separate terminal window/tab and run docker container ls.

$ docker container ls

You should see a new container there with an image ID that matches the ID of the image you saw when you ran docker image ls.

CONTAINER ID   IMAGE          COMMAND       CREATED         STATUS
5cee4af0cfa9   91f4fa2a754a   "/bin/bash"   4 seconds ago   Up 3 seconds

Poking around in our container

Just for fun, run a few commands in the container and see what’s what.

$ pwd    # show the current directory
$ ls -la # show the contents of the current directory
$ whoami # show the current user

Running the Lisp REPL

Finally, let’s run the Lisp REPL by running the sbclcommand inside our container.

$ sbcl

Once you’re inside the REPL, run this piece of “hello, world!” Lisp code.

(format t "hello, world!")

Exiting the container

Press CTRL+D to exit the Lisp REPL and then CTRL+D again to exit the container. The container will delete itself once exited.

Deleting the image

Run the following command to delete the image.
The rmipart stands for “remove image” and the -fflag stands for “force”.

$ docker rmi -f <image id>

Recap

You’ve completed this exercise. You’re now capable of the following things:

  • Writing a Dockerfilethat specifies an operating system to use and some software to install
  • Listing Docker images and containers
  • Building Docker images
  • Shelling into a container
  • Using the software that was installed on the container

If you possess these capabilities and have at least a little bit of a grasp of the underlying concepts, then you’re well on your way to being able to use Docker to accomplish real work.

Docker concepts and terminology

Host machine

The host machine is the machine that Docker is running on. When Dockerizing for development, the host machine is typically the laptop that you’re working on. In a production environment, the host machine could be (for example) an AWS EC2 instance.

Images and containers

Images and containers are sufficiently interrelated that it makes sense to explain them together.

A Docker image is like a blueprint. A container is like the house that you build with that blueprint. You can build any number of houses off of the same blueprint. If you want to, you can even build a house today, demolish it tomorrow, change the blueprint, and build a new version of the house the next day.

Let’s take that analogy and make it concrete. A Docker image (which again is the blueprint) might specify something like “use the Ubuntu operating system, install PostgreSQL, and get PostreSQL running.”Using that image, you can then create a container which, according to the image, will run on Ubuntu, have PostgreSQL installed and have PostgreSQL running.

A container is like a computer inside your computer. If you have a container running, you can do things like shell into it or use a web browser to visit a website hosted on the container, for example.

Dockerfile

A Dockerfile is a specification for building an image.

For example, here’s a Dockerfile that says “Use Ubuntu as the operating system, and install PHP”.

FROM ubuntu:20.04

RUN apt update && apt install -y php

WORKDIR /usr/src

Volume

One of the defining characteristics of a Docker container is that it’s ephemeral. Containers can be started, stopped, created and deleted, and they often are. This creates a problem if you want to save any files or make permanent changes to existing files. Once your container gets deleted, anything you’ve done on the container’s filesystem is gone.

The solution to this problem is volumes. The idea is that instead of storing things on the container’s filesystem, you store things somewhere on the host machine’s filesystem.

To use volumes, you can tell Docker that a certain service should store its files in a certain location on the host machine. For example, you can say, “Hey Docker, whenever you need to store PostgreSQL data, store it in /var/lib/postgresql/data on the host machine”.

This way your data can survive containers being stopped or deleted.

Docker Compose

Docker Compose is a tool for composing an environment from one or more services.

For example, if my development environment needs PostgreSQL, Redis and Elasticsearch, I could tell Docker via a Docker Compose config file that it should install all three of these services and get them running for me. I can also tell Docker that my environment includes e.g. a custom Rails application.

In addition to specifying that all these services exist, I can use a Docker Compose file to specify how these services are supposed to be configured and how they are to talk to one another.

Docker Hub

Docker Hub is a place where Docker images are stored. You can think of it analogously to open source libraries being hosted on GitHub.

If I want to use Redis, for example, I can tell Docker to grab me the redis:4.0.14-alpine image and the default place that Docker will look for this image is on Docker Hub.

What to do about bloated Rails Active Record models

Overview

It’s a common problem in Rails apps for Active Record models to get bloated as an application grows. This was a problem that I personally struggled with for a number of years. As I’ve gained experience, I’ve figured out a couple tactics for addressing this problem, tactics that I feel have worked well. I’ll share my these in this post.

First let’s talk about how and why this problem arises.

How bloated Active Record models arise

Some background on the Active Record pattern

Many Rails developers might not know that the Active Record pattern existed before Rails. The pattern was named by Martin Fowler in Patterns of Enterprise Application Architecture. The idea behind Active Record (in broad strokes) is that a database table has a corresponding class. For example, a patients table would have a Patient class. An instance of the class represents one row of data for the table. The class is endowed with capabilities like saving an object to the database as a new row, updating an existing row, finding records, and so on.

Rails’ specific implementation of the Active Record pattern is a big part of what gives Rails developers the ability to create so much functionality with such little code. Unfortunately, the Active Record pattern is also somewhat ripe for abuse to a programmer who doesn’t have much experience writing structured code outside of frameworks.

An easy Active Record trap to fall into

There’s a common problem in Rails projects which can perhaps be summarized like this: Rails developers want to keep controllers from getting bloated (rightly), so they push as much domain logic as possible down to the Active Record models.

But because the Active Record models constitute a limited number of “buckets”, the domain logic code accumulates in the Active Record models. If behavior is added to an application ten times faster than database tables are added, then all the behavior will pile up in those ten Active Record classes. The bloat problem isn’t solved, it’s just moved from the controller layer to the model layer.

To be clear, this isn’t a weakness of the Active Record pattern. The root cause of the problem is inexperience. Many Rails developers aren’t aware of other code organization strategies outside of Active Record.

Here are two ways I combat this “bloated Active Record model” problem.

Tactic #1: objects

When I need to add a piece of behavior to an application, I often consider adding it straight to the Active Record model, especially early in the lifecycle of the application. But I tend to also consider adding the behavior as a new object.

Below is an example of when I found a piece of behavior to make sense as a separate object rather than as part of an Active Record model. In the application I work on at work, which is a medical application, we periodically download a file containing all the insurance charges we’ve accumulated since the last time we downloaded charges. At one point we had a new need, a way to see a list of all the past batches of charges that had been downloaded. And for each item, we wanted to see the total dollar amount for that batch as well as the unique insurance claim count for that batch.

I of course could have put the code for this feature in the Charge Active Record object. But this new feature was so peripheral that I didn’t want to clutter up Charge with the code for it. So I conceived of a new object called DownloadedChargeCollection which has methods total_balance and unique_claim_count. Here’s what that object looks like.

module Billing
  class DownloadedChargeCollection
    attr_reader :file_download

    def initialize(file_download)
      @file_download = file_download
    end

    def unique_claim_count
      charges.map(&:appointment).uniq.count
    end

    def total_balance
      Money.new(balances.sum)
    end

    def self.from(file_downloads)
      file_downloads.map { |fd| new(fd) }
    end

    private

    def charges
      Charge.where(file_download: @file_download).includes(
        :insurance_payments,
        :appointment
      )
    end

    def balances
      charges.map(&:balance_cents).compact
    end
  end
end

The way this object is used is that, in the view, we do collection.unique_claim_count and collection.total_balance. This makes the view code very easily understandable. Then if we want to dig into the details DownloadedChargeCollection, that code is pretty understandable as well because the object is pretty small.

Designing good objects

Good object design is a subjective art. As for me, I like to design objects that are crisp abstractions that represent a “thing”. That’s why the above object is called DownloadedChargeCollection as opposed to DownloadedChargeManager or DownloadAllChargesService or something like that. I think class names that end in “-er” are a code smell because it often means that the object represents a fuzzy or confused idea.

I also like to write objects’ code in a declarative style as opposed to an imperative style. Notice how the method names are unique_claim_count and total_balance (nouns) rather than e.g. get_unique_claim_count and calculate_total_balance (commands). I prefer naming methods for what they return rather than for what they do. This is also why I prefer to name my objects as nouns rather than commands.

Coming up with abstractions can feel hard and unnatural if you don’t have much experience with it. One of the biggest abstraction breakthroughs for me was when I realized that an object didn’t have to represent something that already had a name, it could be a new invention. For an example of an object like this, Rails defines an object called ActionController::UnpermittedParameters. That’s not necessarily all that natural of an idea. The idea only exists because someone decided it exists. Similar story with my DownloadedChargeCollection. It’s a totally made-up concept that only exists because I decided it exists, but that doesn’t make it any less valid or useful of an abstraction.

Tactic #2: mixins/concerns

Sometimes I need to add a piece of behavior which is tightly coupled to the attributes of an Active Record model (or other unique capabilities of an Active Record model like scopes) but is highly peripheral to the model. It’s not part of the “essence” of the model.

In those cases a separate object wouldn’t necessarily make a lot of sense due to the tight coupling with the Active Record object. My new object would be chock full of “feature envy” and therefore probably be hard to understand. But I also don’t want to give up and just put the new behavior straight in the Active Record model because that would hurt the understandability of the Active Record model. In cases like this I tend to reach for a mixin or concern.

Below is an example of this. In this case I needed to endow an Appointment model with two scopes and an enum. Putting this code in a concern has two benefits: 1) it keeps the code out of the Appointment model so that I don’t feel like I have to understand this queueing-related code in order to understand Appointment in general and 2) it shows me that my enum and two scopes are all related to one another, something that would have been less clear (probably not clear at all) if I had put the code straight into the Appointment model.

module InReviewAppointmentQueue
  extend ActiveSupport::Concern

  included do
    enum subqueue: %i(clean holding)

    scope :missing_insurance, -> do
      joins(:patient).merge(
        Patient.left_joins(:insurance_accounts).where("insurance_accounts.id is null")
      )
    end

    scope :charge_queue_new, -> do
      where(subqueue: nil) - missing_insurance
    end
  end
end

Reusable concerns vs. one-off concerns

It seems to me that most of the concerns I see other people write have been written for the purpose of extracting common behavior with a goal of DRYing up code. There might be a concern called Archivable, for example, that adds archiving behavior to any class that includes it.

To me, DRYing up code is just one use case for concerns, and not even the main use case. Most of my concerns are only useful to one class. My goal with these concerns isn’t to DRY up my code but rather to hide details and to group related pieces of code together.

Concerns vs. mixins

Sometimes, when I extract peripheral behavior out of a model, I put that behavior into a concern. But at one point I realized that I concern is nothing but a Ruby mixin with a little bit of DSL syntax to make certain things easier, and that sometimes I have to need at all for this DSL syntax. If there’s no need for it, I don’t see why I should use it. So lately I’ve been favoring plain old Ruby mixins over concerns.

I’ve also grown a distaste for the idea of an app/models/concerns directory. I use a lot of namespaces in my application, sometimes nested two deep. If I have a concern that relates to something in the Billing::Eligibility namespace, for example, I’d rather put that concern in app/models/billing/eligibility/my_concern.rb than app/models/concerns/billing/eligibility/my_concern.rb. The latter choice would require me to mirror my whole directory structure inside app/models/concerns and also make it less obvious which model files are related to which concerns. (And again, I also often choose to use a regular old Ruby mixin rather than a concern.)

Criticisms of concerns

You can find a lot of criticisms of concerns online: that concerns are inheritance, and composition is better than inheritance; that concerns don’t necessarily remove dependencies, they just spread them across multiple files; that concerns create circular dependencies; and that when code is spread across multiple files, it can be unclear where methods are defined.

I address all of these criticisms in a separate post called “When used intelligently, Rails concerns are great“. The TL;DR is that concerns can be used either well or poorly, just like any other tool.

A word about service objects

The idea of “service objects” (which means different things depending on who you ask) seems to have grown in popularity in the Rails community in recent years. The most commonly-accepted definition of “service object” seems to be something roughly equivalent to the command pattern.

When service objects are bad

I think the command pattern can have its place. I’ve made use of some small and simple command pattern objects myself. The trouble with service objects (which again is usually the command pattern by a different name, as far as I can tell) is when inexperienced developers reach for service objects reflexively, as a perceived panacea, out of ignorance of the other options available (like regular old OOP abstractions). Not everything is best expressed as a command.

Another problem with service objects, as I’ve already mentioned, is that the term “service object” means different things to different people. When a concept is vague and has multiple meanings, I’d call that a “concept smell”.

When service objects (or rather, the command pattern) can be good

In general I prefer a declarative coding style over an imperative style. But a certain amount of imperative coding is necessary at some point because at some point your declarative code has to get used to actually do something. So maybe you can write 95% of your code in a declarative style but not 100%. The “tip of the pyramid” has to be imperative.

I think service objects—or I think more accurately, the command pattern—can be a decent way to package up the proportion of code in an application that needs to be expressed imperatively. It’s certainly better than stuffing it all in a controller.

Here’s an example of an imperative object I wrote called RemittanceFileParser. The style is imperative but the object is very simple. Most of the “real” work is pushed down to ElectronicRemittanceAdviceFile, which is written in a declarative style. (ElectronicRemittanceAdviceFile in turn delegates its logic to finer-grained declarative objects.)

class RemittanceFileParser
  attr_reader :results

  def initialize(content: nil, insurance_deposit: nil)
    @content = content
    @results = NiceEDI::ElectronicRemittanceAdviceFile.new(@content).parse
    @insurance_deposit = insurance_deposit
  end

  def perform
    ActiveRecord::Base.transaction do
      @results[:claim_payment_items].each do |claim_payment_item|
        claim_payment_item[:services].each do |service|
          save_insurance_payment!(
            service: service,
            claim_payment_item: claim_payment_item,
            remittance_amount: @results[:remittance_amount]
          )
        end
      end
    end
  end

  def string_to_cents(value)
    (value.to_r * 100).to_i
  end

  def save_insurance_payment!(service:, claim_payment_item:, remittance_amount:)
    @insurance_deposit.insurance_payments.create!(
      service_amount_cents: string_to_cents(service[:service_amount]),
      date_of_service: service[:date_of_service],
      cpt_code_freeform: service[:cpt_code],
      npi_code: claim_payment_item[:npi_code],
      patient_control_number: claim_payment_item[:patient_control_number],
      patient_first_name: claim_payment_item[:patient_first_name],
      patient_last_name: claim_payment_item[:patient_last_name],
      ma18_code_present: claim_payment_item[:ma18_code_present],
      medicare_secondary_payer_name: claim_payment_item[:medicare_secondary_payer_name],
      remittance_amount_cents: string_to_cents(remittance_amount)
    )
  end
end

So I think the takeaway here is: it’s fine and even necessary to use imperative code, but putting all your model code into imperative-style objects as a way of life is probably a mistake. I find it better, to the extent possible, to express my domain concepts as small, declarative objects.

The liberating realization that “you’re on your own”

Coding without frameworks

Imagine that you want to write a command-line Ruby program (no framework) that simulates, say, the Wheel of Fortune game show. As you add more and more code, you realize that Wheel of Fortune is actually pretty complicated, and it takes a lot of code to replicate it.

Because there’s a lot of logic, you wouldn’t be able to just put all your code in one big procedural file. You’d create a confusing mess for yourself pretty quickly if you did. You’d have to impose some structure somehow. And because you’re not using a framework, you’d have to come up with that structure yourself.

How would you structure this code? For me, I’d use the principles of object-oriented programming. I would compose my program of small, crisply-defined, declarative objects. If the program got big enough, I’d create some namespaces to make it easier to see what’s related to what. I might make use of mixins as well. But more than anything else, I’d use objects and OOP principles.

Tiny Rails apps

Now imagine a tiny Rails app that just has a few CRUD interfaces. Unlike our Wheel of Fortune Ruby program which is 0% framework code, this Rails app would be almost 100% framework code. You wouldn’t need to make a single design decision. You’d just need to run rails g scaffold a couple times and maybe add a couple lines for associations.

Large Rails apps

Lastly, imagine a huge Rails app with a lot of complicated domain logic. With an app like this, Rails can only help you so much. Rails can help abstract away common jobs like handling HTTP requests and talking to the database but the framework can’t help with the singularly unique domain logic of your application. No framework ever could.

In order to keep your domain logic organized and sufficiently easy to understand, you of course need not just tools but skills. You’re past the point where Rails can help you structure your code and so you need to impose the structure yourself.

Learning design skills (like the principles of OOP for example) is of course not easy. You’re never done learning. But hopefully the realization that design skills, not Rails, is the key to building maintainable Rails apps, is a useful one. It was for me.

Takeaways

  • The “bloated Active Record model” problem often arises when programmers follow the “skinny controllers, fat models” principle and allow all the domain logic to accumulate in Active Record models.
  • A “model” doesn’t have to mean an Active Record model, but can be any piece of code that models a concept.
  • A couple good ways to organize model code are to use objects and mixins.
  • Using service objects isn’t always necessarily a bad idea, but reflexively using service objects out of ignorance of other code organization options probably is.
  • Once a Rails application grows beyond a certain size, you can no longer rely on Rails itself to help keep your design sound but must rely on your own design skills.