29 Aug 2016 · Software Engineering

    Isolate Side Effects in Ruby

    15 min read
    Contents

    Post originally published on http://www.rubypigeon.com. Republished with author’s permission.

    What Are Side Effects?

    Side effects are any observable change caused by calling a function. For a function to be free of side effects, it must do nothing other than provide a return value.

    These are examples of side effects:

    • Writing to a file,
    • Printing output (e.g. puts('hello')),
    • Raising an exception, and
    • Mutating a non-local variable.

    Side Effects Cause Bugs

    Have you ever been hunting down a bug, and narrowed it down to a single statement — where you can comment out just one line of code, and the bug magically disappears? That is the sting of an unexpected side effect. We’ve all been there.

    If you’re not careful, side effects can proliferate as the codebase grows, making them difficult to anticipate. When we call functions without fully understanding all of their side effects, that’s when the bugs crawl out.

    As an example, let’s look at some typical Rails controller code:

    class UsersController < ApplicationController
      def update
        @user = User.find(params[:id])
        if @user.update(user_params)
          redirect :back, success: 'Updated successfully'
        else
          render 'edit'
        end
      end
    
      def user_params
        params.require(:user).permit(
          :username,
          :password,
          :password_confirmation,
          :profile_image,
        )
      end
    end
    

    What are the side effects of the user_params method? There are none, as far as I can tell. It takes the params hash, transforms it, and returns the result. It does nothing but provide a return value, so it’s side-effect-free.

    What are the side effects of the @user.update method call? Well, it updates the database at some point, but it could be doing all kinds of other things.

    • The @user object itself is getting its attributes mutated, even if validation fails. What if the view requires the previous valid values? Do I need to reload the current_user object to get the new values?
    • Speaking of validation, that’s going to mutate the errors object on the model. That has to happen before the view gets rendered.
    • Surely that password isn’t being saved as plain text. Something must be going on under the hood to hash it.
    • It’s probably logging something. Hopefully the password isn’t being written out in plaintext. Better check that.
    • What if it’s sending an email in the after_save callback? If so, is it supposed to send an email in this particular situation, or do I have to suppress it?
    • What about that profile_image attribute. That looks like a file. Could it possibly overwrite an existing file? Is it sitting in a temporary directory that needs to be cleaned out periodically? If it’s stored on the disk, what happens if there isn’t enough disk space available? If it’s being uploaded to S3 instead, should I be expecting some kind of exception to be raised if the upload fails?

    These are all implicit side effects. It’s difficult to predict what will happen by looking at the calling code, which is fine if you understand the entire codebase, but dangerous if you don’t.

    The point I’m trying to make is that side effects can be unpleasant surprises. They’re just thorny by nature.

    Side Effects Are Hard To Test

    Side effects are often slow IO operations, like writing to a disk or a socket. If you want to test these things, don’t expect your test suite to be snappy. Take a PostgreSQL database in a typical Rails application, for example. When database access is sprinkled through basically every single part of an application, almost every test is going to hit the database at some point, so it should be no surprise when the test suite is slow. On the other hand, if the majority of the tests are isolated, but a few of them hit the database, you would expect the test suite to be much faster.

    To speed up the test suite, you might start using stubs and mocks to avoid actually hitting the database. But if the side effects are still sprinkled everywhere through the codebase, you’re going to be doing a lot of stubbing and mocking — so much that it can turn your tests into spaghetti.

    In FP terminology, a “pure function” is a function that generates a return value based on its arguments, and does nothing else. Pure functions can not have side effects, and must always return the same value given the same arguments. This means that pure functions depend upon nothing but their arguments and constants.

    Testing pure* functions is easy. You put some arguments in, you get a return value out, then you run some assertions on the return value. That’s it. No mocking or stubbing, and it’s usually super fast. Plus it’s easy to write my favourite kind of tests: truth tables. It’s the ideal kind of unit test.

    Side Effects Are Necessary

    If an application has no side effects, it doesn’t actually do anything. A typical web application has plenty of side effects, like updating/inserting into the database, logging, pushing background jobs onto queues, and interacting with external web APIs. These things just can’t be removed. Even sending a HTTP response back to the browser involves writing to a socket, which is a kind of side effect.

    So how do you handle code that is necessary, but problematic? Isolate it. Extract the side effects into separate units, so that the remaining code can be written in a functional style.

    If this sounds familiar, you may be remembering Gary Bernhardt’s excellent talk: Boundaries. In the talk he explains what he calls “functional core, imperative shell.” The functional core is the domain logic, written in a functional style. The imperative shell uses the results of the functional core to cause the necessary side effects. I highly recommend watching this talk if you haven’t seen it before, and even if you have, it’s worth rewatching.

    Avoid Mutation

    The previous article was about avoiding mutation, but I want to touch on how mutation causes side effects.

    Think about how the following method could cause bugs.

    def case_insensitive_equals(str1, str2)
      # I've heard that upcase! is faster than upcase,
      # so this method should be ultra fast.
      return str1.upcase! == str2.upcase!
    end
    

    If you need a hint, consider this usage code:

    BOSS_NAME = 'Joanna Smith'
    
    if case_insensitive_equals(current_user.name, BOSS_NAME)
      current_user.is_the_boss = true
      current_user.save!
    end
    

    Firstly, the BOSS_NAME constant gets changed, and that’s the opposite of what constants are supposed to do. Secondly, it converts the boss’ name to uppercase and saves it to the database. Now the web application is going to be shouting at the boss.

    This is all caused by the nasty mutation side effect of the case_insensitive_equals method. Methods that are called for their return value usually don’t mutate their arguments. Because the side effect is unexpected, it can easily cause bugs, and those bugs are particularly hard to find.

    The example above involved method arguments, but mutating any kind of shared state is a side effect. This includes globals, instance variables, and even updating a database.

    Isolated Mutation is OK

    Mutation isn’t that bad, per se. It’s the side effects caused by mutation that get you. But what if you could use mutation without any side effects?

    Consider this example, taken from the previous article:

    def symbolize_keys(hash)
      result = {}
      hash.each do |key, value|
        result[key.to_sym] = value
      end
      result
    end
    

    This method repeatedly mutates the hash inside the result variable. However, the mutations are not observable from the outside, because they all happen within local scope. The method does nothing other than provide a return value, so it is side-effect-free.

    This kind of side-effect-free mutation within local scope is the least harmful kind of mutation. Any bugs that get introduced will be limited to the method itself, and are unlikely to break other parts of the codebase. I still recommend avoiding mutation by default, but you can relax that rule when the method has no side effects.

    If you really want to mutate an argument, or an instance variable, or any piece of shared state, be sure to make a duplicate. The duplicate is isolated to local scope, so mutating it won’t cause side effects.

    Use Imperative Style For Side Effects

    When it comes to the parts of your application that are specifically designed to cause side effects, you basically have to give up on writing functional-style code. It’s just the wrong tool for the job. Instead, switch to imperative style.

    Imperative style code is just a sequence of statements, where each statement has some kind of side effect. Here is an abbreviated example from a real web application:

    module Commands
      class StopRecording
    
        def call
          if ffmpeg_pids.any?
            kill_ffmpeg_processes!
            SessionRepo.update(session_id, live: false)
            AngleRepo.update({session_id: session_id}, ffmpeg_pid: nil)
            wait_for_processes_to_die!
            Result.success
          else
            Result.failure("Recording was already stopped")
          end
        end
    
        # ... code omitted ...
    
      end
    end
    

    The success branch is just four statements and a return value. You can tell that they have side effects because the return values are ignored. The bangs (!) on the method names are also a hint. These are explicit side effects. Each line has one fairly obvious, predictable effect.

    Notice the return value. I could get rid of the Result class and just raise an exception to indicate failure, but I purposely chose to avoid exceptions. Exceptions are a kind of side effect that is forced upon the caller. I’m trying to isolate the side effects, not propagate them into the calling code.

    This kind of code will require tests with mocking and stubbing. But hopefully it will require fewer tests, because some of the logic has been extracted out of it.

    Example Refactoring To Isolation

    Let’s refactor some hypothetical code for sending monthly bills.

    class MonthlyBillingJob
      SENIOR_CITIZEN_DISCOUNT = 5
    
      def perform
        Account.where(free: false) do |account|
          bill = Bill.new(
            account: account,
            amount: account.plan.amount,
          )
    
          if account.type == :senior_citizen
            bill.amount -= SENIOR_CITIZEN_DISCOUNT
          end
    
          bill.save!
    
          BillMailer.new_bill(bill).deliver_now
        end
      end
    end
    

    Before we change anything, think about writing tests for the class above. How hard will it be to test the senior citizen discount? How hard will it be to test that bills are inserted into the database, and emails are sent? We’ll revisit this after refactoring.

    There are two side effects to isolate: creating the bill in the database, and sending the email. The first thing I want to think about is making those side effects explicit, and minimal. Here is the imperative-style implementation that I would like to see:

    billable_accounts.each do |account|
      bill = create_bill!(account)
      send_email!(bill)
    end
    

    The next thing I want to think about is extracting the domain logic. The senior citizen discount is definitely domain logic, and so is the query for billable accounts. I’m going to pull both of those out into a functional-style module.

    While querying the database is not functionally pure, I’m going to include it in the functional-style code. Queries are generally free from observable side effects, with the exception of a few edge cases like the N+1 problem.

    module Billing
      SENIOR_CITIZEN_DISCOUNT = 5
    
      def self.billable_accounts
        Account.where(free: false)
      end
    
      def self.monthly_bill(account)
        Bill.new(
          account: account,
          amount: account.plan.amount - discounts(account),
        )
      end
    
      def self.discounts(account)
        if account.type == :senior_citizen
          SENIOR_CITIZEN_DISCOUNT
        else
          0
        end
      end
    end
    

    Notice that the monthly_bill method creates an ActiveRecord object. Just creating the object is fine, because it has no side effects. Actually inserting the object into the database is a side effect, and I wouldn’t want that in this functional-style code.

    After incorporating the above functional-style code, the job class looks like this:

    class MonthlyBillingJob
      def perform
        Billing.billable_accounts.each do |account|
          bill = Billing.monthly_bill(account)
          bill.save!
          BillMailer.new_bill(bill).deliver_now
        end
      end
    end
    

    It’s not the exact implementation I was aiming for, but it’s close. I could make it exact by introducing a few extra methods, but I think it’s already clear enough. While I would slightly prefer send_email!(bill) to BillMailer.new_bill(bill).deliver_now, there really isn’t much of a difference.

    Now let’s analyze the results a bit.

    How hard is it to test that bills are created, and emails are sent? Since it has been decoupled from the domain logic, the MonthlyBillingJob class only needs a single test. The test needs mocks, but it’s not too bad:

    job = MonthlyBillingJob.new
    bill = double
    mailer = double
    
    expect(Billing).to receive(:billable_accounts).and_return([:sams_acc])
    expect(Billing).to receive(:monthly_bill).with(:sams_acc).and_return(bill)
    expect(bill).to receive(:save!)
    expect(BillMailer).to receive(:new_bill).with(bill).and_return(mailer)
    expect(mailer).to receive(:deliver_now)
    
    job.perform
    

    Notice how the test never actually needs to use a real Account or Bill object. This is a good thing, because it makes the MonthlyBillingJob class more resistant to change. Removing all database access should also ensure that the test is fast.

    The job class is dumb — it doesn’t know or care about most of the domain logic. We can change how the accounts are queried, or how the bills are created, and the job class will still work without modification.

    Moving on to the functional-style code, how hard is it to test the seniors discount? It’s dead easy:

    account = Account.new(type: :senior_citizen)
    discount = Billing.discount(account)
    expect(discount).to eq(5)
    

    And it will continue to be dead easy every time you need to change the domain logic for discounts.

    All the methods on the Billing module could have been kept on the MonthlyBillingJob class. They could still be written in a functional style, and be stubbed out in the same way in the test. However, even with the side effects isolated into separate methods, I don’t like the two styles of code being right next to each other in the same class. I’d rather have a separate file that I could point to and say “there’s the billing logic.” I think that’s better than having it sprinkled all over the codebase. I prefer clear boundaries between the functional and non-functional code.

    Applying It To Rails: Prefer Skinny Models

    Globals suck, right? That bit of wisdom has been drilled into programmers for a long time now.

    They suck because they are a prime example of shared mutable state. They allow bits of your application to communicate with each other via mutation side effects, causing otherwise separate units of code to be coupled together.

    Guess what else is shared mutable state: the database in a Rails application. The model classes in a Rails application are basically global variables, and they suffer from the same problems. They can be, and often are, used from everywhere in the codebase. That creates a huge surface area for introducing bugs.

    With that in mind, do you really want to take the “fat model, skinny controller” approach and implement your application’s functionality in the model layer? I don’t. I would rather use ActiveRecord for interacting with the database, and implement the domain logic separately, preferably with functional-style code.

    Keep in mind that MVC isn’t a set of three big buckets that all code has to fall into. Just because functionality is being removed from the models doesn’t mean it goes into the controllers. The side-effecty code can be moved into a new layer, usually named something like service objects, operations or commands. The functional-style code can live in the lib/ directory, or maybe somewhere new like app/domain/.

    Enlightenment comes when you use objects in a server-side web application to model actions, not things.

    Brad Urani

    And if you value your sanity, stay away from ActiveRecord::Callbacks. They are specifically designed to cause implicit side effects, like automatically sending an email after you save a model object. That’s the diametric opposite of what I’m trying to argue for in this article.

    Summary

    The aim is to minimise the naughty code and maximise the nice code, like some kind of weird software developer Santa Claus. To achieve this, code is split into two parts: the functional part and the imperative part.

    The functional part is simpler, easier to test, and just easier to work with in general. Ideally, this part contains the domain logic of your application — the real-world rules and decisions that your application handles for its users.

    The imperative part is still necessary and important, but it’s more prone to bugs, and harder to test. This part ideally contains minimal domain logic. It should blindly carry out orders like “send this email” or “update this row in the database.” The less it does, the better.

    The functional-style code is the brain, and the imperative code is the brawn. Keeping the two separate is worth the effort, in my opinion.

    P.S. Would you like to learn how to build sustainable Rails apps and ship more often? We’ve recently published an ebook covering just that — “Rails Testing Handbook”. Learn more and download a free copy.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Tom is a full-stack web and iOS developer. He's currently writing articles for Ruby developers on RubyPigeon.