Reputation: 5178
The delegate
method seems like a great alternative to doing nil checks all over the place in rails apps, but I am having trouble applying it to situations where I have more than one association.
Consider these associations:
#app/models/user.rb
class User < ActiveRecord::Base
belongs_to :blog
end
#app/models/blog.rb
class Blog < ActiveRecord::Base
belongs_to :hash_tag
has_one :user
end
#app/models/hash_tag.rb
class HashTag < ActiveRecord::Base
has_one :blog
end
I grab a user:
@user = User.find(1)
And I want to find his blog:
@user.blog
=> nil
It returns nil here because this user
happens to have no associated blog
, so the following code would break the application if I did something like this for this user
:
@user.blog.title
=> undefined method `title' for nil:NilClass
So I could do this:
@user.blog.title if @user.blog.present?
But this is a nil check, and we want to avoid nil checks because otherwise they will be absolutely everywhere in the app.
So you can do this which applies the law of demeter and works great:
# open up app/models/user.rb
class User < ActiveRecord::Base
belongs_to :blog
delegate :title, to: :blog, prefix: true, allow_nil: true #add this
end
And now we can do this, which is great because if the user
does not have a blog
then nil
is just returned as opposed to that error: undefined method 'title' for nil:NilClass
:
@user = User.find(1)
@user.blog_title
Great, that works and we avoided the nil check. But what if we want to grab the associated hash_tag.tag_name
? If nil was not an issue we could do this:
@user = User.find(1)
@user.blog_title.hash_tag.tag_name
Not only would this break the law of demeter, but because nil is an issue and the associated hash_tag object may not exist, we again will one day run into the error: undefined method 'title' for nil:NilClass
.
I attempted to again open up the User
class and add in a nested target for a delegate, but it was not working for me:
# again: open up app/models/user.rb
class User < ActiveRecord::Base
belongs_to :blog
delegate :title, to: :blog, prefix: true, allow_nil: true
delegate :tag_name, to: :'blog.hash_tag', prefix: true, allow_nil: true #doesn't work
end
Apparently my syntax is off because it does not like my specifying this nested target as: :'blog.hash_tag'
.
What I want to do is to be able to say: @user.blog_hash_tag_tag_name
. Is this possible with delegate?
I did review the Delegate documentation. I did not see it mentioning more than one association as is my current situation.
Upvotes: 3
Views: 5222
Reputation: 688
As a personal opinion, @user.blog_hash_tag_tag_name
is horrible to look at.
That said, wanting to define both delegates at the user level is also a violation of LoD because you are using knowledge of the inner workings of blog (the fact that it belongs to a hash_tag) from the user class.
If you want to use delegates, you should add to class User
delegate :hash_tag_tag_name, to: :blog, prefix: true, allow_nil: true
and to class Blog
delegate :tag_name, to: :hash_tag, prefix: true, allow_nil: true
Upvotes: 10
Reputation: 7482
I would recommend you to use Null Object Pattern, which was very nicely implemented in https://github.com/avdi/naught gem (check its documentation, it rocks!) and his ActiveRecord counterpart https://github.com/Originate/active_null.
Upvotes: 1