Scoped Struct

Ever write a ruby class and wish you could organize your methods into sub-objects? Take, for example, a class that represents an NFL player:

class Player
  def fumbles_dropped; end
  def fumbles_lost; end
  def fumbles_recovered; end
  def passes_attempted; end
  def passes_completed; end
  def passes_incomplete; end
  def pass_completion_percentage; end
end

But we’re ~Ruby Programmers~ and we write DRY code, right? Wouldn’t it be nicer if you could take all that repetition and eliminate it?

class Player
  scope :fumbles do
    def dropped; end
    def lost; end
    def recovered; end
  end
  scope :passes do
    def attempted; end
    def completed; end
    def incomplete; end
    def completion_percentage; end
  end
end

And then instead of those long ugly method names, you could just be all like,

my_player.passes.attempted
my_player.passes.completed
my_player.fumbles.recovered

Eez better, yes?

The methods you define in the scoping block, when called, run in the same context as if you defined them on the class itself. So you ccan have them referencing instance variables or other methods of the class as if they were local. Likewise, you can also reference other scoped methods from scoped methods, even from other scopes; just make sure to reference them by their full scoped path:

class Player
  def name
    "John Rambo"
  end
  scope :formatting do
    def loud_name
      name.upcase + '!!!'
    end
  end
  scope :actions do
    def scream_my_name
      puts "MY NAME IS #{formatting.loud_name}"
    end
    def scream_twice
      2.times{ actions.scream_my_name }
    end
  end
end
Player.new.actions.scream_twice # prints "MY NAME IS JOHN RAMBO!!!" twice

And that’s not all! The object returned by the scoping method, you can pass it around and it will still act the way you’d expect:

fumble_records = my_players.collect{|player| player.fumbles}
fumble_records.first.dropped

And if you want even more organizational fun, you can stick your methods in modules and then include them in their own scoping blocks:

module Passes
  def attempted; end
end
module Fumbles
  def dropped; end
end
class Player
  scope :passes do
    include Passes
  end
  scope :fumbles do
    include Fumbles
  end
end

Troubleshooting

One thing that will cause weirdness is if you name a scoped method the same thing as one of your base instance methods. Rename it to something else and all will be well.

Credits

By Mike Ferrier (www.mikeferrier.ca)

Contributions by Hampton Catlin (www.hamptoncatlin.com)