Why I use protected attr_reader in Ruby
For a while I’ve been using a pattern of declaring protected
attr_reader
s.
class Foo
def initialize(bar)
@bar = bar
end
protected
attr_reader :bar
end
There are three main reasons for this:
public
accessors are badattr_reader
has better error handling than instance variablesprotected
makes equality comparison easier
Why not use public
accessors?
It’s common to see attr_*
methods defined in the public scope of a class.
Much has been written about the problems of getters and setters, so I won’t go over this in too much detail.
The gist is that an a well designed class is composed of one or more encapsulated objects.
Encapsulation is used to hide the values or state of a structured data object inside a class.
A good object should be a black box with a clearly articulated set of behaviours – the public API. When we expose the encapsulated objects to the outside world, we allow the boundaries of our system to blur and become tangled.
Public getters (attr_reader
) and setters (attr_writer
) cause different problems.
Public getters violate the principle of “Tell, Don’t Ask”.
Allowing users of the object to access its attributes encourages the calling code to get complicated.
# BAD
class Post
attr_reader :title, :status
def initialize(title:, status:)
@title = title
@status = status
end
end
posts = [
Post.new(title: 'A draft post', status: 'draft'),
Post.new(title: 'A published post', status: 'published')
]
# Somewhere where we're listing all posts.
# Here we're reaching into post and operating on its encapsulated members.
posts.each do |post|
puts "[#{ post.status.upcase }] #{ post.title }"
end
If we remove the attr_reader
s from public visibility, we’re forced to define behaviour on the class.
If we change our minds about what we want to display when a post is printed, we can just change the implementation details in Post#print
, rather than having to search around the application for places where we’ve asked for all the attributes we need for printing posts.
# GOOD
class Post
def initialize(title:, status:)
@title = title
@status = status
end
def print
puts "[#{ status.upcase }] #{ title }"
end
private
attr_reader :title, :status
end
posts = [
Post.new(title: 'A draft post', status: 'draft'),
Post.new(title: 'A published post', status: 'published')
]
# Somewhere where we're listing all posts.
# Now we're telling the post to print itself.
posts.each(&:print)
Public setters, in my opinion, are even worse. Similar to the problem above, they encourage users of the object to define behaviour outside of the class.
In this case, we're defining what it means to "publish" a post outside of Post
.
# BAD
class Post
attr_reader :title
attr_accessor :status
def initialize(title:, status:)
@title = title
@status = status
end
end
posts = Post.new(title: 'A draft post', status: 'draft')
# Somewhere where we're publishing a post.
# This is likely defined in a controller where a user has interacted with the UI
# to publish their post – quite distant from the "Post" class.
post.status = 'published'
Again, we want behaviour relating to the post
defined in the Post
class.
Here we avoid allowing calling code to mutate the post
instance directly.
# GOOD
class Post
def initialize(title:, status:)
@title = title
@status = status
end
def publish
@status = 'published'
end
private
attr_reader :title, :status
end
posts = Post.new(title: 'A draft post', status: 'draft')
# Somewhere where we're publishing a post.
# Post now controls what it means to publish a post.
post.publish
Public setters have a tendency to cause calling code to become extremely complex as requirements expand.
Imagine our publishing workflow as we add features.
Now we want to record who and when the post was published.
# BAD
# Using public setters encourages lots of complexity in our
# PublishingController.
class PublishingController
def publish_post(post)
post.status = 'published'
post.published_at = Time.now
post.published_by = editor
end
end
# GOOD
class Editor
def publish(post)
post.publsh(self)
end
end
class Post
def publish(published_by)
@status = 'published'
@published_at = Time.now
@published_by = published_by
end
end
# Now we've defined the behaviour in the relevant objects, we're simply telling
# the editor to publish the post.
class PublishingController
def publish_post(post)
editor.publish(post)
end
end
Error handling of methods is better
We could avoid using attr_reader
altogether and directly call the instance variables.
Instance variables default to nil
, which can be misleading if you make a mistake when typing the instance variable.
class Post
def initialize(title:, status:)
@title = title
@status = status
end
def published?
# Oops, typo
@staaaaaatus == 'published'
end
end
post = Post.new(title: 'A post', status: 'published')
post.published?
# => false
attr_reader
s define methods, so making a mistake is much more obvious.
When you call an undefined method a NameError
is raised, making it much easier to figure out that you've made a mistake.
class Post
def initialize(title:, status:)
@title = title
@status = status
end
def published?
# Oops, typo
staaaaaatus == 'published'
end
private
attr_reader :status
end
post = Post.new(title: 'A post', status: 'published')
post.published?
# => NameError (undefined local variable or method `staaaaaatus' for
#<Post:0x00007fb4248bff58 @title="A post", @status="published">)
Equality comparison
Up until now I’ve been using private
for the examples, but this post is all about why I use protected
.
One case where you do want access to the attributes of an object are when comparing whether two objects are the same.
def ==(other)
title == other.title &&
status == other.status
end
If we were to use private
we'd get a NoMethodError
. when trying to call other.title
.
class Post
def initialize(title:, status:)
@title = title
@status = status
end
def ==(other)
title == other.title &&
status == other.status
end
private
attr_reader :title, :status
end
post_1 = Post.new(title: 'A post', status: 'published')
post_2 = Post.new(title: 'A post', status: 'published')
post_1 == post_2
# NoMethodError (private method `title' called for
#<Post:0x00007fdf0b09f280 @title="A post", @status="published">)
Protected methods are a little different from private methods. In addition to being able to call methods with an implicit receiver, you can call a protected method with an explicit receiver as long as this receiver is self
or an object of the same class as self
.
By using protected
we keep our encapsulated objects hidden from public tampering, but allow access when we compare instances of the same class.
class Post
def initialize(title:, status:)
@title = title
@status = status
end
def ==(other)
title == other.title &&
status == other.status
end
protected
attr_reader :title, :status
end
post_1 = Post.new(title: 'A post', status: 'published')
post_2 = Post.new(title: 'A post', status: 'published')
post_1 == post_2
# true
post_1.status
# NoMethodError (protected method `status' called for
#<Post:0x00007fdfb7189428 @title="A post", @status="published">)
This approach has been working well for me 90% of the time. There are a couple of downsides to be aware of though.
According to rubyguides.com, “A protected method is slow because it can’t use inline cache.” In 2018 this resulted in “a difference of 8.5% in performance”.
The other downside is that you can't compare duck-types of different classes in this way, since protected
checks class.
class Blog
def initialize(title:, status:)
@title = title
@status = status
end
protected
attr_reader :title, :status
end
blog = Blog.new(title: 'A post', status: 'published')
post_1 == blog
# NoMethodError (protected method `title' called for
#<Post:0x00007fdfb7189428 @title="A post", @status="published">)
It’s pretty rare that either of these downsides has caused a problem for me, and any pain has been far outweighed by cleaner code resulting from avoiding public getters and setters.