Search code examples
rubyoopconventions

Ruby 2.1: Composing a Bicycle of Parts -> private method `select' called for nil:NilClass (NoMethodError)


I am getting an error when running this code. The Following is the output:

L
Bicycle#Ex3.rb:32:in `spares': private method `select' called for nil:NilClass (NoMethodError)
    from Bicycle#Ex3.rb:10:in `spares'
    from Bicycle#Ex3.rb:111:in `<main>'

Here is the code:

class Bicycle
  attr_reader :size, :parts

  def initialize(args={})
    @size     = args[:size]
    @parts    = args[:parts]
  end

  def spares
    parts.spares # return an array
  end

  def lead_days
    1
  end
  #...
end

class Parts
  attr_reader :parts

  def initialize(args={})
    @parts = parts
  end

  def size
    parts.size
  end

  def spares
    parts.select{|part| part.needs_spare} 
  end
end

class Part
  attr_reader :name, :description, :needs_spare

  def initialize(args)
    @name      = args[:name]
    @description = args[:description]
    @needs_spare = args.fetch(:needs_spare, true)
  end
end

class RoadBikeParts < Parts
  attr_reader :tape_color

  def post_initialize(args)
    @tape_color = args[:tape_color]
  end

  def local_spares
    {tape_color: tape_color}
  end

  def default_tire_size
    '23'
  end
end 

class MountainBikeParts < Parts
  attr_reader :front_shock, :rear_shock

  def post_initialize(args)
    @front_shock = args[:front_shock]
    @rear_shock = args[:rear_shock]
  end

  def local_spares
      { rear_shock: rear_shock}
  end

  def default_tire_size
    '2.1'
  end
end

chain = Part.new(
                 name: 'chain',
                 description: '10 speed')

road_tire = Part.new(
                     name: 'tape_size', 
                     description: '23')
tape = Part.new(
               name: 'tape_color',
               description: 'red')
mountain_tire = Part.new(
                         name: 'tire_size',
                         description: '2.1')
rear_shock = Part.new(
                      name: 'rear_shock',
                      description: 'Fox')

front_shock = Part.new(
                       name: 'front_shock',
                       description: 'Manitou',
                       needs_spare: false)

road_bike_part = Parts.new([chain, road_tire, tape])                                

road_bike = Bicycle.new(
                        size: 'L',
                        parts: Parts.new([chain,
                                          road_tire,
                                          tape]))

puts road_bike.size
#puts road_bike.parts.size
puts road_bike.spares.size

It is clear this line --> puts road_bike.spares.size is given the error NoMethodError, however, I am not sure how I can make a work around to correct this issue for this example. The spares method is returning an array of Part objects, however it seems my problem lies in the fact the spares method .select is private from the calling object.

Any advice to revise this code would be great. Thanks.


Solution

  • What's happening here is that Parts#parts is nil. You're getting the error on this line:

    # parts is nil
    parts.select{|part| part.needs_spare}
    

    In the initializer of Parts, its parts attribute does not get assigned properly:

    def initialize(args={})
      @parts = parts
    end
    

    So when being initialized, it assigns @parts with the value of parts. But since parts is not a local variable there, it calls the Parts#parts method, which returns nil.

    If you change the initializer to the following:

    def initialize(parts)
      @parts = parts
    end
    

    You'll be able to run the code. But subclasses of Parts seem to expect a Hash in the initializer, rather than an Array like their super class does though.