Sep 01 2015

Static guarantees.

As developers we're tasked to take a problem specified in the fuzzy language of human interaction and translate it into a representation that can be understood by a machine. To do that we have to simulate the machine in our heads, and manually trace every possible path of execution. The number of execution paths grows at every branch point in the code, and there are often many different ways to branch an execution path. This results in a search space that can very rapidly grow to be unmanageable when trying to figure out which code path is being followed.

To combat this complexity we make assumptions about how our code will behave at runtime. With any assumptions we make, if they are enforced by the language then we can safely prune large swaths of potential code paths from our search space. An example of this kind of assumption is found in strongly, statically typed languages, where we can safely assume a value will be one of a restricted range of values instead of all possible values. Another example occurs when dealing with objects in object oriented programming languages. When a method is called on an object we can safely assume that, when operating in that method, we're operating in the context of the object it was called on, not a different object.

Let’s take a look at an example of how you can increase the safety of assumptions about your applications. I'll be using Ruby and Rails for the example, but the technique is applicable to any object oriented language.

Suppose you're working on a project involving a web store, and you already have code allowing users to purchase a product.

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def purchase
product = Product.find(params[:id])
product.purchase
rescue ActiveRecord::RecordNotFound
render text: "Could not find product"
end
end
# app/models/product.rb
class Product < ActiveRecord::Base
def purchase
update_attribute :purchased, purchased + 1
end
end

Notice that there are two code paths in the controller method. The first occurs when a product is found. The second occurs if the product is not found, when we branch into the rescued exception.

Now there is a business requirement to keep track of discontinued products, which users are not allowed to purchase. Let’s start implementing this as a boolean flag on the product model.

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def purchase
product = Product.find(params[:id])
# Using Rails' question method for boolean attributes
unless product.discontinued?
product.purchase
end
rescue ActiveRecord::RecordNotFound
render text: "Could not find product"
end
end

Notice that we've gone up from two code paths to three by adding a conditional. If we use a different technique however, we can add the discontinued product functionality without increasing code path complexity. This technique involves subclassing Product to model purchasable products and discontinued products as distinct entities, encapsulating the purchase functionality only for purchasable products.

# app/models/product.rb
class Product < ActiveRecord::Base
end
# app/models/purchasable_product.rb
class PurchasableProduct < Product
default_scope -> { where(discontinued: false) }
def purchase
update_attribute :purchased, purchased + 1
end
end
# app/models/discontinued_product.rb
class DiscontinuedProduct < Product
default_scope -> { where(discontinued: true) }
end

We use default_scope to let Rails know of some conditions to always apply when querying for an instance of each class. These are enforcing that we can't ever accidentally receive an instance of a PurchasableProduct when it's actually discontinued. Combined with moving purchase to PurchasableProduct these changes mean that we can assume a DiscontinuedProduct will never be purchased. That a DiscontinuedProduct will never be purchased is an assumption that is enforced by the language and libraries, and so we can rely on it when writing the controller method.

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def purchase
product = PurchasableProduct.find(params[:id])
product.purchase
rescue ActiveRecord::RecordNotFound
render text: "Could not find product"
end
end

Now we're back down to the original two code paths in the controller, either a PurchasableProduct is found or a PurchasableProduct is not found.

I think this approach is a big win for developers. If your domain has multiple distinct types of entities and functionality that only applies to some of those types, then this neatly encapsulates that functionality so it's harder to accidentally introduce a bug, such as purchasing a discontinued product. It also makes it much easier to mentally simulate and debug the application, because we have more safe assumptions to lean on.

In a future article I'll be exploring some ideas around problem specification that can help guide us towards solutions like the one above.

Daniel Wilson

Share: