Classes: values that carry behaviour
A blueprint with state inside: initialize, instance variables, the attribute shortcuts, to_s, and inheritance with super.
A blueprint, and the object it makes
A class is a blueprint: class Account ... end describes what an account is and what it can do. Account.new('Ada', 100) runs the initialize method to build one object, and the values stored with an at-sign — @owner, @balance — belong to that object alone. Two accounts built from the same class cannot see each other's balance.
Instance variables are private by their nature: the outside world reaches them through methods, and the short way to write those methods is the attribute family. attr_reader :owner, :balance creates reader methods; attr_accessor creates a reader and a writer; attr_writer only the writer. Each is shorthand for a small method you would otherwise type by hand — which is worth knowing, because that is all an attribute is.
Behaviour, and to_s
The interesting part of a class is not the data but the handful of things it is allowed to do with it. deposit changes the balance only through a decision — refusing a negative amount, for instance — and every caller in the program gets that decision for free. This is the real benefit of a class: it is a place where a rule can live once.
Every object has a to_s, and the default one is unhelpful. Redefining it makes an object printable: "#{account}" and puts account both go through to_s, so a well-chosen one turns debugging output from #<Account:0x00007f...> into Ada: 120.00. self inside a method is the object the method was called on, which is how one Account can be compared with another.
Inheritance and super
class Savings < Account says that a Savings is an Account with additions — it inherits every method and may override some. When the child overrides initialize, it usually calls super first: the parent builds the parts it knows about, and the child adds its own. is_a?(Account) answers true for a Savings, which is exactly the promise the inheritance makes.
private marks methods that are internals: callable from the inside, not by anyone holding the object. Ruby's privacy is a signal to the reader more than a wall for an attacker — Ruby is a language of consenting adults — but it is a strong signal, and using it is how a class says which parts are the interface and which are the plumbing.
Objects, messages, and lookup
An object is a bundle of instance variables plus a pointer to its class. account.deposit(50) sends the message deposit to that object; Ruby looks for the method in the object's class, then its ancestors, and runs the first definition it finds. That lookup is why attr_reader works and why a subclass can replace one method without touching the rest.
self is the object receiving the current message. Inside an instance method it is the object, which is how one account compares itself with another; inside def self.build it is the class, which is how a factory method is written. Instance variables belong to whatever self is, so @balance in two accounts is two separate values.
State belongs to the object
An instance variable is created the first time a method assigns it, and it lives as long as the object. Two objects from one class have separate copies, which is why one thermostat can be warm while another is cold. The class holds the methods; the objects hold the values.
Reading an instance variable that was never assigned gives nil rather than an error. That silence is convenient during construction and dangerous afterwards: a typo in @balance produces a missing value, and the failure appears where the value is used, not where it was misspelled.
An unassigned instance variable reads as nil, so a typo in @balance can turn into a missing value far from the mistake. attr_accessor hands the outside world a writer, which lets it bypass every rule the class was protecting — use attr_reader unless a writer is genuinely part of the interface. And a class variable written @@count is shared by the class and all its subclasses, which is rarely what a per-class count means.
An account, and a savings account
class Account
attr_reader :owner, :balance
def initialize(owner, balance = 0)
@owner = owner
@balance = balance
end
def deposit(amount)
return false unless amount.positive?
@balance += amount
true
end
def withdraw(amount)
return false if amount > @balance
@balance -= amount
true
end
def to_s
"#{@owner}: #{format('%.2f', @balance)}"
end
private
def audit_line
"balance now #{@balance}"
end
end
class Savings < Account
def initialize(owner, balance, rate)
super(owner, balance)
@rate = rate
end
def add_interest
@balance += @balance * @rate
end
end
account = Account.new('Ada', 100)
account.deposit(50)
account.withdraw(30)
puts account
puts account.balance
savings = Savings.new('Grace', 1000, 0.05)
savings.add_interest
puts savings
puts savings.is_a?(Account)A thermostat
Run it and watch the state change. Then add a method that reports whether the current temperature is within a target range, and a second thermostat to prove the two objects keep their own state.
An instance variable that was never assigned reads as nil rather than raising, so a misspelled @blance turns a balance into a missing value somewhere far from the typo. Naming the variable in one place — the reader method — keeps the spelling honest.
Try these next
- Add a
transfer_to(other, amount)method that withdraws here and deposits there, refusing if the balance is short. - Give an account a
to_sthat shows the owner and the balance with two decimals. - Write a class method that builds a zero-balance account from a name, and use it.
A ticket that can be used once
Run it and watch the status change. Then add a `refund` method that makes a used ticket valid again, and a class method that builds a free child ticket.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
When does initialize run?
