How would you best implement a Proxy Object?

Open Discussion: Lets say you have a User object which has multiple nicknames, and multiple profiles. But this User will only want to share one of each in any specific situation and not share the others. How would you go about it?

Here’s my idea on an implementation. Let me know what you think. This is an ActiveRecord example:

class ProxyUser
  def initialize(user_id:, nick_id:, profile_id:)
    @user_id = user_id
    @nick_id = nick_id
    @profile_id = profile_id
  end

  def profiles
    @profile ||= Profile.where(id: @profile_id)
  end

  def nicknames
    @nickname ||= Nickname.where(id: @nick_id)
  end

  def method_missing(mthd, *args, &block)
    (@user ||= User.where(id: @user_id).first).send(mthd, *args, &block)
  end
end

This lazily loads the ActiveRecord objects and memoizes them. I welcome all input, refactorings, and alternative implementations.

1 Like

I don’t think there’s anything wrong with this, but if I was code reviewing I might leave a few comments/questions like these:

I’m assuming you are leaving the profiles and nicknames plural because you want the proxy object to be used interchangeably with an actual user. Is this what we want to convey, or is this a special use case of a user that requires a particular business rule?

I tend to favor using one of the ruby libraries for delegation over just using method_missing, but I also prefer to decorate rather then do a full proxy. That way I’m explicitly specifying what the proxy exposes, rather then then just passing anything through.

In a Rails app, you may find yourself mixing some of the active model modules too. Again, depending on your tastes and preferences.

1 Like

Thanks!

Yes. I’m not sure what information will be needed in this use case. Only that the other identities the user owns should not leak out. So by design it should be the same user.

That’s a good idea. I was thinking of how Forwardable might apply in this but I didn’t see it quite fitting this situation. I will keep in mind the benefit of safely restricting what gets exposed for future practices.

1 Like

After sleeping on it, if it is critical that other identities not leak, then making this class completely interchangeable w/ the User model is a risk. It would be pretty easy to have a regression where an instance of the user model is dropped-in in place of the proxy and suddenly you’re leaking those identities.

2 Likes

Are the nicknames and profiles interchangeable too? If not, why not just let a user have multiple profiles (linked to a specific nickname) and then just choose who to share each profile with?

No. In this case a nickname is a higher level tag like a twitter handle. It’s a primary contact reference. Each nickname can own multiple profiles, but not the other way around. Lets say some one wants to host one event with a professional identity and another with a surfer persona. Those you do not want to mix up.

People take their identity as highly important so I would say yes.

There’s nothing wrong with this @danielpclark. The only observation I’d make would be that I’d rather hide any business logic away from the proxy, SRP and all that, so the #profiles and #nicknames methods should belong to the real object and not the proxy, AFAIC. I’d probably do something like that:

class User
  PROFILES = {a: 'Work', b: 'Home', c: 'Leisure'}
  NICKNAMES = {john: 'Crusher', nick: 'Bruiser', mark: 'Loser'}

  def initialize(user_id, nick_id, profile_id)
    @user_id = user_id; @nick_id = nick_id; @profile_id = profile_id
  end

  def get_profile
    PROFILES[@profile_id]
  end
  def get_nickname
    NICKNAMES[@nick_id]
  end

end


class ProxyUser
  def initialize(user_id, nick_id, profile_id)
    @user_id = user_id; @nick_id = nick_id; @profile_id = profile_id
  end

  def method_missing(mthd, *args, &block)
    User.new(@user_id, @nick_id, @profile_id ).send(mthd, *args, &block) if User.instance_methods.include? mthd
  end
end

I know it’s more code but you have a User object that’s easier to maintain and your Proxy doesn’t expose any implementation details.

1 Like

Interesting. Your usage of User.new in method_missing… each method call will generate a new user instance, perform the method, and then let the garbage collector deal with it. Is that ideal? The same user could be instantiated multiple times before the garbage collector kicks in. I’m just curious. (I generally don’t worry about GC, but perhaps I should)

The get_ methods are a nice touch.

1 Like

I don’t worry about the GC either. If I have an object that’s expensive to create, e.g. requires a db connection, then I’ll cache it.

class User
  PROFILES = {a: 'Work', b: 'Home', c: 'Leisure'}
  NICKNAMES = {john: 'Crusher', nick: 'Bruiser', mark: 'Loser'}
  USER_CACHE = {}

  def initialize(user_id, nick_id, profile_id)
  	if USER_CACHE[user_id] 
  		# get cached user 
  	else
    	  #get user from db and add them to cache
    end
  end
...........

I’m using a Hash here for simplicity, but obviously something like Redis would be ideal in this case. I suppose the only difference with what you’ve done is that I’d keep all the User-related logic in the User object and let the Proxy only worry about delegation.

2 Likes