Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

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!

Free Resource

Learn how to build modern digital experience apps with Crafter CMS. Download this eBook now. Brought to you in partnership with Crafter Software

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
      end
end

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
    end

    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
        passengers
        .select { |passenger| passenger.age < min_age }
        .select { |passenger| country_codes_requiring_all_passengers_to_be_adult.include?(passenger.country_code) }
        .any?
    end
end

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
    end

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

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

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

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

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)
    passengers
        .younger_than(min_age)
        .from_any_of_countries(country_codes_requiring_all_passengers_to_be_adult)
        .any?
end

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.

Crafter is a modern CMS platform for building modern websites and content-rich digital experiences. Download this eBook now. Brought to you in partnership with Crafter Software.

Topics:
ruby ,array

Published at DZone with permission of Damian Jaszczurowski. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

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

{{ parent.tldr }}

{{ parent.urlSource.name }}