30. Designing a small class

You can build classes, make instances, and inherit. This last chapter of Part 6 is not about new syntax — it is about taste: designing a class that is pleasant and safe to use. The big idea, encapsulation, is simpler than it sounds.

Encapsulation: let the methods guard the data

Encapsulation means keeping an object's data valid by changing it only through its own methods. The outside world asks the object to do things; it never reaches in to scramble the fields.

Here is a BankAccount. The balance should never go negative, and you cannot deposit a negative amount. The methods enforce those rules:

local Account = {}
Account.__index = Account

function Account.new(owner)
    return setmetatable({ owner = owner, balance = 0 }, Account)
end

function Account:deposit(amount)
    if amount > 0 then
        self.balance = self.balance + amount
    end
end

function Account:withdraw(amount)
    if amount > 0 and amount <= self.balance then
        self.balance = self.balance - amount
        return true        -- success
    end
    return false           -- not enough money, or a silly amount
end

function Account:getBalance()
    return self.balance
end

Using it:

local acc = Account.new("Keiko")
acc:deposit(100)
acc:withdraw(30)
print(acc:getBalance())     -- 70

print(acc:withdraw(1000))   -- false  (refused: not enough)
print(acc:getBalance())     -- 70     (unchanged)

The account can never reach an impossible state: every change goes through a method that checks first. If outside code could write acc.balance = -500 directly, that guarantee is gone. So the deal is: talk to the object through its methods.

A clean public surface

Think of a class as having two sides:

  • The inside — its fields and helper logic. Details that may change.
  • The outside — the methods other code calls. A small, clear set of actions: deposit, withdraw, getBalance.

A good class keeps the outside small and obvious. A user of your Account needs only three verbs; they never need to know the balance lives in a field called balance. Store it in cents tomorrow, and as long as the three methods behave the same, nothing else breaks.

Name methods as actions

Methods do things, so name them with verbs: deposit, withdraw, move, attack, reset. Fields hold things, so name them with nouns: balance, name, hp. acc:withdraw(30) should read like an instruction — it is one.

Open exercises/30/01-account.lua. It has the BankAccount class. Add a :canAfford(amount) method that returns true if the balance is at least amount, without touching it. Use it before a withdraw.

Three questions for any class

When you design a class, ask:

  1. What does it know? → its fields (an account knows its balance).
  2. What can it do? → its methods (deposit, withdraw, check).
  3. What must always stay true? → the rules the methods protect (the balance is never negative).

Answer those three and the class almost writes itself.

Homework

Homework files: exercises/30/homework/.

Problem 1 — Counter class

Open exercises/30/homework/01-counter.lua. Design a Counter that starts at 0 with :increment(), :get(), and :reset(). The count must change only through those methods. Show it counting, reading, and resetting.

Problem 2 — Clamped health

Open exercises/30/homework/02-health.lua. Design a Health class with a max. :heal(n) raises health but never above max; :damage(n) lowers it but never below 0; :get() reads it. The rule to protect: health stays between 0 and max.

Problem 3 — Light switch

Open exercises/30/homework/03-switch.lua. Design a Switch that is on or off. :toggle() flips it, :isOn() reports its state. Start it off, toggle a few times, and print the state after each.

Challenge — Stack with a guard

Open exercises/30/homework/04-stack.lua. Design a Stack with :push(v), :pop(), and :size(). The rule to protect: :pop() on an empty stack must not crash — return nil instead. Show it working, including a pop on an empty stack.

Stuck or finished? Open the homework solutions page.