Skip to content

bkuhlmann/wholable

Repository files navigation

Wholable

Wholable allows you to turn your object into a whole value object by ensuring object equality is determined by the values of the object instead of by identity. Whole value objects — or value objects in general — have the following traits as also noted via Wikipedia:

  • Equality is determined by the values that make up an object and not by identity (i.e. memory address) which is the default behavior for all Ruby objects except for Data and Structs.

  • Identity remains unique since two objects can have the same values but different identity. This means BasicObject#equal? is never overwritten — which is strongly discouraged — as per BasicObject documentation.

  • Value objects should be immutable (i.e. frozen) by default. This implementation enforces a strict adherence to immutability in order to ensure value objects remain equal and discourage mutation.

Features

  • Ensures equality (i.e. #== and #eql?) is determined by attribute values and not object identity (i.e. #equal?).

  • Allows you to compare two objects of same or different types and see their differences.

  • Provides pattern matching.

  • Automatically defines public attribute readers (i.e. .attr_reader) based on provided keys.

  • Ensures object inspection (i.e. #inspect) shows all registered attributes.

  • Ensures object is frozen upon initialization.

Requirements

  1. Ruby.

Setup

To install with security, run:

# 💡 Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
gem install wholable --trust-policy HighSecurity

To install without security, run:

gem install wholable

You can also add the gem directly to your project:

bundle add wholable

Once the gem is installed, you only need to require it:

require "wholable"

Usage

To use, include Wholable along with a list of attributes that make up your whole value object:

class Person
  include Wholable[:name, :email]

  def initialize name:, email:
    @name = name
    @email = email
  end
end

jill = Person.new name: "Jill Smith", email: "jill@example.com"
jill_two = Person.new name: "Jill Smith", email: "jill@example.com"
jack = Person.new name: "Jack Smith", email: "jack@example.com"

jill.name              # "Jill Smith"
jill.email             # "jill@example.com"

jill.frozen?           # true
jill_two.frozen?       # true
jack.frozen?           # true

jill.inspect           # "#<Person @name=\"Jill Smith\", @email=\"jill@example.com\">"
jill_two.inspect       # "#<Person @name=\"Jill Smith\", @email=\"jill@example.com\">"
jack.inspect           # "#<Person @name=\"Jack Smith\", @email=\"jack@example.com\">"

jill == jill           # true
jill == jill_two       # true
jill == jack           # false

jill.diff(jill)        # {}
jill.diff(jack)        # {
                       #   name: ["Jill Smith", "Jack Smith"],
                       #   email: ["jill@example.com", "jack@example.com"]
                       # }
jill.diff(Object.new)  # {:name=>["Jill Smith", nil], :email=>["jill@example.com", nil]}

jill.eql? jill         # true
jill.eql? jill_two     # true
jill.eql? jack         # false

jill.equal? jill       # true
jill.equal? jill_two   # false
jill.equal? jack       # false

jill.hash              # 3650965837788801745
jill_two.hash          # 3650965837788801745
jack.hash              # 4460658980509842640

jill.to_a              # ["Jill Smith", "jill@example.com"]
jack.to_a              # ["Jack Smith", "jack@example.com"]

jill.to_h              # {:name=>"Jill Smith", :email=>"jill@example.com"}
jack.to_h              # {:name=>"Jack Smith", :email=>"jack@example.com"}

jill.with name: "Sue"  # #<Person @name="Sue", @email="jill@example.com">
jill.with bad: "!"     # unknown keyword: :bad (ArgumentError)

As you can see, object equality is determined by the object’s values and not by the object’s identity. When you include Wholable along with a list of keys, the following happens:

  1. The corresponding public attr_reader for each key is created which saves you time and reduces double entry when implementing your whole value object.

  2. The #to_a and #to_h methods are added for convenience in order to play nice with Data and Structs.

  3. The #deconstruct and #deconstruct_keys aliases are created so you can leverage pattern matching.

  4. The #==, #eql?, #hash, #inspect, and #with methods are added to provide whole value behavior.

  5. The object is immediately frozen after initialization to ensure your instance is immutable by default.

Caveats

Whole values can be broken via the following:

  • Duplication: Sending the #dup message will cause your whole value object to be unfrozen. This might be desired in certain situations but make sure to refreeze when able.

  • Post Attributes: Adding additional attributes after what is defined when including Wholable will break your whole value object. To prevent this, let Wholable manage this for you (easiest). Otherwise (harder), you can manually override #==, #eql?, #hash, #inspect, #to_a, and #to_h behavior at which point you don’t need Wholable anymore.

  • Deep Freezing: The automatic freezing of your instances is shallow and will not deeply freeze nested attributes. This behavior mimics the behavior of Data objects.

Development

To contribute, run:

git clone https://github.com/bkuhlmann/wholable
cd wholable
bin/setup

You can also use the IRB console for direct access to all objects:

bin/console

Tests

To test, run:

bin/rake

Credits