When you are accessing private, internal state of your object, do you prefer to use attribute accessors (getter and setter methods) or direct instance variable access? Consider this contrived example:
class LineItem
def initialize(item, quantity)
@item = item
@quantity = quantity
end
# Access our internal state via instance variables
def total
@item.price * @quantity
end
# Or via attr_readers
def total
item.price * quantity
end
#... which would require something like this:
private
attr_reader :item, :quantity
end
Which would you prefer? In the simplest cases, the two are functionally equivalent, so does it matter? Is there a point where your preference for one over the other changes? What are the pros and cons of each approach?
Weâll be discussing this at the thoughtbot Boston developer discussion tomorrow afternoon and will post our notes and any follow-up here. Please feel free to contribute your thoughts here at any time.
I use private accessor all the time. By sending a message instead of using data directly, thatâs one less coupling you have, for now the data structure can change (for instance an hash keys, or an hash to an array) and you only have to rewrite the accessor and not all the methods that use it.
In our discussion today we were originally evenly split between people that generally use attribute accessors for private state and those that use instance variables directly.
** Attribute Accessors: **
PRO: Uniform access. Refactor to the method (add memoization, etc) is done in a single place.
PRO: Typo protection. Typos of a method give a more obvious error (and are guaranteed to error). Typos of an instance variable donât error. You may get a test failure, but the error wonât be as obvious.
PRO: Accidental assignment is more difficult (only possibly if you also have a private attr_writer as well).
CON: Using attr_reader in private scope warns when running ruby with warnings (ruby -w). Can either live with it (who runs with -w, anyway?), write the reader by hand, or use attr_extras.
** Instance Variables: **
PRO: Makes state dependency obvious at a glance (syntax highlighting)
PRO: You know immediately that accessing an instance variable is not expensive (no db read, etc).
CON: If you need to introduce a reader at a later date, the refactor is simple, but much wider (file, possibly any included modules)
We spent some time wondering why it is that ruby would opt to silently return nil when a previously undefined attribute is accessed. If a warning or error was raised youâd at least be alerted to typos.
We also discussed the pros and cons of the attr_* macros. These really apply to all uses - be they private or public. That is, attr_* yield methods that are difficult to grep for and are not understood by ctags.
As a group, we couldnât come to a consensus on which practice should be recommended. No one was particularly swayed from their original position, though one person did say they would probably give attribute readers a try.
The one thing we definitely did agree on was that consistency should be valued above all in this case. Donât use an attribute reader for some private state but instance variables for other private state in the same class. Be consisten in a class and try to be consistent across a project if thatâs feasible.
One wrinkle we didnât discuss as it relates to the point of consistency⊠Letâs say Iâve been consistently using instance variable access for private state in my class but then need to refactor to a private getter for one bit of state. The consistency guideline we all thought was a good idea now dictates that we change the access method of any other private state as well. Is this really what we want?
To me, this is a further argument for attribute readers at all times (the camp I was originally in and remain in). What do you guys think?
Could you give a small code example of an instance variable that would need to be refactored to a private getter?
I tend to use instance variables because I like seeing when Iâm accessing an objectâs state rather than calling a method. As for the refactoring argument, in JoĂ«lâs example above, if the item method had to be changed to create a new item or return the first item from an order object, I would prefer to change the name of that method to something more descriptive and refactor anyway.
@joannecheng What if an item is a hash that the class receives on instanciation, and for some reason it does not know or care about, one of the hashâs key gets renamed. Youâll have to go through all the methods of the class and change it, because you have a coupling to the data structure.
Consider some fictional payment processor that we interface with that to charge or refund credit cards. Weâve wrapped a class around this to isolate that external dependency:
class Payment
def initialize(card, amount)
@card = card
@amount = amount
end
def charge
Processor.charge(@card, @amount)
end
def refund
Processor.refund(@card, @amount)
end
end
The interface changes one day to support charging in various currencies. It now requires that you pass a Money object so it knows what currency youâre charging in. But for our case, this is always USD and we donât want to burden the rest of our app with having to pass amounts around as something other than an integer. SoâŠ
class Payment
CURRENCY = 'USD'
def initialize(card, amount)
@card = card
@amount = amount
end
def charge
Processor.charge(card, amount)
end
def refund
Processor.refund(card, amount)
end
private
attr_reader :card
def amount
Money.new(@amount, CURRENCY)
end
end
You could create a new method amount_as_money and use that in charge and refund. Had you been using attr_readers all along youâd simply change attr_reader :amount to the method definition and youâd be done. In this case you could also do the conversion in the initializer, but there are times when this isnât feasible.
I think wrapping instance variables is simply a micro-level example of depending upon abstractions. In the above example, we probably wouldnât directly access a Stripe class, but instead access PaymentProcessor class. If we treat the internal data or state like an object rather than data, our system becomes a little more flexible.
Even though I prefer this method, Iâm not really sure if the wins are big enough either way.
Thanks for the example, @derekprior. I can see how this would break consistency across a project.
@zamith If I were accessing the value of a hash via key more than twice in my class, I would have created a private method to do so. Itâs different from creating a private getter method.
@joannecheng Point taken. I guess that if you create a private method anytime your data is more complex than an integer, string, etc, youâll probably get the same benefits of having the reader on the first place.
You might argue that youâd name it better, but youâd also have to be very disciplined with work refactor, so that no data coupling creeps in to the class.
Maybe having private reader for everything is using a sledgehammer on a nail, but it does get the job done for me.
Another benefit is that youâre sending messages all the time which is nice in an OO environment.
I know right away that Iâm using the instance variable @item as opposed to guessing wether item method is doing something extra.
The argument is that readers are easier to re-factor is YAGNI, in my opinion. Iâve hardly ever seen that strategy being taken advantage of.
As for consistency, this is the same as our stance on single vs double quotes for strings. The argument there is that if you see double quotes you know something interesting is happening (e.g. interpolation, special characters). Say, for consistency, we decided to use instance variables, then if I encounter an item method, I know that something interesting is happening, and itâs not just a getter for the instance variable and I should probably go and look at that method. If we always used readers then itâs very easy to assume they all are just readers, and not a method that does something extra.
Iâd agree with Greg, primarily for the reason that itâs very easy to grok unfamiliar code using the @instance_variable syntax and then knowing that Iâd want to consider that a method is probably doing something, if just memoizing the value.