Kelas

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] --> C

Inheritance — 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
includeextendprepend
Jenis methodInstanceClassInstance
Posisi di lookupSetelah kelasSebelum kelas
Kegunaan utamaShared behaviorHelper kelasWrapping/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 #

  • initialize adalah konstruktor — inisialisasi semua variabel instance di sini, gunakan setter internal agar validasi berjalan sejak awal.
  • attr_reader/writer/accessor mengurangi boilerplate — tapi tulis setter sendiri jika butuh validasi; jangan ekspos attr_writer ke luar jika tidak perlu.
  • Class method dengan self. atau class << 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.
  • include untuk instance method, extend untuk class method, prepend untuk wrapping — pilih berdasarkan kebutuhan, bukan kebiasaan.
  • Open class memang powerful, tapi berbahaya — pertimbangkan Refinements sebagai alternatif yang lebih aman untuk memodifikasi kelas bawaan.
  • Comparable memberi semua operator perbandingan — cukup definisikan <=> dan include Comparable, sisanya gratis.
  • Struct untuk value object sederhana — jauh lebih ringkas dari menulis kelas manual, dan mendapat == by value secara otomatis.
  • Definisikan to_s dan inspectto_s untuk output ke user, inspect untuk 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.

← Sebelumnya: Fungsi   Berikutnya: Interface →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact