Over a million developers have joined DZone.

Custom Lists in Ruby

DZone 's Guide to

Custom Lists in Ruby

Lists are great. No one can disagree with that. But what happens when you need to squeeze a bit more functionality out of your lists when using Ruby? Check it out!

· Web Dev Zone ·
Free Resource

Lists are great. No one can disagree with that. But what happens when you need to squeeze a bit more functionality out of your lists when using Ruby? Check it out!

Consider a very simple model:

class Passenger
    attr_accessor :age
    attr_accessor :country_code 

    def initialize(age, country_code)
        @age = age
        @country_code = country_code

It's very common to work with list of items of a specific type like the one above, for example:

 list = [Passenger.new(21, 'PL'), Passenger.new(17, 'DE')] 

Usually, in the beginning, we have a full list, but later in order to decide whether specific action should be available/triggered we need to decide according to the state of items within this list. Let's say we need to show a notification if exists a passenger who comes from Poland and is under 18. It's pretty simple:

class Service
    def initialize(min_age = 18, country_codes_requiring_all_passengers_to_be_adult = ['PL'])
        @min_age = min_age
        @country_codes_requiring_all_passengers_to_be_adult = country_codes_requiring_all_passengers_to_be_adult

    def show_notification?(passengers)
        #I'm aware those conditions could be computed in a single step [one select]. This version is just a bit more readable, although the performance is degraded
        .select { |passenger| passenger.age < min_age }
        .select { |passenger| country_codes_requiring_all_passengers_to_be_adult.include?(passenger.country_code) }

The problem is, if we decide to test our service we'll have to check all possible combinations in order to guarantee it works as expected. Let's count those scenarios:

  • passenger over 18 from Poland
  • passenger over 18 not from Poland
  • passenger under 18 from Poland
  • passenger under 18 not from Poland

When new factor comes into play, we need revisit service specs and cover it with a new scenario. Eventually, we finish with complex code, full of different condition checks. Alternatively, we may try a different approach and instead of using Array as the structure, we may define custom PassengerList type.

class CustomList
    include Enumerable

    def initialize(items)
        @items = items

    def each(&block)
        @items.each { |item| block.call(item) }

    def select(&block)
    # ... rest of methods like reject, which should return new instance, instead of items subset

class PassengerList < CustomList
    def younger_than(age)
        select { |p| p.age < age } #will return new instance of PassengerList

    def from_any_of_countries(country_codes)
        select { |p| country_codes.include?(p.country_code) }

What is important here is the fact that we never return the Array but always an instance of PassengerList. What we get is the nice chainability:

def show_notification?(passengers)

Now we can test our service only by verifying whether chain of methods has been invoked on list without checking whether each one condition works as expected (those will be tested in isolation in specs of PassengerList). Code seems to be even more declarative than before.

It's very important to keep in mind that since we introduce custom lists, we should guarantee that this type is used across the whole application. Otherwise, we may end up in a situation when in some parts we depend on an array and in some we depend on PassengerList — what will be extremely confusing.

Obviously, the problem could be solved in multiple different ways.

ruby ,array

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}