ValueSemantics--A gem for making value objects

??? words · ??? min read

This is a work in progress from The Pipeline. It will likely change before it is published to the main listing of articles. It is currently at the "drafted" stage.

TODO: Intro

What’s the Point?

Before getting into the details of this gem, you might be wondering: what are value objects, and why would I want to use them?

The best answer, in my opinion, is to watch The Value of Values, a talk by Rich Hickey, the creator of the programming language Clojure. It is a talk about how software systems are designed, from the perspective of someone who has created a functional programming language, but the concepts apply to all languages.

I can’t recommend Rich’s talk enough, so if you’re not already familiar with value objects, go watch it before reading the rest of this article.

ValueSemantics Overview

ValueSemantics provides a way to make value classes, with a few additional features. These value classes are like immutable Structs, but they can be strict and explicit about what values they allow, and they come with a little bit of additional, commonly-desired functionality.

Basic usage looks like this:

class Person
  include ValueSemantics.for_attributes {
    name
    birthday
  }
end

p = Person.new(name: 'Tom', birthday: Date.new(2020, 4, 2))
p.name #=> "Tom"
p.birthday #=> #<Date: 2020-04-02 ((2458942j,0s,0n),+0s,2299161j)>

p2 = Person.new(name: 'Tom', birthday: Date.new(2020, 4, 2))
p == p2 #=> true

The functionality above looks like a Struct with unnecessary extra steps. This is by design.

More advanced usage might look like this:

class Person
  include ValueSemantics.for_attributes {
    name String, default: 'Anonymous'
    birthday Either(Date, nil), coerce: true
  }

  def self.coerce_birthday(value)
    if value.is_a?(String)
      Date.parse(value)
    else
      value
    end
  end
end

# attribute defaults and coercion
p = Person.new(birthday: '2020-04-02')
p.name #=> 'Anonymous'
p.birthday #=> #<Date: 2020-04-02 ((2458942j,0s,0n),+0s,2299161j)>

# non-destructive updates (creates a new object)
p.with(name: "Dane")
#=> #<Person name="Dane" birthday=#<Date: 2020-04-02 ((2458942j,0s,0n),+0s,2299161j)>>

# attribute validation
p.with(name: nil)
#=> ArgumentError: Value for attribute 'name' is not valid: nil

This example shows the main features of the gem:

  1. The objects are intended to be immutable. The #with method creates new objects based on existing objects, without changing the existing ones.

  2. Attributes can have defaults.

  3. Attributes can be coerced. This means that nearly-correct attribute values can be automatically converted into actually-correct values. In the example above, Strings are coerced into Dates.

  4. Attributes can be validated. Validators ensure that attribute values are correct, and raise exceptions if they are not.

Check out the documentation for all of the details.

The main design goals for this gem, in order of importance, were:

  1. Be extensible

    ValueSemantics is primarily concerned with data types, and those vary from project to project. You should be able to define your own custom validators and coercers easily, and they should be as powerful as the built-in ones.

  2. Be unobtrusive

    While ValueSemantics does have affordances that encourage you to use it as intended, it shouldn’t force you to use it that way by restricting your choices.

  3. Follow conventions

    As much as possible, ValueSemantics should conform to existing Ruby standards.

  4. Be standalone

    The gem should be light-weight, with minimal (currently zero) dependencies.

It’s a Module Builder

ValueSemantics is an implementation of the [module builder pattern][module_builder], as opposed to a class builder, or a base class with class methods. Mixin modules are more flexible than forcing users to inherit from something.

Maybe you are already forced to inherit from something else, and Ruby doesn’t have multiple inheritance, so that would be a problem. Using a mixin sidesteps this.

The module builder pattern allows any of the methods to be overridden. This is not always true of the other approaches, because they often define methods directly on the class, not on the superclass, which means that you would need hacks like prepend to override them.

I’m not a fan of reopening classes, like the way that Struct requires if you want to add a method. I want your own classes to look like your own classes, and for ValueSemantics to be as unobtrusive as possible.

[module_builder]:

Validation is Case Equality

You could be excused for thinking that there is special code for handling String, Integer, nil, etc. Nope! Attribute validation works via standard case equality.

The validators in ValueSemantics can be any object that implements the #=== method. There are plenty of classes built into Ruby that already implement this method – Module, Regexp, Range, and Proc, just to name a few. Anything that works in a case expression will work as a validator.

Having no special handling for built-in types means that your own custom validators are first-class citizens. The small number of built-in validators are built on top of ValueSemantics, not integrated with it, in the same way that you would write a custom validator.

Gems like qo are already compatible with ValueSemantics, because they conform to the same case equality standard.

require 'qo'

class Person
  include ValueSemantics.for_attributes {
    # using Qo matcher as a validator
    age Qo[Integer, 1..120]
  }
end

Person.new(age: 150)
#=> ArgumentError: Value for attribute 'age' is not valid: 150

Callable Objects

Coercers are just [callable objects][callable_objs]. This is another Ruby standard, already implemented by Proc and Method in the standard library.

This gives us various ways to use existing Ruby functionality, without necessarily needing to write a custom coercer class.

class Whatever
  include ValueSemantics.for_attributes {
    # use existing class methods
    updated_at coerce: Date.method(:parse)

    # use a lambda
    some_json coerce: ->(x){ JSON.parse(x) }

    # use Symbol#to_proc
    some_string coerce: :to_s.to_proc

    # use Hash#to_proc
    dunno coerce: { a: 1, b: 2 }.to_proc
  }
end

Whatever.new(
  updated_at: '2018-12-25',
  some_json: '{ "hello": "world" }',
  some_string: [1, 2, 3],
  dunno: :b,
)
#=> #<Whatever
#     updated_at=#<Date: 2018-12-25 ((2458942j,0s,0n),+0s,2299161j)>
#     some_json={"hello"=>"world"}
#     some_string="[1, 2, 3]"
#     dunno=2
#     >

Default generators are also callable objects, allowing you to do similar things:

class Person
  include ValueSemantics.for_attributes {
    created_at default_generator: Time.method(:now)
  }
end

Person.new
#=> #<Person created_at=2018-12-24 12:21:55 +1000>

And again, since there is no special handling for built-ins, your custom coercers are first-class citizens.

[callable_objs]:

To Freeze or not to Freeze

There is ongoing debate about the best way to do immutability in Ruby. A lot of this debate revolves around what things should be frozen, and how they should be frozen.

ValueSemantics takes an approach to immutability that I call “no setters”. The “no setters” approach does not freeze anything. The objects could be frozen, but it’s completely optional. Instead of enforcing immutability by freezing, you just don’t provide any methods that mutate the object.

If you use the object the way that it is intended to be used, through its public methods, then it is effectively immutable. However, ValueSemantics doesn’t restrict you from making bad decisions if you are determined to do so.

You are able to mutate attributes:

p = Person.new(name: "Tom")
p.name.replace("Dane")
p.name #=> "Dane"

You are able to write mutating methods:

class Person
  include ValueSemantics.for_attributes { name }

  def name=(new_name)
    @name = new_name
  end
end

You are able to use Ruby magic:

p = Person.new(name: "Tom")
p.instance_eval { @name = "Dane" }
p.name #=> "Dane"

ValueSemantics is designed with affordances that make it feel natural to use it the right way, but it doesn’t prevent a determined Rubyist from using it the wrong way. This is a “sharp knife” approach to immutability. There might be legitimate reasons why you need to mutate the object, so I want to leave the avenue open to you. This is part of the deliberate decision to make this gem as unobtrusive as possible.

My current opinion on this topic is that these things should be picked up in code review, not enforced by the language or the gem. It is the sort of thing that Rubocop is meant for.

The guilds feature of Ruby 3 will likely come with new functionality for deep freezing objects. When that happens, I may revisit this decision. I’m also open to adding freezing to ValueSemantics as an optional feature.

ValueSemantics does freeze some of its own, internal objects, but it should never freeze any of your objects.

Integration Continuity

Sometimes you want to do some coercion that is a little bit complicated, but it’s specific to this one class, so you don’t want to write a separate, reusable coercer just yet. You could do this by overriding initialize, but I think this is a fairly common scenario, so I wanted to provide a nicer way to do it. This provides a step in between no coercion and reusable coercion objects.

class Person
  include ValueSemantics.for_attributes {
    birthday coerce: true
  }

  def self.coerce_birthday(value)
    if value.is_a?(String)
      DateTime.strptime(value, "%a, %d %b %Y %H:%M:%S %z")
    else
      value
    end
  end
end

I stole this idea of “integration continuity” from Casey Muratori’s talk Designing and Evaluating Reusable Components. See the section from 6:28 to 11:45.

This is a small example of integration continuity. When we use a framework or a library, we usually start of with a simple integration that doesn’t use a lot of the available features. As our software grows over time, we tend to integrate more and more. Sometimes we look at integrating some new functionality, and discover that it’s only possible with a huge amount of effort, and the resulting implementation is overkill compared to our requirements. I want to avoid that, and provide a smoother transition.

This is why everything is optional in ValueSemantics. You can start using it with no defaults, no validators, and no coercers, as if it was a simple immutable Struct-like object. Then you can start using the other features as you need to. Every time you choose to use an additional feature, the transition should be easy.

DSL is Icing

In my opinion, a DSL should always be the icing on the cake. It should be a rich but thin layer on top of a stable, well-designed base. You don’t want pockets of icing in random locations throughout the cake.

Random pockets of icing in a cake actually sound delicious.

In that spirit, the DSL in ValueSemantics is completely optional.

This usage of the DSL:

class Person
  include ValueSemantics.for_attributes {
    name String
    age Integer
  }
end

Could also be written as:

class Person
  include ValueSemantics.build_module([
    ValueSemantics::Attribute.new(name: :name, validator: String),
    ValueSemantics::Attribute.new(name: :age, validator: Integer),
  ])
end

In fact, this is the same API that the DSL is built on top of. ValueSemantics.for_attributes is just a shorthand way to write this:

attributes = ValueSemantics::DSL.run {
  name String
  age Integer
}

ValueSemantics.build_module(attributes)

The DSL is not required, and is completely segregated from the rest of the gem. It is just there to make the attributes read more nicely, by removing unnecessary implementation details like ValueSemantics::Attribute.

This also gels well with the concept of integration continuity. It enables super advanced integrations with the gem, like automatically generating value classes based on database column information at run time. I don’t expect many people to take the step from hard-coded attributes to dynamic attributes, but it is possible to do, as a side effect of good design.

Stability

Value objects are a something of a cross-cutting concern. There is no dedicated part of your app where all of the value objects live. You shouldn’t be making an /app/values directory in your Rails apps, for example. They can be used anywhere.

The term shotgun surgery means to implement something by making small changes in many different places. It is a code smell, indicating that code might have been copy-pasted many times, or that there might be design problems.

This is an important design consideration, as a gem author. If a bug is introduced, that bug could affect anywhere and everywhere in the app. If backwards-incompatible changes are introduced, then updating the gem requires shotgun surgery.

These considerations relate to the stable dependencies principle, which states that code should only depend upon code that is more stable than itself. If the Struct class had backwards-incompatible changes in every release of Ruby, it would be a nightmare to use. But in reality, if you wrote some code using Struct in Ruby 1.8, it would still work today in Ruby 2.6, more than 15 years later. That is the level of stability that I’m aiming for in ValueSemantics.

I’m addressing these considerations in the following ways:

  1. The gem is finished. You could copy and paste ValueSemantics into your project and never update it, if that’s what you want. I do expect to add a few small things, but if you don’t need them, there is no need to update.

  2. It has zero dependencies. Updating the gem is easier, and you don’t have to worry about version conflicts.

  3. All future versions should be backwards compatible. Because the gem is “finished,” I don’t see any reason to introduce breaking changes. You don’t need to do shotgun surgery if the API stays the same.

  4. The gem is tested with 100% mutation coverage across multiple Ruby versions. In a nutshell, mutation coverage is like line coverage on steriods. I’m not saying that the gem is completely bug-free, but it is tested much more thoroughly than most gems.

Comparison to Existing Gems

There are already gems for making value objects, or something similar.

  1. Struct

    Struct is built into the Ruby standard library. Struct objects are mutable, which means that they aren’t really value types, but they are similar. It doesn’t have any validation or coercion. Any attributes that aren’t supplied at instantiation will default to nil.

    Struct is specially designed to have good performance in terms of memory and speed. ValueSemantics doesn’t have particularly bad performance, but it’s not super optimised either.

  2. Values

    The values gem works like Struct except that it is immutable, and provides non-destructive updates. It is a little bit more strict that Struct, in that it will raise an exception if any attributes are missing.

    ValueSemantics provides extra functionality, like defaults, validation, and coercion.

  3. Anima

    The anima gem is very similar to the values gem, except that values builds a class, and Anima builds a module that you mix into your own class.

    ValueSemantics is a superset of Anima, adding defaults, validation, and coercion.

  4. Adamantium

    The adamantium gem provides a way to automatically freeze objects after they are initialized. It also has some functionality around memoization and non-destructive updates. You can make value classes using Adamantium, but that is not its primary purpose. It’s primary purpose is to freeze everything.

    ValueSemantics does not freeze anything. Adamantium doesn’t implement equality, attr_readers, or #initialize, so implementing a value class requires you to write some boilerplate manually. Adamantium doesn’t provide validation or coercion.

  5. Virtus

    Virtus is “attributes on steroids for plain old ruby objects”. It provides a lot of functionality, including validation and coercion. Virtus objects are mutable by default, but Virtus.value_object creates immutable objects. Virtus is a predecessor of the dry-struct and dry-types gems.

    ValueSemantics is smaller than Virtus. The validation and coercion in ValueSemantics is more simplistic than Virtus.

  6. dry-struct

    The dry-struct gem is probably the most popular out of all of these options, and the most similar to ValueSemantics in terms of features. It has full-featured validation and coercion provided by dry-types, and dry-struct classes can be used in dry-types schemas. It has optional functionality for freezing objects.

    ValueSemantics is a simpler (and has less features) than dry-struct. dry-struct is integrated with the dry-rb ecosystem, whereas ValueSemantics is standalone with no dependencies.

See A Review Of Immutability In Ruby for more information about these gems.

Further Resources

  • re value objects
  • re mutation testing
  • re module builder
  • re callable objects
  • “A Review Of Immutability In Ruby”

TODO

  • consistent “value object” “value class” vocab
  • reorder sections
  • remove references to “right” “wrong” “good” “bad” “correct” “incorrect” usage
  • update README in gem repo
  • change ValueSemantics::Semantics to ValueSemantics::InstanceMethods
  • change ValueSemantics_Whatever to ValueSemantics_Attributes
  • turn en-dashes into em-dashes
  • update ValueSemantics readme with note for potential contributors
  • Make the DSL return a “recipe” object, not an array of attrs

Got questions? Comments? Milk?

Shoot an email to [email protected] or hit me up on Twitter (@tom_dalling).

← Previously: How To Create An Anti-corruption Layer

Next up: You Are Not Your Code →

Join The Pigeonhole

Don't miss the next post! Subscribe to Ruby Pigeon mailing list and get the next post sent straight to your inbox.