Menu
Coddy logo textTech

Private Setters

Part of the Object Oriented Programming section of Coddy's Ruby journey. Lesson 19 of 57.

Often a value should be readable by everyone but changeable only by the object. attr_reader gives the public reader, and private attr_writer a setter that only the object can use. This works because attr_writer returns the names of the methods it defines:

class Wallet
  attr_reader :balance

  def initialize
    self.balance = 0
  end

  def earn(amount)
    self.balance = balance + amount
  end

  private attr_writer :balance
end

w = Wallet.new
w.earn(30)
puts w.balance
begin
  w.balance = 1_000_000
rescue NoMethodError => e
  puts e.message
end

Output:

30
private method `balance=' called for an instance of Wallet

A private setter is called with self. inside the class, the one receiver Ruby accepts for private methods. Without self. the assignment would only create a local variable:

class Lamp
  attr_reader :on

  def initialize
    self.on = false
  end

  def toggle
    self.on = !on
    self
  end

  private

  attr_writer :on
end

puts Lamp.new.toggle.toggle.toggle.on

Output:

true

Going through a private setter instead of assigning @balance directly keeps one place for rules, such as a check that the value is valid:

class Tank
  attr_reader :litres

  def initialize(capacity)
    @capacity = capacity
    self.litres = 0
  end

  def fill(amount)
    self.litres = litres + amount
  end

  def drain(amount)
    self.litres = litres - amount
  end

  private

  def litres=(value)
    @litres = value.clamp(0, @capacity)
  end
end

t = Tank.new(50)
t.fill(80)
puts t.litres
t.drain(70)
puts t.litres

Output:

50
0

private attr_accessor :history hides both the reader and the setter, for internal state that outside code should not even see:

class Editor
  def initialize
    self.history = []
  end

  def type(text)
    history << text
  end

  private attr_accessor :history
end
challenge icon

Challenge

Easy

Complete the class Wallet. balance is readable from outside but set only through a private setter, starting at 0. earn(amount) adds to it. spend(amount) subtracts and returns true, or returns false without a change when the balance is too low. history returns the number of successful changes, also kept behind a private setter.

The supplied code reads commands +n or -n, printing balance 30 after each, or declined when a spend fails. Then it prints changes: 2 and tries wallet.balance = 1000000, printing cannot set balance when Ruby refuses.

Your code goes in wallet.rb. main.rb holds the supplied input/output code and cannot be edited.

Try it yourself

require_relative 'wallet'

# Supplied input/output code: keep it as it is
input = []
while (line = gets)
  input << line.chomp
end
wallet = Wallet.new
input.each do |cmd|
  amount = cmd[1..].to_i
  if cmd.start_with?("+")
    wallet.earn(amount)
    puts "balance #{wallet.balance}"
  elsif wallet.spend(amount)
    puts "balance #{wallet.balance}"
  else
    puts "declined"
  end
end
puts "changes: #{wallet.history}"
begin
  wallet.balance = 1_000_000
  puts "balance set"
rescue NoMethodError
  puts "cannot set balance"
end
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Online Ruby compiler