Contracts and Value objects in Ruby

Venky Iyer, 2 Jan 2018


Fin’s codebase is now a couple of years in development — 2+ years of fast-paced engineering and iteration as we evolve our product and scale. As you’d expect, software entropy is beginning to rear its ugly head. In the words of Professor Moody, there is only one real solution to this:

img

The tools and techniques that we can use to stave off software entropy are different for our JS applications and our Ruby backend¹. The JS codebase benefits from the better tooling and more opinionated libraries from that community — React, Redux, Flow, Prettier, ESLint, CommonJS modules etc.

On the other hand, our server-side Ruby on Rails codebase is more vulnerable for a few reasons:

  • ActiveRecord gives you amazing powers, but does not necessarily guide you to create clear seams between models — which can result in a big ball of mud application
  • Ruby’s incredible expressiveness and meta-programmability can be a double-edged sword
  • Ruby doesn’t yet have native support for type hinting or checking to help enforce type constraints and improve documentation.

There has been plenty of discussion around techniques to address these issues. In this post, I will discuss two measures that we have been deploying at Fin to combat these problems:

  1. Contracts for type hinting and documentation
  2. Value objects to extract domain structure out of ActiveRecord models

1: Contracts at Fin

The Contracts.ruby library developed by Adit Bhargava lets us take a baby step towards type hinting in Ruby. It does runtime type validation, and being pure Ruby, it is relatively easy to extend and debug. The contracts library has a great tutorial and RubyDoc, and the Github repo is active, so I won't bother repeating that material; suffice it to say I strongly recommend using contracts if you are running Ruby in a medium sized codebase:

  • Adding contracts for your functions clarifies your thinking while writing code, so your interfaces are well-defined and not overly lenient in what they accept.
  • We only run contracts in dev and test environments so it has negligible cost in production, but it still ends up being helpful to highlight input interface changes. It also helps us avoid specs that are so stubbed or mocked that they no longer resemble the code they are testing.
  • It is great documentation that does not [rot](It is great documentation that does not rot because it isn’t separate from the code.) because it isn’t separate from the code.
  • Though contracts does not support type propagation or inference, it is simple and expressive, and it's easy to imagine how one might migrate from contracts to a true type checker like ‘rdl’ as it gains more support for Rails.

Some other tips about how we use contracts:

  • Do not use the Method Overloading functionality in contracts! Ruby and Rails are magical enough without adding this to the mix.
  • Contracts don’t work in Rails controllers ☹️ If you know how to use contracts in controllers, let us know in the comments!
  • We haven’t settled on a good approach to share and reuse contracts across our codebase. If you have a preferred approach, let me know.
  • We alias Contracts to C for convenience:
  
    C = Contracts
  

We have a small number of extensions to help us improve our dev experience with contracts.

  • to handle an occasional redefinition of a class (🤔 tell me if you know why this happens!)
  • to have contracts accept RSpec doubles, so that we can pass mocks to functions that have contracts:
  • to help simplify contract violation error messages:

Custom contracts are easy to define; here are some of the contracts we use:

  • To validate an ActiveRecord::Relation of a specific ActiveRecord model:

You can declare this contract like ActiveRecordRelation[User] and it will fail for relations that return other models.

To declare that a method takes an ID of a certain ActiveRecord model:

This contract can be declared like ID['User'] (this contract takes a string instead of a class to avoid circular dependencies). Note that ID does not actually validate that the ID is a user ID (because that would require a database call); it only serves as documentation.

  • To validate ActiveRecord enum keys or values:

This contract can be used like ActiveRecordEnumValue[User.roles]

Including Contracts correctly while using ActiveSupport::Concern can sometimes be a process of trial-and-error. Here are some rules that we've found to work well:

  1. Always include C as the first include in classes, if you're using contracts or if you're including concerns that have contracts
  2. Do not include C in the included do block of ActiveSupport::Concern
  3. Do include C in module ClassMethods in ActiveSupport::Concern
  4. Do include C at the top level of ActiveSupport::Concern, and before extend ActiveSupport::Concern
  5. Do not explicitly include Contracts::Core etc

2 Value objects

Martin Fowler has a great introduction to Value objects here. At Fin, we tend to use Value objects to represent complex data that gets passed to and from Service classes.

For example, consider a service class that mutates Fin entries:

We’re passing an array of data blobs into this service class, and the straightforward way to do so would be to represent each blob as a hash. In this example, the contract type AttributeData would be an alias for a Hash type, something like

This has a few downsides:

  • Since we don’t enforce contracts in production, this input is completely unconstrained in that environment. The data blobs passed into this service class could have extra keys or missing keys, and it would be left to the application code to handle this.
  • We named the contract type AttributedData, but this name and its associated shape does not propagate to callsites where the data array is defined, or users of @attribute_datas. Contracts are unfortunately very local to the method that they decorate.
  • Our data hashes are mutable — you can imagine the potential bugs (or unmaintainable code) that might arise from working with these raw hashes.

ValueStruct serves as a better implementation for such data blobs, by taking advantage of the contracts library[4] to validate the members of a struct[2].

To use it in this example, we’d first define the AttributeData ValueStruct, as

This is analogous to the usual Ruby Struct definition like

  
    Customer = Struct.new(:name, :address)
  

except, that ValueStruct.define takes the contract for its initializer, and returns a Struct that enforces that contract.

We can instantiate AttributeData as long as we conform to the contract:

We can work with it like a Struct:

  
    [1]> a_data.attribute_value
    "123"
  

ValueStruct is dependent on the contracts library to validate its inputs; so it does not validate their types in production (at least the way we use contracts). However, it does have one trick up its sleeve — we built it to enforce the presence of non-optional keys, even though it doesn't validate their types.

Further, ValueStruct is immutable:

  
[2]> a_data.attribute_value = "345"
ValueStruct::ImmutabilityError: ValueStruct::ImmutabilityError
  

but we can clone instances and modify them during cloning:

Neat, huh? What’s really cool, and a testament to Ruby’s metaprogramming abilities, is how simple this is to put together.

Let’s take a look under the hood of ValueStruct:

We begin with a recipe for a Struct that takes keyword arguments[3], pulled from SO:

Let’s implement cloning on KeywordStruct:

ValueStruct.define takes a contract and returns a descendant of KeywordStruct:

And finally, let’s add immutability:

The full implementation is here; it is nearly identical to the snippets above except for an addition to patch a deficiency in the KeywordArgs contract.

Conclusion

We find that annotating functions with contracts makes them much easier to approach as a new engineer, and we can refactor them with more assurance that we understand what they are doing. Value objects are a neat pattern for creating lightweight objects that have shape but not a lot of behavior.

We are also introducing some other techniques around annotating method visibility, using Rubocop to influence developer behavior, and using Service objects to make “thinner” models. I hope to follow up with posts describing these in more detail.

Thanks for reading!

Footnotes

  1. we also have Python (devops and ML) code, and a Swift iOS application, but those are topics for a future post
  2. There is some discussion here on the Contracts repo about contracts-enabled value objects
  3. Ruby 2.5 just added support for keyword arguments to Struct — but this won’t let us set up contracts for the arguments
  4. The dry-types library is another nice candidate for this