1
votes

I'm trying to integrate Redis into a Rails app to replace "has_many through" relations. I'd like to do that seamlessly so we don't have to change the code through out the app. My idea is to override different methods of some class attributes (the followers attribute of the class Speaker for example) to be able to create custom behavior when using them: Here are behaviors I'd like to get to:

s = Speaker.new
s.followers # calls custom getter and returns [User1, User2]
s.followers << User.create
s.followers # calls custom getter and returns [User1, User2, User3]

Here is my idea inspired by Overriding instance variable array's operators in Ruby

class Speaker < ActiveRecord::Base  
  attr_accessor :followers

  def initialize
    super
    @followers = []
    class << @followers
      def <<(u)
        puts "setter #{u.id}" 
        $redis.set "speaker#{self.id}followers", u.id
        super(u.id)
      end
    end
  end

  def followers
    puts "getter"
    user_ids = $redis.get "speaker#{self.id}followers"
    User.find_all user_ids
  end

end

The problem is that the implementation of the followers getter override the implementation of "def <<(val)"

if the getter "def followers" is not defined:

s.followers
# []
s.followers << User.create 
# "setter 1"
# [1]
s.followers
# [1]
s.followers << User.create 
# "setter 2"
# [1, 2]
s.followers
# [1, 2]

if the getter "def attendees" is defined:

s.followers << User.create
# ["My", "Custom", "Array", User1]
s.followers
# ["My", "Custom", "Array"]
s.followers << User.create
# ["My", "Custom", "Array", User2]
s.followers
# ["My", "Custom", "Array"]

How could I get the getter and the setter "<<" to work together?

2

2 Answers

0
votes
def followers<<(val)

That's not going to work. The reason is that:

foo.followers << "abc"
# is actually 2 methods!
foo.followers().<<("abc")

So #followers needs to return an object of a class that has an overridden #<< method.


Much in the way that a rails association returns an association proxy object.

# Book has_many Pages
book.pages # an assoc proxy
book.pages << Page.new # calls #<< on the assoc proxy

Book#pages<< doesn't exist. Book#pages returns an instance of ActiveRecord::HasManyAssociationProxy (or something similar), which then implements the #<< instance method.

0
votes

Your problem here is that the getter is returning a new array. You modify the singleton class of the @followers array, but that is not being used in the getter:

def followers
  puts 'getter'
  ['a','new','array']
end

If you want to have a custom getter, then you need to make sure that the getter returns @followers (without changing the underlying reference), or you need to re-decorate the array.

However, what AlexWayne suggested is the proper way to do this. Return a proxy object that handles the redis details:

class FollowersList < SimpleDelegator
  def initialize(assoc)
    @assoc = assoc
    super(_followers)
  end

  def _reload
    __setobj__ _followers
    self
  end

  def _followers
    user_ids = $redis.get key
    User.find_all user_ids
  end

  def _key
    "speaker#{@assoc.id}followers"
  end

  # implement your overrides. The _reload method is to force the list to sync
  # with redis again, there are other ways to do this that wouldn't do the query
  # again
  def <<(val)
    $redis.lpush key, val.id
    _reload
  end

  #etc
end