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.
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.
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.
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.
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.
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)
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.