So I use null objects for a couple of things in the application I’m working on, and it’s proven to be a useful pattern. But when I try to implement it for an object in a has_many relation I run into a problem: an empty relation returns an ActiveRecord::Associations::CollectionProxy, which is truthy also when empty. This causes the simple way of substituting a null object for a missing relation to fail:
class Plant < ApplicationRecord
has_many :leaves
def leaves
super || [Null::Leaf.new]
end
end
class Leaf < ApplicationRecord
end
class Null::Leaf
def number_of_leaflets
0
end
end
Many method calls on .leaves could return nil. For example, if there is only one leaf, then .leaves.second will return nil. .leaves[23] will often return nil as well.
Rather than try to override every method so it will return a null object, the best strategy may be to put such logic at the call site, so plant.leaves.first || Null::Leaf.new.
If there are many such sites, and the same method is involved in each case, I would create helper methods on Plant, for example .first_leaf, or .main_leaf.
Thanks, understood. But my case is a model where I need at least one - any more is optional, and would always be made up of actual records. The null object should only be there for otherwise empty associations. It wouldn’t make sense to call .second or any other method that targets a particular index on my object - apart from .first.
To extend my example, forget about leaves - instead imagine that a plant always belongs to a particular breed, but it could also be a cross-breed between two or more breeds. I would like to have a null breed that acts as a stand-in for any plant where the breed is not known. Seems a perfect match for a null object.
Fourth day with this blocker. May have resigned to overriding .first, which I found can be done by passing a block to has_many:
class Leaf < ApplicationRecord
...
has_many :leaves, dependent: :destroy do
def first
super || Null::Leaf.new
end
end
...
end
Now to see if this will allow me to do what I wanted, which is to provide a method on the null object that does a particular thing differently to how the regular object behaves.
To anyone who might be wondering how it went: I gave up. I’ve accepted that the null object pattern is only really a viable substitute for an individual object instance, and cannot (easily) be used as a stand in for an empty relation. Rails already provides its own null object for empty relations, which it’s best not to tamper with (<< won’t work on these, for example). Shame, because it would have been neat to have the option to include a custom null object in an otherwise empty relation - but I know better than to swim against the Rails current for long.
I would be hesitant to overwrite the relationship accessor to return something that is not an ActiveRecord::Relation object. Seems to violate the principle of least surprise. Maybe take inspiration from find_or_initialize_by? Something like:
def load_or_initialize_leaves
leaves.build if leaves.empty?
leaves
end
As my suggestion shows I might recommend instead of returning a special “null” object I would probably lean towards building an in-memory object. That way it’s still a relation you just have a pending object in that relation. If you really still want the null object maybe:
def load_or_stub_leaves
if leaves.empty?
[Null::Leaf.new]
else
leaves
end
end
I like that! The whole idea with having a null object in an otherwise empty relation is that I can call a particular method on it which returns a different result than a real object would. In my actual case it’s got to do with Pundit and determining whether a particular user can edit a particular plant; this is normally based on what “leaves” it has (which each leaf giving a particular group of users editing permission), but if it has none it should be editable by all such groups. Imagine a managed_by?(user) method on Leaf which returns true if it belongs to a user’s group, while the null leaf would respond true to the same method for any user in any group (and false for users that are not members of any group).
TBH I’m not even sure I’m describing this correctly - the actual code is much bigger than the example I used in this thread, and involves three levels of nesting instead of two. It’s… complicated. But I found a compromise solution that works without a null object, so I’ve moved on from this idea.