27. OOP Sederhana dengan Metatable

Bab 26 menaruh beberapa method langsung ke sebuah tabel. Cara itu oke untuk satu objek, tapi buang-buang tenaga kalau kamu butuh banyak objek dengan bentuk yang sama. Bayangkan 100 anjing — menyalin bark ke masing-masing satu per satu itu konyol. Bab ini memperkenalkan metatable, lalu menggunakannya untuk membuat satu tabel method yang dipakai bersama oleh semua instance.

Di sinilah kode bergaya Roblox mulai terasa familiar. Part, Player, dan Tool di Roblox semuanya mengikuti pola ini.

Class dengan .new dan :method

Berikut ini adalah pola "class" standar di Lua. Baca dua kali, lalu cek penjelasannya:

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

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

Penjelasannya:

  1. Point = {} membuat tabel class — semua method disimpan di sini, didefinisikan sekali saja.
  2. Point.__index = Point adalah baris ajaibnya. Baris ini memberitahu Lua: kalau pencarian kunci pada sebuah instance gagal, cari juga di sini. Metatablenya adalah Point itu sendiri, dengan __index yang menunjuk balik ke dirinya sendiri.
  3. Point.new(x, y) adalah fungsi biasa (perhatikan titik, bukan titik dua). Fungsi ini membuat sebuah instance — tabel kosong — mengatur metatablenya ke Point, mengisi field-fieldnya, lalu mengembalikannya.
  4. setmetatable({}, Point) memasang metatable, sehingga tabel kosong itu sekarang tahu: untuk kunci yang tidak ada, cari di dalam Point.
  5. function Point:distance(other) mendefinisikan method dengan titik dua, menjadikan self sebagai parameter pertama — sama seperti bab 26.
  6. a:distance(b) adalah tempat pemanggilan. a adalah sebuah instance, jadi Lua mengecek a untuk distance. Tidak ketemu, maka __index mengarahkan pencarian ke Point, yang memilikinya. Method berjalan dengan self = a dan other = b.

Hasilnya: satu tabel method, banyak instance kecil, tidak ada penyalinan.

Buka exercises/27/01-point.lua. Tambahkan method Point:move(dx, dy) yang menambahkan dx ke self.x dan dy ke self.y. Buat sebuah titik, pindahkan dua kali, lalu cetak posisinya.

Kenapa perlu indirection metatable?

Sebuah class butuh metatable, bukan tabel biasa, karena penugasan ke sebuah instance tidak boleh memengaruhi class-nya.

Kalau kamu melewati metatable dan menulis local self = Point, maka self.x = 0 akan menulis x = 0 langsung ke class Point itu sendiri. Setiap "instance" akan berbagi x yang sama — bukan itu artinya "instance".

setmetatable({}, Point) memberikan tabel kosong baru untuk setiap instance. Penulisan masuk ke instance; pembacaan jatuh ke class hanya kalau instance tidak memiliki kuncinya.

Class kedua: Character

Class karakter kecil yang bisa menerima kerusakan:

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

local c = Character.new("Keiko", 100)
c:takeDamage(30)
print(c.hp)            -- 70
c:heal(50)
print(c.hp)            -- 100   (capped at max_hp)
print(c:isAlive())     -- true

Setiap Character.new(...) menghasilkan instance baru dengan name, hp, dan max_hp miliknya sendiri. Keempat method tinggal di Character; setiap instance menjangkaunya melalui __index.

Pewarisan dengan dua lompatan __index

Class anak mewarisi dari class induk dengan memberikan metatable milik class anak rantai __index-nya sendiri. Dua tabel singkat berikut ini:

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 from Animal
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog

function Dog.new(name)
    local self = Animal.new(name)        -- build with Animal's fields
    return setmetatable(self, Dog)       -- but with Dog as metatable
end

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

local rex = Dog.new("Rex")
rex:describe()    -- I am Rex.   (inherited from Animal)
rex:bark()        -- Rex: Woof!  (defined on Dog)

Baris kuncinya adalah local Dog = setmetatable({}, { __index = Animal }). Baris ini menyatakan: ketika pencarian jatuh ke Dog dan tidak ketemu, jatuh lagi ke Animal. Jadi rex:describe() mengecek rex, tidak ketemu; mengecek Dog, tidak ketemu; mengecek Animal, ketemu.

Itu sudah cukup untuk sebagian besar keperluan pewarisan. Metatable bisa melakukan lebih banyak (operator overloading, perbandingan kustom, tabel yang bisa dipanggil seperti fungsi), tapi pola-pola ini mencakup semua yang dibutuhkan mini-proyek Bagian 6 dan jembatan Roblox.

PR

Soal 1 — Class Point dengan move

Buka exercises/27/homework/01-point.lua. Buat class Point seperti di atas, plus method Point:move(dx, dy) yang menambahkan delta ke posisi. Uji dengan membuat dua titik, memindahkan salah satu, lalu mencetak jarak di antara keduanya.

Soal 2 — Class Character

Buka exercises/27/homework/02-character.lua. Buat class Character dengan .new(name, hp), :takeDamage(amount), :heal(amount), dan :isAlive(). Tambahkan method :report() yang mencetak nama dan HP saat ini. Jalankan pertarungan kecil: serang dua kali, sembuhkan sekali, dan cetak laporan setelah setiap aksi.

Soal 3 — Class Rectangle

Buka exercises/27/homework/03-rectangle.lua. Buat class Rectangle dengan .new(width, height), :area(), dan :perimeter(). Uji dengan dua persegi panjang berukuran berbeda.

Tantangan — Animal dan Dog

Buka exercises/27/homework/04-inheritance.lua. Buat Animal dan Dog persis seperti contoh pewarisan di bab ini. Kemudian tambahkan class anak kedua yaitu Cat yang juga mewarisi dari Animal dan punya method :meow() sendiri. Buat satu anjing dan satu kucing, panggil :describe() pada keduanya, lalu :bark() pada anjing dan :meow() pada kucing.

Buntu atau sudah selesai? Buka halaman solusi PR.