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:
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.
- Contracts for type hinting and documentation
- 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
contractsin 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.
contractsdoes not support type propagation or inference, it is simple and expressive, and it's easy to imagine how one might migrate from
contractsto a true type checker like ‘rdl’ as it gains more support for Rails.
Some other tips about how we use
- 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
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::Relationof a specific
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
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
ActiveRecordenum keys or values:
This contract can be used like
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:
include Cas the first include in classes, if you're using contracts or if you're including concerns that have contracts
- Do not
include Cin the
includeddo block of
include Cat the top level of
ActiveSupport::Concern, and before
- Do not explicitly include
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 to validate the members of a struct.
To use it in this example, we’d first define the
This is analogous to the usual Ruby Struct definition like
Customer = Struct.new(:name, :address)
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:
> 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.
ValueStruct is immutable:
> 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
We begin with a recipe for a
Struct that takes keyword arguments, pulled from SO:
Let’s implement cloning on
ValueStruct.define takes a contract and returns a descendant of
And finally, let’s add immutability:
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!
- we also have Python (devops and ML) code, and a Swift iOS application, but those are topics for a future post
- There is some discussion here on the Contracts repo about contracts-enabled value objects
- Ruby 2.5 just added support for keyword arguments to
Struct— but this won’t let us set up contracts for the arguments
- The dry-types library is another nice candidate for this