Kelas #
Kelas adalah fondasi pemrograman berorientasi objek di Ruby — dan Ruby mengambil OOP lebih serius dari kebanyakan bahasa. Di Ruby, segalanya adalah objek: integer, string, nil, bahkan kelas itu sendiri adalah objek dari kelas Class. Ini bukan sekadar filosofi — ia punya implikasi praktis yang dalam. Kamu bisa menambahkan method ke kelas yang sudah ada (termasuk kelas bawaan seperti String dan Integer), kelas terbuka untuk di-reopen kapan saja, dan setiap objek bisa introspeksi dirinya sendiri. Artikel ini membahas semua aspek kelas di Ruby dari yang paling dasar hingga pola desain yang digunakan di codebase profesional.
Mendefinisikan Kelas #
Kelas didefinisikan dengan keyword class diikuti nama dalam format PascalCase. Setiap kelas adalah turunan dari Object secara implisit jika tidak ada inheritance yang dinyatakan:
class Produk
# isi kelas di sini
end
# Kelas adalah objek dari Class
puts Produk.class # => Class
puts Produk.superclass # => Object
puts Object.superclass # => BasicObject
puts BasicObject.superclass # => nil (puncak hierarki)
initialize — Konstruktor #
initialize adalah metode khusus yang dipanggil otomatis saat objek baru dibuat dengan new. Di sinilah variabel instance diinisialisasi:
class Produk
def initialize(nama, harga, stok = 0)
@nama = nama
@harga = harga
@stok = stok
end
def info
"#{@nama} — Rp #{@harga} (stok: #{@stok})"
end
end
laptop = Produk.new("Laptop", 15_000_000, 10)
mouse = Produk.new("Mouse", 350_000) # stok default 0
puts laptop.info # => Laptop — Rp 15000000 (stok: 10)
puts mouse.info # => Mouse — Rp 350000 (stok: 0)
Atribut — attr_reader, attr_writer, attr_accessor #
Variabel instance tidak bisa diakses dari luar kelas secara langsung. Ruby menyediakan tiga shortcut untuk membuat getter dan setter:
# ANTI-PATTERN: getter dan setter manual — verbose dan repetitif
class Produk
def nama
@nama
end
def nama=(nilai)
@nama = nilai
end
def harga
@harga
end
end
# BENAR: gunakan attr_* untuk mengurangi boilerplate
class Produk
attr_reader :id # hanya bisa dibaca dari luar
attr_writer :stok # hanya bisa ditulis dari luar
attr_accessor :nama, :harga # bisa dibaca dan ditulis
def initialize(id, nama, harga)
@id = id
@nama = nama
@harga = harga
@stok = 0
end
def info
"#{@nama} — Rp #{@harga}"
end
end
p = Produk.new(1, "Keyboard", 450_000)
puts p.id # => 1 (reader)
puts p.nama # => Keyboard (reader dari accessor)
p.nama = "Mechanical Keyboard" # (writer dari accessor)
p.harga = 750_000 # (writer dari accessor)
p.stok = 25 # (writer)
puts p.info # => Mechanical Keyboard — Rp 750000
puts p.id # => 1 (tidak bisa diubah — hanya reader)
# p.id = 99 # => NoMethodError: undefined method 'id='
Validasi di dalam Setter #
Satu alasan tidak selalu menggunakan attr_writer langsung adalah kamu kehilangan kemampuan validasi. Ketika perlu memvalidasi nilai yang di-set, tulis setter sendiri:
class Produk
attr_reader :nama, :harga, :stok
def initialize(nama, harga)
self.nama = nama # gunakan setter sendiri agar validasi jalan
self.harga = harga
@stok = 0
end
def nama=(nilai)
raise ArgumentError, "Nama tidak boleh kosong" if nilai.to_s.strip.empty?
@nama = nilai.strip
end
def harga=(nilai)
raise ArgumentError, "Harga harus positif" unless nilai.is_a?(Numeric) && nilai > 0
@harga = nilai
end
def stok=(nilai)
raise ArgumentError, "Stok tidak boleh negatif" if nilai < 0
@stok = nilai
end
end
p = Produk.new("Laptop", 15_000_000)
p.stok = 10
# p.harga = -500 # => ArgumentError: Harga harus positif
# p.nama = "" # => ArgumentError: Nama tidak boleh kosong
Instance Method vs Class Method #
Instance method dipanggil pada objek, class method dipanggil pada kelas itu sendiri:
class Pengguna
@@jumlah = 0
attr_reader :nama, :email
def initialize(nama, email)
@nama = nama
@email = email
@@jumlah += 1
end
# Instance method — dipanggil pada objek
def perkenalan
"Halo, saya #{@nama} (#{@email})"
end
def aktif?
true # disederhanakan
end
# Class method — dipanggil pada kelas Pengguna
def self.jumlah_terdaftar
@@jumlah
end
# Cara alternatif mendefinisikan class method — class << self
class << self
def buat_admin(nama)
new(nama, "#{nama.downcase}@admin.com")
end
def buat_guest
new("Tamu", "[email protected]")
end
end
end
u1 = Pengguna.new("Rina", "[email protected]")
u2 = Pengguna.new("Budi", "[email protected]")
admin = Pengguna.buat_admin("SuperAdmin")
puts u1.perkenalan # => Halo, saya Rina ([email protected])
puts Pengguna.jumlah_terdaftar # => 3
puts admin.email # => [email protected]
flowchart TD
A[Kelas Pengguna] --> B[Instance Methods\ndipanggil pada objek]
A --> C[Class Methods\ndipanggil pada kelas]
B --> B1["perkenalan\naktif?\nto_s"]
C --> C1["jumlah_terdaftar\nbuat_admin\nbuat_guest"]
D[Objek u1] --> B
E[Pengguna] --> CInheritance — Pewarisan #
Inheritance memungkinkan kelas anak mewarisi semua metode dan atribut kelas induk. Gunakan operator <:
class Hewan
attr_reader :nama, :usia
def initialize(nama, usia)
@nama = nama
@usia = usia
end
def bernapas
"#{@nama} sedang bernapas..."
end
def deskripsi
"#{@nama} (#{self.class.name}), #{@usia} tahun"
end
def suara
raise NotImplementedError, "#{self.class} harus mengimplementasikan #suara"
end
end
class Kucing < Hewan
attr_reader :ras
def initialize(nama, usia, ras)
super(nama, usia) # panggil initialize kelas induk
@ras = ras
end
def suara
"Meow!"
end
def deskripsi
"#{super}, ras: #{@ras}" # panggil deskripsi kelas induk lalu tambahkan
end
end
class Anjing < Hewan
def suara
"Woof!"
end
def ambil(benda)
"#{@nama} mengambil #{benda}!"
end
end
kucing = Kucing.new("Mochi", 3, "Persia")
anjing = Anjing.new("Rex", 5)
puts kucing.bernapas # => Mochi sedang bernapas... (diwarisi)
puts kucing.suara # => Meow!
puts kucing.deskripsi # => Mochi (Kucing), 3 tahun, ras: Persia
puts anjing.suara # => Woof!
puts anjing.ambil("bola") # => Rex mengambil bola!
# Cek hubungan inheritance
puts kucing.is_a?(Kucing) # => true
puts kucing.is_a?(Hewan) # => true
puts kucing.is_a?(Anjing) # => false
puts Kucing.ancestors.inspect
# => [Kucing, Hewan, Object, Kernel, BasicObject]
super — Memanggil Metode Kelas Induk #
super memanggil metode dengan nama yang sama dari kelas induk. Ada tiga bentuk penggunaannya:
class Kendaraan
def initialize(merk, tahun)
@merk = merk
@tahun = tahun
end
def info
"#{@merk} (#{@tahun})"
end
end
class Mobil < Kendaraan
def initialize(merk, tahun, pintu)
super(merk, tahun) # teruskan argumen tertentu ke induk
@pintu = pintu
end
def info
super + ", #{@pintu} pintu" # panggil info induk, lalu tambahkan
end
end
class Motor < Kendaraan
def initialize(merk, tahun, cc)
super # teruskan SEMUA argumen ke induk (merk dan tahun)
@cc = cc
end
def info
"#{super} — #{@cc}cc"
end
end
puts Mobil.new("Toyota", 2022, 4).info # => Toyota (2022), 4 pintu
puts Motor.new("Honda", 2023, 150).info # => Honda (2023) — 150cc
Modul dan Mixins #
Ruby hanya mengizinkan single inheritance — satu kelas hanya bisa punya satu kelas induk. Tapi Ruby mengatasi keterbatasan ini dengan mixins: kemampuan menyisipkan metode dari modul ke dalam kelas menggunakan include, extend, atau prepend.
module Dapat_Dicetak
def cetak
puts "--- #{self.class.name} ---"
instance_variables.each do |var|
puts " #{var}: #{instance_variable_get(var)}"
end
puts "---"
end
end
module Dapat_Diserialisasi
def to_json_sederhana
pairs = instance_variables.map do |var|
key = var.to_s.delete("@")
val = instance_variable_get(var)
"\"#{key}\": #{val.inspect}"
end
"{ #{pairs.join(', ')} }"
end
end
module Dapat_Dibandingkan
def sama_dengan?(other)
self.class == other.class &&
instance_variables.all? do |var|
instance_variable_get(var) == other.instance_variable_get(var)
end
end
end
class Produk
include Dapat_Dicetak
include Dapat_Diserialisasi
include Dapat_Dibandingkan
attr_accessor :nama, :harga
def initialize(nama, harga)
@nama = nama
@harga = harga
end
end
p1 = Produk.new("Mouse", 350_000)
p2 = Produk.new("Mouse", 350_000)
p3 = Produk.new("Keyboard", 450_000)
p1.cetak
# --- Produk ---
# @nama: "Mouse"
# @harga: 350000
# ---
puts p1.to_json_sederhana
# { "nama": "Mouse", "harga": 350000 }
puts p1.sama_dengan?(p2) # => true
puts p1.sama_dengan?(p3) # => false
include vs extend vs prepend #
module Sapa
def halo
"Halo dari #{self}!"
end
end
# include — tambahkan method sebagai INSTANCE method
class Kelas1
include Sapa
end
puts Kelas1.new.halo # => Halo dari #<Kelas1:...>
# extend — tambahkan method sebagai CLASS method
class Kelas2
extend Sapa
end
puts Kelas2.halo # => Halo dari Kelas2
# prepend — sisipkan method DI DEPAN kelas dalam method lookup
# method modul dipanggil SEBELUM method kelas (berguna untuk wrapping/decoration)
module Logger
def simpan
puts "[LOG] Sebelum simpan"
hasil = super
puts "[LOG] Setelah simpan"
hasil
end
end
class DataModel
prepend Logger
def simpan
puts "Menyimpan ke database..."
true
end
end
DataModel.new.simpan
# => [LOG] Sebelum simpan
# => Menyimpan ke database...
# => [LOG] Setelah simpan
include | extend | prepend | |
|---|---|---|---|
| Jenis method | Instance | Class | Instance |
| Posisi di lookup | Setelah kelas | — | Sebelum kelas |
| Kegunaan utama | Shared behavior | Helper kelas | Wrapping/decoration |
Open Class — Membuka Kelas yang Sudah Ada #
Salah satu fitur paling khas Ruby — dan paling kontroversial — adalah kemampuan membuka dan menambahkan method ke kelas yang sudah ada, termasuk kelas bawaan Ruby:
# Tambahkan method ke kelas Integer bawaan Ruby
class Integer
def detik
self
end
def menit
self * 60
end
def jam
self * 3600
end
def hari
self * 86_400
end
def dalam_rupiah
"Rp #{to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1.').reverse}"
end
end
puts 5.menit # => 300
puts 2.jam # => 7200
puts 3.hari # => 259200
puts 1_500_000.dalam_rupiah # => Rp 1.500.000
# Tambahkan method ke String
class String
def snake_case
gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
.downcase
end
def pascal_case
split('_').map(&:capitalize).join
end
def blank?
strip.empty?
end
end
puts "NamaLengkapPengguna".snake_case # => nama_lengkap_pengguna
puts "nama_lengkap".pascal_case # => NamaLengkap
puts " ".blank? # => true
Open class (juga disebut monkey patching) adalah pedang bermata dua. Ia membuat kode lebih ekspresif, tapi menambahkan method ke kelas bawaan Ruby bisa menyebabkan konflik nama dengan gem lain atau versi Ruby di masa depan. Sebagai alternatif yang lebih aman, gunakan Refinements — fitur Ruby yang membatasi perubahan kelas hanya ke scope file atau modul tertentu, tidak secara global.
Comparable — Operator Perbandingan Gratis #
Dengan meng-include modul Comparable dan mendefinisikan satu metode <=>, kelas kamu secara otomatis mendapatkan semua operator perbandingan (<, >, <=, >=, between?, clamp):
class Suhu
include Comparable
attr_reader :nilai, :satuan
def initialize(nilai, satuan = :celsius)
@nilai = nilai
@satuan = satuan
end
def dalam_celsius
case @satuan
when :celsius then @nilai
when :fahrenheit then (@nilai - 32) * 5.0 / 9
when :kelvin then @nilai - 273.15
end
end
def <=>(other)
dalam_celsius <=> other.dalam_celsius
end
def to_s
"#{@nilai}°#{@satuan.to_s[0].upcase}"
end
end
mendidih = Suhu.new(100, :celsius)
beku = Suhu.new(32, :fahrenheit) # = 0°C
ruangan = Suhu.new(300, :kelvin) # = 26.85°C
puts mendidih > ruangan # => true
puts beku < ruangan # => true
puts ruangan.between?(beku, mendidih) # => true
suhu_list = [mendidih, ruangan, beku]
puts suhu_list.sort.map(&:to_s).inspect
# => ["32°F", "300°K", "100°C"]
puts suhu_list.min # => 32°F
puts suhu_list.max # => 100°C
Struct — Kelas Data Sederhana #
Struct adalah cara cepat membuat kelas data sederhana tanpa harus menulis initialize, getter, dan setter secara manual:
# Membuat Struct
Titik = Struct.new(:x, :y)
Warna = Struct.new(:r, :g, :b)
Alamat = Struct.new(:jalan, :kota, :kode_pos, keyword_init: true)
p1 = Titik.new(3, 4)
puts p1.x # => 3
puts p1.y # => 4
puts p1.to_a # => [3, 4]
puts p1 == Titik.new(3, 4) # => true (perbandingan by value!)
# Struct dengan metode tambahan
Titik = Struct.new(:x, :y) do
def jarak_ke(other)
Math.sqrt((x - other.x)**2 + (y - other.y)**2)
end
def to_s
"(#{x}, #{y})"
end
end
a = Titik.new(0, 0)
b = Titik.new(3, 4)
puts a.jarak_ke(b) # => 5.0
# keyword_init: true — lebih ekspresif saat membuat instance
alamat = Alamat.new(jalan: "Jl. Sudirman No. 1", kota: "Jakarta", kode_pos: "10220")
puts alamat.kota # => Jakarta
Kapan pakai Struct vs kelas biasa:
Struct:
✓ Data container sederhana tanpa logika kompleks
✓ Butuh perbandingan nilai (== by value) secara otomatis
✓ Ingin mengurangi boilerplate initialize + attr_accessor
✓ Value object yang immutable
Kelas biasa:
✓ Ada validasi di setter/initialize
✓ Ada logika bisnis yang kompleks
✓ Butuh kontrol visibility yang detail
✓ Inheritance dari kelas lain
to_s dan inspect — Representasi Teks Objek #
Dua metode yang paling berguna untuk kelas custom adalah to_s (untuk output yang ramah pengguna) dan inspect (untuk output debugging):
class Pesanan
attr_reader :id, :total, :status
def initialize(id, total, status = :pending)
@id = id
@total = total
@status = status
end
# Untuk puts, string interpolasi, output ke user
def to_s
"Pesanan ##{@id} — Rp #{@total} [#{@status}]"
end
# Untuk debugging, p(), irb
def inspect
"#<Pesanan id=#{@id}, total=#{@total}, status=:#{@status}>"
end
end
pesanan = Pesanan.new(42, 150_000, :diproses)
puts pesanan # => Pesanan #42 — Rp 150000 [diproses]
puts "#{pesanan}" # => Pesanan #42 — Rp 150000 [diproses]
p pesanan # => #<Pesanan id=42, total=150000, status=:diproses>
puts pesanan.inspect # => #<Pesanan id=42, total=150000, status=:diproses>
Prinsip Desain Kelas yang Baik #
flowchart TD
A[Kelas yang baik] --> B["Single Responsibility\nSatu alasan untuk berubah"]
A --> C["Enkapsulasi\nSembunyikan detail internal"]
A --> D["Interface minimal\nHanya expose yang perlu"]
A --> E["initialize yang jelas\nInisialisasi semua state"]
B --> B1["Pisahkan Produk dan ProdukRepository"]
C --> C1["Validasi di setter\nbukan di pemanggil"]
D --> D1["Semua internal jadi private\nPublic hanya API yang diperlukan"]
E --> E1["Jangan biarkan @var nil\ntanpa alasan yang jelas"]# ANTI-PATTERN: kelas yang melakukan terlalu banyak
class Pengguna
attr_accessor :nama, :email
def initialize(nama, email)
@nama = nama
@email = email
end
def simpan_ke_database # ← tanggung jawab persistence
DB.execute("INSERT INTO users ...")
end
def kirim_email_selamat_datang # ← tanggung jawab email
Mailer.send(...)
end
def hasilkan_laporan # ← tanggung jawab reporting
# ...
end
end
# BENAR: pisahkan tanggung jawab
class Pengguna
attr_reader :nama, :email
def initialize(nama, email)
self.nama = nama
self.email = email
end
private
def nama=(nilai)
raise ArgumentError, "Nama wajib diisi" if nilai.to_s.strip.empty?
@nama = nilai.strip
end
def email=(nilai)
raise ArgumentError, "Format email tidak valid" unless nilai.match?(/\A\S+@\S+\z/)
@email = nilai.downcase
end
end
class PenggunaRepository
def simpan(pengguna)
DB.execute("INSERT INTO users (nama, email) VALUES (?, ?)", pengguna.nama, pengguna.email)
end
end
class PenggunaSambutan
def kirim(pengguna)
Mailer.send(to: pengguna.email, subject: "Selamat datang, #{pengguna.nama}!")
end
end
Ringkasan #
initializeadalah konstruktor — inisialisasi semua variabel instance di sini, gunakan setter internal agar validasi berjalan sejak awal.attr_reader/writer/accessormengurangi boilerplate — tapi tulis setter sendiri jika butuh validasi; jangan eksposattr_writerke luar jika tidak perlu.- Class method dengan
self.atauclass << self— untuk factory method, utilitas tingkat kelas, dan counter bersama.- Single inheritance, banyak mixin — Ruby hanya mengizinkan satu superclass, tapi modul bisa di-include sebanyak yang dibutuhkan.
includeuntuk instance method,extenduntuk class method,prependuntuk wrapping — pilih berdasarkan kebutuhan, bukan kebiasaan.- Open class memang powerful, tapi berbahaya — pertimbangkan Refinements sebagai alternatif yang lebih aman untuk memodifikasi kelas bawaan.
Comparablememberi semua operator perbandingan — cukup definisikan<=>dan include Comparable, sisanya gratis.Structuntuk value object sederhana — jauh lebih ringkas dari menulis kelas manual, dan mendapat==by value secara otomatis.- Definisikan
to_sdaninspect—to_suntuk output ke user,inspectuntuk debugging. Tanpa keduanya, output default tidak informatif.- Single Responsibility Principle — pisahkan persistence, email, dan logika bisnis ke kelas yang berbeda. Kelas yang bagus punya satu alasan untuk berubah.