Classes and Objects

16.1 Defining a Class

class BankAccount
  attr_reader :owner, :balance        # getters
  attr_accessor :nickname             # getter + setter
  # attr_writer :pin                  # setter only

  MINIMUM_BALANCE = 0

  @@accounts_created = 0              # class variable (prefer class-level instance variables)

  def initialize(owner, balance = 0)  # constructor
    @owner = owner
    @balance = balance
    @transactions = []
    @@accounts_created += 1
  end

  def deposit(amount)
    raise ArgumentError, "Amount must be positive" unless amount.positive?
    @balance += amount
    @transactions << [:deposit, amount]
    self                              # return self to allow chaining
  end

  def withdraw(amount)
    return false if amount > @balance
    @balance -= amount
    @transactions << [:withdraw, amount]
    true
  end

  def to_s = "#{owner}: $#{format('%.2f', balance)}"
  def inspect = "#<BankAccount owner=#{owner.inspect} balance=#{balance}>"

  def self.accounts_created = @@accounts_created     # class method
end

acc = BankAccount.new("Ana", 100)
acc.deposit(50).deposit(25)
puts acc                     # uses to_s
p acc                        # uses inspect
acc.nickname = "Savings"
Ba
…

16.2 self and Class Methods

class Temperature
  attr_reader :celsius

  def initialize(celsius) = @celsius = celsius

  def self.from_fahrenheit(f) = new((f - 32) * 5.0 / 9)   # factory method

  class << self                        # open the singleton class: several class methods
    def freezing = new(0)
    def boiling = new(100)
  end

  def fahrenheit = celsius * 9.0 / 5 + 32
  def warmer_than?(other) = celsius > other.celsius
end

Temperature.from_fahrenheit(212).celsius
Temperature.boiling.warmer_than?(Temperature.freezing)

16.3 Class-Level Instance Variables (Preferred over @@)

@@vars are shared across the whole subclass tree, which surprises people. Class-level instance variables belong to each class separately.

class Widget
  @count = 0
  class << self
    attr_accessor :count
  end
  def initialize = self.class.count += 1
end

16.4 Object Copying and Freezing

copy = acc.dup         # shallow copy (not frozen, no singleton methods)
clone = acc.clone      # shallow copy keeping the frozen state and singleton methods
acc.freeze; acc.frozen?
def initialize_copy(source)    # customize dup/clone (deep copy fields)
  super
  @transactions = source.instance_variable_get(:@transactions).dup
end
deep = Marshal.load(Marshal.dump(obj))   # deep copy trick (only for marshalable objects)

16.5 Open Classes (Monkey Patching)

Reopening core classes is powerful but risky. Prefer refinements (Module 26) or separate helper modules.

class String
  def shout = upcase + "!"
end
"hi".shout     # "HI!"