DEV Community

MetaDave πŸ‡ͺπŸ‡Ί
MetaDave πŸ‡ͺπŸ‡Ί

Posted on

Ruby Value Objects for Sets


Sometimes a set of value objects has its own attributes in addition to the attributes of the individual values themselves.


When we have a single value and we want to infer behaviour from it, for example when we have country codes and we want to be able to read the continent name, the currencies, or the key of the national anthem, then a value object is probably what we're looking for.

And when we have a set of such single values, such as a list of countries to be visited in a trip, then we also want to send messages to that list of countries.

At the simple end, we might have #count, or #uniq_count if countries can be present more than once, currencies (expecting a list of the currencies for all of the countries in the list to be returned), etc..

Quite often there are issues of compatibility or completeness involved in a set, and the code for detecting these issues needs a home.

That home is probably a value object.

class Countries
  def initialize(*countries)
    @countries = Array.wrap(countries)

  delegate :count,
           to: :countries

  def currencies

  def uniq_count

  def compatible?

  def closed_loop?
    countries.first == countries.last

A bibliographic example

Consider an example: a code list that we at Consonance use a lot, the Thema subject categories for book publishing. A publisher assigns multiple codes to a book to inform third parties, such as sales agents, other publishers, distributors, retailers, and ultimately the potential buyer, what it is about and who might be interested in it.

You can see by browsing the list that there is a huge range in the subjects coding, reflecting that some books are intended for professional or academic practitioners, for children, for people with a general interest in pets, and there are also codes for place, time, and special themes (birthdays, seasonal holidays etc).

Many of these can be combined for a single book, and we expect all of them to be individually valid of course.

But some further questions can arise when you consider all of the codes assigned to a book:

  • Are codes being used which suggest that others should also be present, and if so are those others actually present?
    • Do children's books have an age range?
    • Do law books contain a geographical restriction? In general every professional law book relates to a particular geographical region.
    • Is a book about archaeology also coded for both time and place?
  • Are there codes being used which are possibly not compatible with each other?
    • Is a book about a scholarly subject also coded as being of interest to children under five years of age?
    • Is a book coded for general interest also coded for professional interest?

So individual codes can each be valid, while the combination of them can be problematic in a number of ways.

The important point here is that these complete? and valid? attributes are of the set of codes, not of the individual codes themselves, and might be implemented as:

class ThemaSet
  def initialize(*set)
    @set = set

  delegate :any?, to: :set

  def errors
    errors_of_incompatibility + errors_of_incompleteness

  private def childrens?

  private def professional_or_scholarly?

  private def historical?

  private def child_incompatible?
    childrens? && (professional_or_scholarly? || adult_themed?)

  private def missing_time?
    !timed? && historical?

  private def missing_place?
    !placed? && (historical? || legal?)

  private def errors_of_incompatibility
    [].tap do |errors|
      errors << "codes incompatible with child coding are present" if child_incompatible?

  private def errors_of_incompleteness
    [].tap do |errors|
      errors << "codes should be accompanied by time coding"  if missing_time?      
      errors << "codes should be accompanied by place coding" if missing_place?

Back to the real world

So the pattern there is reasonably clear, I think.

The set-value object examines the attributes of the individual value objects in order to produce characterisations of the set as a whole.

Although the example given was quite specific to a business area, this pattern of complete? vs incomplete?, and compatible? vs incompatible?, leading to attributes of valid? and a list of errors is a common one.

And this makes it amenable to the use of the ValidValidator described here

In many cases the individual objects will not be plain value objects, but might be derived from an ActiveRecord-backed has_many association. But if the set of objects has its own attributes, then that is still a candidate for this technique.

class Book
  has_many :thema_codes

  def thema_set
    @_thema_set ||=

  delegate :valid?,
           to:     :thema_set,
           prefix: true

Further functionality

A variation on this can be used to answer the question, β€œwhat is the effect of a particular change to a set?”.

Given a current set and a candidate member of it, you can compare the original against the original + candidate-value, and ask of it "have I changed a property of the set by making this change, such as introducing (or solving) any errors?"

And the implementation of that is reasonably clear:

class SetComparator
  def initialize(set_object, new_value)
    @original_set = set_object
    @new_set      = << new_value)

  def errors_added
    new_set.errors - original_set.errors

  def errors_removed
    original_set.errors - new_set.errors

Top comments (0)