The problem Link to heading
Recently I’ve seen in a project I work on a lot of occurrences of this code:
if user.privacy && user.privacy.enables_page?(...)
The first part of the condition above is a bad practice in object oriented design. It forces collaborators of user
to know a part of its implementation - it could have a privacy
or it couldn’t.
What we want Link to heading
Wouldn’t it be much better to just write this:
if user.privacy.enables_page?(...)
Hiding the responsibility inside the user class? It would be much cleaner and follow the Tell, don’t ask principle.
How to get there Link to heading
There are many ways to achieve this behaviour, but most of them will be based on The null object pattern. We want user.privacy
to return an object which responds falsey to all the method of the original Privacy
class.
First solution Link to heading
A trivial implementation could look like this:
# null_privacy.rb
class NullPrivacy
def enables_page? any_page
false
end
end
But how we are going to tie this class to our User
class? Strategies may change depending on your persistence layer. A one-fits-all solution is this: build an abstraction layer around your user.privacy
relation.
# user.rb
has_one :_privacy
def privacy
_privacy || NullPrivacy.new
end
def privacy= privacy
self._privacy = privacy
end
What we’re doing here is renaming the original privacy
field to _privacy
, so that we won’t call it directly, and we’re building two accessor methods to use User#privacy
and User#privacy=
as usual.
Another solution Link to heading
If you want to keep things even simpler, you can model your Privacy
class so that a new instance of this class behaves exactly like a NullPrivacy
, and thus avoid the need of a NullPrivacy
. Just keep in mind that things could change in your code in the future, so nail this down with a test before proceeding to avoid nasty surprises in the future.
… and Mongoid Link to heading
Finally, if you are using Mongoid
and you can model your Privacy
class as described above, there’s a one-line solution:
# user.rb
has_one :privacy, autobuild: true