skip to content

How do you let Ruby Salary objects compare amounts in == without making amount public, and why does private fail there?

level: middleimportance: must knowfreq 62%

answer

  1. peer access, not subclass access
  2. protected :amount
  3. other.amount is an explicit receiver
  4. respond_to? hides protected methods
  5. protected calls skip the inline cache

basics

~10 s

Declare the reader protected: protected :amount. Inside Salary#==, other.amount is then allowed because the caller is a Salary, while outside code gets NoMethodError. A private amount fails because other.amount uses an explicit receiver.

solid answer

~40 s

Comparison needs to read the other object's state, so `==` writes `other.amount`. With `private :amount` that raises `NoMethodError`, because private forbids any explicit receiver except literal `self`. `protected :amount` is the fix: a protected method may be called on another object when the calling code's `self` is a kind of `Salary`, so `Salary#==` and `Salary#<=>` can read peers' amounts, while `salary.amount` from outside still raises. Guard the comparison with `other.is_a?(Salary)` first, otherwise comparing with an Integer raises `NoMethodError` instead of returning `false`. Protected methods are left out of `respond_to?` unless you pass `true`, and the `Module#protected` documentation notes they are slower to call than other methods because they cannot use the inline cache.

code

ruby · 19 lines
ruby
class Salary
  include Comparable

  def initialize(amount) = @amount = amount

  def <=>(other)
    return nil unless other.is_a?(Salary)
    amount <=> other.amount       # peer access allowed: protected
  end

  protected

  def amount = @amount
end

Salary.new(4_000) < Salary.new(5_000)   # => true
Salary.new(4_000).amount
# NoMethodError: protected method 'amount' called for an instance of Salary
Salary.new(4_000).respond_to?(:amount)  # => false

go deeper

for a junior

Recall that private blocks other.amount even within the same class, and that protected is what allows it.

for a middle

Explain the protected check on the caller's self, why the type guard is needed in == and <=>, and how respond_to? treats protected methods.

for a senior

Show judgment about exposure: protected only for genuine peer operations, awareness of its inline-cache cost in hot comparisons, and alternatives when even peers should not see the value.

for a principal

Frame protected as a narrow tool and set the team's expectation for when value objects expose state to peers versus comparing through dedicated methods.

## The problem A `Salary` value should not reveal its amount to arbitrary code, but two salaries still need to compare equal when their amounts match, and to sort. ```ruby class Salary def initialize(amount) = @amount = amount def ==(other) other.is_a?(Salary) && amount == other.amount end private def amount = @amount end Salary.new(5_000) == Salary.new(5_000) # NoMethodError: private method 'amount' called for an instance of Salary ``` The left side, `amount`, is fine: it has no explicit receiver. The right side, `other.amount`, is rejected. Ruby's `private` is **per object**: only the receiver-less form or the literal `self.` may call a private method, and the fact that `other` is also a `Salary` does not matter. ## The fix: protected Replace `private` with `protected` for the reader: ```ruby class Salary include Comparable def initialize(amount) = @amount = amount def <=>(other) other.is_a?(Salary) ? amount <=> other.amount : nil end protected def amount = @amount end ``` Now the rule that applies is protected's: 1. The call `other.amount` has an explicit receiver. 2. Ruby checks the object running the calling code, the `self` inside `<=>`. 3. That `self` is a kind of `Salary`, the class that defines `amount`, so the call is allowed. 4. Outside code, whose `self` is not a `Salary`, gets `NoMethodError: protected method 'amount' called for an instance of Salary`. Instances of a `Salary` subclass pass the same check, so a `Bonus < Salary` can compare with a plain `Salary`. ## Details that come up - **The type guard matters.** Without `other.is_a?(Salary)`, `salary == 5000` calls `5000.amount` and raises `NoMethodError`. Equality should answer `false`, and `<=>` should return `nil` for incomparable values. - **`respond_to?` ignores protected methods.** `salary.respond_to?(:amount)` returns `false`; `respond_to?(:amount, true)` includes private and protected methods and returns `true`. - **Performance.** The `Module#protected` documentation notes that protected methods are slower than others because they cannot use the inline cache. For a comparison called in a tight sort loop that can matter, and some teams compare an exposed, harmless value instead. - **Scope.** Protected methods are still visible to every object of the class family. If the value must not reach even peers, compare in a way that does not expose it at all, for example with a public method that returns only the comparison result. ## Alternatives to protected Protected is not the only design that keeps the amount out of public view: - **Expose a comparison, not the value.** A public `<=>` or `==` is itself the interface; callers learn the ordering without seeing the number. - **Expose a safe projection.** A public method returning a band or a rounded figure may be enough for callers, and peers can compare that. - **Compare a value object.** If the amount is wrapped in its own immutable object with public comparison, `Salary` can delegate to it and keep no protected methods at all. Each of these trades a little extra code for a clearer statement of what outside code may know. ## Choosing between the three | Need | Visibility | |---|---| | anyone may read the amount | `public` | | only the object itself uses it | `private` | | objects of the same family must read each other's value | `protected` | `protected` is rarely needed outside comparison, arithmetic between value objects and merging objects of one type; when it appears elsewhere, check whether a public method or a private one would say more clearly what is intended.

  • In Ruby, can a protected method defined in Salary be called on a Salary from inside an unrelated Report class?
    No. Ruby checks that the calling code's `self` is a kind of the class that defines the method. Inside `Report`, `self` is a `Report`, so `salary.amount` raises `NoMethodError` for a protected method, exactly as it would from the top level.
  • In Ruby, how do you check whether an object has a protected method, given that respond_to? returns false?
    Pass `true` as the second argument: `respond_to?(:amount, true)` includes private and protected methods. Class-level lists exist too: `Salary.protected_instance_methods` returns the protected methods, including inherited ones unless you pass `false`.

saying these in an interview costs you the question

  • Private methods can be called on other instances of the same class
  • Protected methods are callable from any code outside the class
  • respond_to?(:amount) returns true for a protected amount
  • Protected costs nothing extra at call time
  • Comparing with a non-Salary is safe without a type guard