Building The Conversation

Failing fast with DBC- and lovin’ it

Sprinkled throughout our code are some little helpers that check the app is working as expected. These little helpers implement the Design By Contract (DBC) style of fail often, fail fast.

The DBC helpers act similar to the assert macro in the C programming language, where the failing of a DBC check represents a bug, and the exception raised would not normally be caught by the app (i.e. the app would stop running).

An example of a helper is:

    DBC.require(check, msg = "")


A classic example of a DBC.require check would be in a square_root function.

    def Math.square_root(x)
      DBC.require(x >= 0, "Cannot get the square root a number less than zero")

      # Code to calculate square root
      # ...
    end


This particular helper (require) does a sanity check on the parameters passed into the method in question. Like most DBC methods, the require method throws an exception if the check does not evaluate to true. Some examples in our code are:

   DBC.require(content.state != :published, "Cannot delete published content")

   DBC.require(record.valid?, record.errors.inspect)


Why use DBC instead of raise?

Design By Contract has some nice advantages over using raise:

  • You get a nice little DSL that formalizes the raising of exceptions within an app (see The DBC methods below)
  • The DSL provides a wrapper that does whatever extra error-checking, tracing, etc you wants to do. For instance, our DBC prints out a stack trace, where as if we used raise we would be at the mercy of particular shell, irb, passengers etc as to whether a stack trace is shown.
  • DBC throws its own specific exceptions, so you can treat them separate to other exceptions.

On top of that, DBC helps highlight the different parts of what you can be checking at the method level.

The DBC methods

We use 4 different versions of DBC helpers throughout the code, which are:

    DBC.require(check, msg = "")
    DBC.assert(check, msg = "") 
    DBC.ensure(check, msg = "") 

    DBC.fail(msg = "")


DBC.require

Written at the top of methods, use it to check:

  • Was the method called with the right state?
  • Was the method called with the right parameters?

DBC.assert

Written half-way through a method, use it to check:

  • Is the method currently doing the right thing?

Note: One place I consistently use DBC.assert is within Rails migrations that use native ruby code.

DBC.ensure

Written at the end of methods, use it to check:

  • Is the method returning the right values?
  • Has the method left the app in the right state?

Note: DBC.ensure is not often used as the cost of doing a check can often be the cost of re-doing the method.

DBC.fail

Insert this where you know the app must now be in an invalid state. For example, it can be useful in the else part of a ruby case statement, when an unexpected value has come through.

Credits

The Design By Contract pattern was first shown to me by Dan Prager. Thanks Dan!

The code

    # Design By Contract pattern
    class DBC 
      class PreconditionException < RuntimeError; end
      class AssertconditionException < RuntimeError; end
      class PostconditionException < RuntimeError; end
      class FailException < RuntimeError; end

      def self.require(condition, message = "")
        unless condition
          error(PreconditionException, message, caller)
        end
      end

      def self.assert(condition, message = "")
        unless condition
          error(AssertconditionException, message, caller)
        end
      end

      def self.ensure(condition, message = "")
        unless condition
          error(PostconditionException, message, caller)
        end
      end

      def self.fail(message = "")
        error(FailException, message, caller)
      end

      private 

      def self.error(klass, message, caller)
        raise klass.new("#{klass.name}-condition failed: #{message}\nTrace was: #{caller.join("\n")}")
      end
    end

Help combat alt-facts and fake news and donate to independent journalism. Tax deductible.