Null Object Pattern for associations in Rails

2019-04-09 07:02发布

问题:

Despite looking at a few answers here regarding Null Objects in rails, I can't seem to get them to work.

class User < ActiveRecord::Base
  has_one :profile
  accepts_nested_attributes_for :profile

  def profile
    self.profile || NullProfile #I have also tried
    @profile || NullProfile #but it didn't work either
  end
end

class NullProfile
  def display #this method exists on the real Profile class
    ""
  end
end

class UsersController < ApplicationController
  def create
    User.new(params)
  end
end

My problem is that on User creation, I pass in the proper nested attributes (profile_attributes) for the Profile and I end up with a NullProfile on my new User.

I am guessing that this means that my custom profile method is getting called on create and returning a NullProfile. How do I do this NullObject properly so that this only happens on read and not on the initial creation of the objects.

回答1:

I was going exactly through and I wanted a clean new object if it wasn't present(if you're doing this just so object.display doesn't err maybe object.try(:display) is better) this too and this is what I found:

1: alias/alias_method_chain

def profile_with_no_nill
  profile_without_no_nill || NullProfile
end
alias_method_chain :profile, :no_nill

But since alias_method_chain is being deprecated, if you're staying on the edge you would have to do the pattern by yourself manually... The answer here seems to provide the better and more elegant solution

2(Simplified/practical version from the answer):

class User < ActiveRecord::Base
  has_one :profile
  accepts_nested_attributes_for :profile

  module ProfileNullObject
    def profile
      super || NullProfile
    end
  end
  include ProfileNullObject
end

note: The order you do this matter(explained in the linked answer)


On what you tried:

When you did

def profile
  @profile || NullProfile
end

It won't behave as expected because the Association is lazily loaded(unless you told it to :include it in the search), so @profile is nil, that's why you're always getting NullProfile

def profile
  self.profile || NullProfile
end

It will fail because the method is calling itself, so it's sort like a recursive method, you get SystemStackError: stack level too deep



回答2:

Instead of using alias_method_chain, use this:

def profile
  self[:profile] || NullProfile.new
end


回答3:

I've found a simpler option than including a private module in the accepted answer.

You can override the reader method and fetch the associated object using the association method from ActiveRecord.

class User < ApplicationRecord
  has_one :profile

  def profile
    association(:profile).load_target || NullProfile
  end
end # class User