How do you let Ruby Salary objects compare amounts in == without making amount public, and why does private fail there?
answer
- peer access, not subclass access
- protected :amount
- other.amount is an explicit receiver
- respond_to? hides protected methods
- protected calls skip the inline cache
basics
~10 sDeclare 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 sComparison 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 linesclass 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) # => falsego deeper
Recall that private blocks other.amount even within the same class, and that protected is what allows it.
Explain the protected check on the caller's self, why the type guard is needed in == and <=>, and how respond_to? treats protected methods.
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.
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