Building a collection with Enumerable
For our new awesome projet, we're gonna display a collection of kittens.
Each kitten is defined by its name, age, and its cuteness. Cuteness being a
number between 0 and 100.
Let's say we have our collection of kittens which looks like something like
this:
<span class="c1"># let's mock something responsible for generating a collection of results</span><span class="no">Search</span> <span class="o">=</span> <span class="no">Struct</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">:results</span><span class="p">)</span><span class="c1"># Basic representation of a kitty</span><span class="no">Kitty</span> <span class="o">=</span> <span class="no">Struct</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:age</span><span class="p">,</span> <span class="ss">:cuteness</span><span class="p">)</span><span class="c1"># Results set</span><span class="n">search</span> <span class="o">=</span> <span class="no">Search</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="o">[</span> <span class="no">Kitty</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"darwin"</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">42</span><span class="p">),</span> <span class="no">Kitty</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">:kitty</span><span class="p">,</span> <span class="mi">37</span><span class="p">,</span> <span class="mi">97</span><span class="p">),</span> <span class="no">Kitty</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"maru"</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">64</span><span class="p">)</span><span class="o">]</span><span class="p">)</span><span class="c1"># Let's iterate</span><span class="n">search</span><span class="o">.</span><span class="n">results</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">kitty</span><span class="o">|</span> <span class="nb">p</span> <span class="n">kitty</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="n">kitty</span><span class="o">.</span><span class="n">age</span><span class="p">,</span> <span class="n">kitty</span><span class="o">.</span><span class="n">cuteness</span><span class="k">end</span>
Ok, this is a perfectly working search feature. We find some results, and
display them to the user.
The product owner of this kitten application would like to display the total
cuteness of this result set and the average cuteness.
<span class="n">search</span><span class="o">.</span><span class="n">results</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&</span><span class="ss">:cuteness</span><span class="p">)</span><span class="o">.</span><span class="n">inject</span><span class="p">(</span><span class="ss">:+</span><span class="p">)</span><span class="o">.</span><span class="n">tap</span> <span class="k">do</span> <span class="o">|</span><span class="n">cuteness</span><span class="o">|</span> <span class="nb">p</span> <span class="n">cuteness</span> <span class="nb">p</span> <span class="n">cuteness</span> <span class="o">/</span> <span class="n">search</span><span class="o">.</span><span class="n">results</span><span class="o">.</span><span class="n">size</span><span class="k">end</span>
Again, this is working. Yet, something's still bugging me. To implement our
awesome features, we make several calls to search.results
, which is an
internal object of the library. It is not our domain code.
Enumerable
Let's build our own private collection of kittens. An object with this API:
<span class="n">kitten</span> <span class="o">=</span> <span class="no">Kitten</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">search</span><span class="o">.</span><span class="n">results</span><span class="p">)</span><span class="n">kitten</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">kitty</span><span class="o">|</span> <span class="nb">p</span> <span class="n">kitty</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="n">kitty</span><span class="o">.</span><span class="n">age</span><span class="p">,</span> <span class="n">kitty</span><span class="o">.</span><span class="n">cuteness</span><span class="k">end</span><span class="nb">p</span> <span class="n">kitten</span><span class="o">.</span><span class="n">total_cuteness</span><span class="nb">p</span> <span class="n">kitten</span><span class="o">.</span><span class="n">average_cuteness</span>
Enumerable provides us with a
module that implements methods like map
, find
, any?
… The king of
methods which make using collection in Ruby such a breeze.
So, we need a Kitten
class which includes Enumerable
and takes an Array
as input.
<span class="k">class</span> <span class="nc">Kitten</span> <span class="kp">include</span> <span class="no">Enumerable</span> <span class="kp">attr_reader</span> <span class="ss">:results</span> <span class="kp">private</span> <span class="ss">:results</span> <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">results</span><span class="p">)</span> <span class="vi">@results</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">(</span><span class="n">results</span><span class="p">)</span> <span class="k">end</span><span class="k">end</span>
A few words about this basic class declaration. results
is set as private
because we do not want it to be directly accessed, everything must use the same
api, aka the Kitten
class itself.
This class accepts as input an object or a collection of objects thanks to the
Array
method called in the initialize
.
Yet, this code does not work. To comply to the Enumerable
contract, we need
to define a each
method. This is the method used behind the scenes by
Enumerable
to provide other methods like map
.
<span class="k">class</span> <span class="nc">Kitten</span> <span class="kp">include</span> <span class="no">Enumerable</span> <span class="kp">attr_reader</span> <span class="ss">:results</span> <span class="kp">private</span> <span class="ss">:results</span> <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">results</span><span class="p">)</span> <span class="vi">@results</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">(</span><span class="n">results</span><span class="p">)</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">each</span> <span class="n">results</span><span class="o">.</span><span class="n">each</span> <span class="p">{</span><span class="o">|</span><span class="n">item</span><span class="o">|</span> <span class="k">yield</span> <span class="n">item</span> <span class="p">}</span> <span class="k">end</span><span class="k">end</span><span class="no">Kitten</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="o">[</span><span class="no">Kitty</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"ohai"</span><span class="p">,</span> <span class="mi">42</span><span class="p">,</span> <span class="mi">100</span><span class="p">),</span> <span class="no">Kitty</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"kitty"</span><span class="p">,</span> <span class="mi">37</span><span class="p">,</span> <span class="mi">97</span><span class="p">)</span><span class="o">]</span><span class="p">)</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">kitty</span><span class="o">|</span> <span class="nb">p</span> <span class="n">kitty</span><span class="o">.</span><span class="n">name</span><span class="k">end</span><span class="c1"># => "ohai"</span><span class="c1"># => "kitty"</span>
Now, we need to add total_cuteness
and average_cuteness
.
<span class="k">class</span> <span class="nc">Kitten</span> <span class="kp">include</span> <span class="no">Enumerable</span> <span class="kp">attr_reader</span> <span class="ss">:results</span> <span class="kp">private</span> <span class="ss">:results</span> <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">results</span><span class="p">)</span> <span class="vi">@results</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">(</span><span class="n">results</span><span class="p">)</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">each</span> <span class="n">results</span><span class="o">.</span><span class="n">each</span> <span class="p">{</span><span class="o">|</span><span class="n">item</span><span class="o">|</span> <span class="k">yield</span> <span class="n">item</span> <span class="p">}</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">total_cuteness</span> <span class="vi">@total_cuteness</span> <span class="o">||=</span> <span class="n">sum</span><span class="p">(</span><span class="o">&</span><span class="ss">:cuteness</span><span class="p">)</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">sum</span><span class="p">(</span><span class="o">&</span><span class="n">block</span><span class="p">)</span> <span class="n">results</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&</span><span class="n">block</span><span class="p">)</span><span class="o">.</span><span class="n">inject</span><span class="p">(</span><span class="ss">:+</span><span class="p">)</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">average_cuteness</span> <span class="n">total_cuteness</span> <span class="o">/</span> <span class="n">count</span> <span class="k">end</span><span class="k">end</span>
Now, let' inspect our brand new Kitten
collection.
<span class="n">kitten</span> <span class="o">=</span> <span class="no">Kitten</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="o">[</span><span class="no">Kitty</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"ohai"</span><span class="p">,</span> <span class="mi">42</span><span class="p">,</span> <span class="mi">100</span><span class="p">),</span> <span class="no">Kitty</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">"kitty"</span><span class="p">,</span> <span class="mi">37</span><span class="p">,</span> <span class="mi">97</span><span class="p">)</span><span class="o">]</span><span class="p">)</span><span class="nb">p</span> <span class="n">kitten</span><span class="o">.</span><span class="n">class</span><span class="c1"># => Kitten</span><span class="nb">p</span> <span class="n">kitten</span><span class="o">.</span><span class="n">to_a</span><span class="o">.</span><span class="n">class</span><span class="c1"># => Array</span><span class="nb">p</span> <span class="n">kitten</span><span class="o">.</span><span class="n">total_cuteness</span><span class="c1"># => 197</span><span class="nb">p</span> <span class="n">kitten</span><span class="o">.</span><span class="n">average_cuteness</span><span class="c1"># => 98</span><span class="nb">p</span> <span class="n">kitten</span><span class="o">.</span><span class="n">count</span><span class="c1"># => 2</span><span class="nb">p</span> <span class="n">kitten</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="o">&</span><span class="ss">:age</span><span class="p">)</span><span class="c1"># => 79</span>
The to_a
method creates a new Array
based on items present in the
collection. So, to_a
returns an instance of Array
, it is not a Kitten
instance anymore.
This gives us a nice interface around a collection. We can easily add useful
methods like total_cuteness
or methods to comply with the
Kaminari pagination interface.
Actually, there's nothing new here. This is one of the most common patterns in
the ruby ecosystem, but lots of rails developers use this pattern in libraries
like ActiveRecord
everyday without noticing that has_many
associations are
not really simple instances of Array
.