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