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.
-
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.
-
Ruby.
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"
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: "[email protected]"
jill_two = Person.new name: "Jill Smith", email: "[email protected]"
jack = Person.new name: "Jack Smith", email: "[email protected]"
jill.name # "Jill Smith"
jill.email # "[email protected]"
jill.frozen? # true
jill_two.frozen? # true
jack.frozen? # true
jill.inspect # "#<Person @name=\"Jill Smith\", @email=\"[email protected]\">"
jill_two.inspect # "#<Person @name=\"Jill Smith\", @email=\"[email protected]\">"
jack.inspect # "#<Person @name=\"Jack Smith\", @email=\"[email protected]\">"
jill == jill # true
jill == jill_two # true
jill == jack # false
jill.diff(jill) # {}
jill.diff(jack) # {
# name: ["Jill Smith", "Jack Smith"],
# email: ["[email protected]", "[email protected]"]
# }
jill.diff(Object.new) # {:name=>["Jill Smith", nil], :email=>["[email protected]", 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", "[email protected]"]
jack.to_a # ["Jack Smith", "[email protected]"]
jill.to_h # {:name=>"Jill Smith", :email=>"[email protected]"}
jack.to_h # {:name=>"Jack Smith", :email=>"[email protected]"}
jill.with name: "Sue" # #<Person @name="Sue", @email="[email protected]">
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:
-
The corresponding public
attr_reader
for each key is created which saves you time and reduces double entry when implementing your whole value object. -
The
#to_a
and#to_h
methods are added for convenience in order to play nice with Data and Structs. -
The
#deconstruct
and#deconstruct_keys
aliases are created so you can leverage pattern matching. -
The
#==
,#eql?
,#hash
,#inspect
, and#with
methods are added to provide whole value behavior. -
The object is immediately frozen after initialization to ensure your instance is immutable by default.
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.
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
-
Built with Gemsmith.
-
Engineered by Brooke Kuhlmann.