27. OOP sederhana dengan metatable — Solusi PR

File solusi .lua ada di exercises/27/homework/solutions/.

Soal 1 — Class Point dengan move

Soal. Class Point dengan .new, :distance, dan :move.

Cara memikirkannya. Mulai dari class Point di bab ini. Tambahkan satu method, :move(dx, dy), yang mengubah nilai self.x dan self.y.

Solusi lengkap.

local Point = {}
Point.__index = Point

function Point.new(x, y)
    local self = setmetatable({}, Point)
    self.x = x
    self.y = y
    return self
end

function Point:distance(other)
    local dx = self.x - other.x
    local dy = self.y - other.y
    return math.sqrt(dx * dx + dy * dy)
end

function Point:move(dx, dy)
    self.x = self.x + dx
    self.y = self.y + dy
end

local a = Point.new(0, 0)
local b = Point.new(3, 4)
print(a:distance(b))   -- 5.0

a:move(1, 1)
print(a.x, a.y)        -- 1   1
print(a:distance(b))   -- sqrt(2^2 + 3^2) ~ 3.6055...

Kesalahan umum.

  • Lupa menulis Point.__index = Point. Tanpa baris itu, a:distance(b) tidak bisa menemukan method-nya, dan Lua akan melaporkan error attempt to call a nil value.

Soal 2 — Class Character

Solusi lengkap.

local Character = {}
Character.__index = Character

function Character.new(name, hp)
    local self = setmetatable({}, Character)
    self.name = name
    self.hp = hp
    self.max_hp = hp
    return self
end

function Character:takeDamage(amount)
    self.hp = self.hp - amount
    if self.hp < 0 then self.hp = 0 end
end

function Character:heal(amount)
    self.hp = self.hp + amount
    if self.hp > self.max_hp then self.hp = self.max_hp end
end

function Character:isAlive()
    return self.hp > 0
end

function Character:report()
    print(string.format("%s: %d / %d HP (alive: %s)",
        self.name, self.hp, self.max_hp, tostring(self:isAlive())))
end

local c = Character.new("Keiko", 100)
c:report()
c:takeDamage(30)
c:report()
c:takeDamage(80)
c:report()
c:heal(20)
c:report()

Contoh keluaran:

Keiko: 100 / 100 HP (alive: true)
Keiko: 70 / 100 HP (alive: true)
Keiko: 0 / 100 HP (alive: false)
Keiko: 20 / 100 HP (alive: true)

Character:report adalah tempat yang pas untuk string.format — tiga nilai dalam satu baris template yang tetap.

Kesalahan umum.

  • Membiarkan hp turun di bawah nol atau naik melebihi max_hp. Dua pengecekan if di takeDamage dan heal membatasi nilainya — mirip fungsi clamp dari PR bab 21, diterapkan sebagai method.

Soal 3 — Class Rectangle

Solusi lengkap.

local Rectangle = {}
Rectangle.__index = Rectangle

function Rectangle.new(w, h)
    local self = setmetatable({}, Rectangle)
    self.w = w
    self.h = h
    return self
end

function Rectangle:area()
    return self.w * self.h
end

function Rectangle:perimeter()
    return 2 * (self.w + self.h)
end

local r1 = Rectangle.new(3, 4)
local r2 = Rectangle.new(10, 2)

print(r1:area())        -- 12
print(r1:perimeter())   -- 14
print(r2:area())        -- 20
print(r2:perimeter())   -- 24

Kesalahan umum.

  • Menyimpan width dan height sebagai variabel lokal di dalam method, bukan sebagai self.w dan self.h. Tujuan objek adalah agar field-nya tetap melekat pada instance.

Tantangan — Animal, Dog, Cat

Solusi lengkap.

-- Base class
local Animal = {}
Animal.__index = Animal

function Animal.new(name)
    local self = setmetatable({}, Animal)
    self.name = name
    return self
end

function Animal:describe()
    print("I am " .. self.name .. ".")
end

-- Dog inherits Animal
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog

function Dog.new(name)
    local self = Animal.new(name)
    return setmetatable(self, Dog)
end

function Dog:bark()
    print(self.name .. ": Woof!")
end

-- Cat inherits Animal
local Cat = setmetatable({}, { __index = Animal })
Cat.__index = Cat

function Cat.new(name)
    local self = Animal.new(name)
    return setmetatable(self, Cat)
end

function Cat:meow()
    print(self.name .. ": Meow.")
end

local rex = Dog.new("Rex")
local whiskers = Cat.new("Whiskers")

rex:describe()       -- I am Rex.
whiskers:describe()  -- I am Whiskers.

rex:bark()           -- Rex: Woof!
whiskers:meow()      -- Whiskers: Meow.

Dog.new dan Cat.new membangun instance Animal terlebih dahulu, lalu mengganti metatable-nya ke class yang lebih spesifik. Rantai pencarian sekarang menjadi instance -> Dog -> Animal (atau instance -> Cat -> Animal).

Kesalahan umum.

  • Lupa menulis Dog.__index = Dog. Kalau tidak ada, method yang didefinisikan di Dog tidak bisa dijangkau dari instance-nya; pencarian langsung melompat ke Animal, karena itulah yang ditunjuk oleh metatable milik Dog (bukan Dog itu sendiri). Ada dua baris __index, pada dua objek berbeda, yang mengerjakan dua hal berbeda.

Selesai?

Class berbasis metatable (Point, Character, Rectangle) dan rantai pewarisan kecil (Animal -> Dog/Cat) kini sudah masuk ke kotak perkakas kamu. Tiga bab berikutnya membangun di atas fondasi ini: Banyak objek sekaligus mengelola kumpulan instance, Pewarisan lebih dalam membuat satu class khusus dari class lain, dan Merancang class kecil membahas cara membuat class yang enak dipakai.