Interface #
Developer yang datang dari Java, C#, atau TypeScript biasanya mencari keyword interface di Ruby — dan tidak menemukannya. Ini bukan kekurangan, tapi pilihan desain yang disengaja. Ruby menganut filosofi yang berbeda secara fundamental: daripada memaksakan kontrak melalui tipe statis, Ruby mengandalkan duck typing — jika sebuah objek punya method yang dibutuhkan, ia bisa digunakan di mana saja tanpa perlu dideklarasikan sebagai implementor dari interface tertentu. Artikel ini menjelaskan bagaimana Ruby mereplikasi manfaat interface melalui modul, pola NotImplementedError, respond_to?, dan duck typing — serta kapan masing-masing pendekatan paling tepat digunakan.
Mengapa Ruby Tidak Punya Interface Formal #
Sebelum membahas solusi Ruby, penting untuk memahami mengapa interface ada di Java dan C#: karena kedua bahasa itu adalah statically typed — tipe setiap variabel harus diketahui saat kompilasi. Interface menjadi cara untuk menyatakan “objek apapun yang masuk ke sini harus punya method X dan Y” agar compiler bisa memverifikasinya.
Ruby adalah dynamically typed — tipe tidak diverifikasi saat kompilasi, hanya saat runtime. Ini berarti Ruby tidak butuh interface untuk alasan yang sama. Yang dibutuhkan hanyalah: ketika method dipanggil, objek yang bersangkutan harus punya method itu. Jika tidak, Ruby akan melempar NoMethodError — dan inilah “type error” versi Ruby.
# Di Java, kamu HARUS deklarasikan bahwa Dog implements Animal
# interface Animal { void speak(); }
# class Dog implements Animal { public void speak() { ... } }
# Di Ruby, kamu tidak perlu deklarasi apapun
class Anjing
def suara
"Guk!"
end
end
class Kucing
def suara
"Meong!"
end
end
class Burung
def suara
"Cicit!"
end
end
# Fungsi ini bekerja dengan SEMUA objek di atas — tanpa interface
def perdengarkan_suara(hewan)
puts hewan.suara
end
perdengarkan_suara(Anjing.new) # => Guk!
perdengarkan_suara(Kucing.new) # => Meong!
perdengarkan_suara(Burung.new) # => Cicit!
Inilah duck typing: “jika ia bisa bersuara seperti hewan, ia adalah hewan” — tanpa perlu surat keterangan resmi dari interface.
Modul sebagai Kontrak Perilaku #
Meskipun Ruby tidak punya interface formal, modul bisa digunakan untuk mendefinisikan kontrak perilaku yang lebih eksplisit. Ada dua cara modul digunakan sebagai pengganti interface: sebagai kontrak dokumentatif (memberi tahu developer apa yang harus diimplementasikan) dan sebagai kontrak enforced (melempar error jika method tidak diimplementasikan).
Pola NotImplementedError — Kontrak Enforced #
Pola paling umum untuk mereplikasi interface di Ruby adalah mendefinisikan method di modul yang melempar NotImplementedError. Method ini berfungsi sebagai placeholder yang memberitahu developer: “kamu harus override method ini di kelas yang include modul ini.”
module Dapat_Disimpan
def simpan
raise NotImplementedError, "#{self.class}#simpan harus diimplementasikan"
end
def hapus
raise NotImplementedError, "#{self.class}#hapus harus diimplementasikan"
end
def temukan(id)
raise NotImplementedError, "#{self.class}.temukan harus diimplementasikan"
end
end
# Implementasi untuk database SQL
class PenggunaRepository
include Dapat_Disimpan
def simpan(pengguna)
DB.execute("INSERT INTO users (nama, email) VALUES (?, ?)",
pengguna.nama, pengguna.email)
puts "#{pengguna.nama} disimpan ke PostgreSQL"
end
def hapus(id)
DB.execute("DELETE FROM users WHERE id = ?", id)
puts "User #{id} dihapus dari PostgreSQL"
end
def temukan(id)
DB.query("SELECT * FROM users WHERE id = ?", id)
end
end
# Implementasi untuk penyimpanan in-memory (berguna untuk testing)
class PenggunaMemoryRepository
include Dapat_Disimpan
def initialize
@store = {}
@next_id = 1
end
def simpan(pengguna)
@store[@next_id] = pengguna
@next_id += 1
puts "#{pengguna.nama} disimpan ke memory"
end
def hapus(id)
@store.delete(id)
puts "User #{id} dihapus dari memory"
end
def temukan(id)
@store[id]
end
end
# Kelas yang lupa mengimplementasikan method — langsung ketahuan saat runtime
class BrokenRepository
include Dapat_Disimpan
# lupa implementasikan simpan, hapus, temukan
end
repo = BrokenRepository.new
repo.simpan(nil)
# => NotImplementedError: BrokenRepository#simpan harus diimplementasikan
Modul dengan Implementasi Default #
Salah satu keunggulan modul Ruby dibanding interface Java adalah modul bisa menyediakan implementasi default untuk method tertentu. Ini mirip dengan default methods di Java 8+, tapi lebih natural di Ruby:
module Dapat_Diekspor
# Method yang HARUS diimplementasikan oleh kelas
def ke_hash
raise NotImplementedError, "#{self.class}#ke_hash harus diimplementasikan"
end
# Method dengan implementasi DEFAULT — tidak wajib di-override
def ke_json
require 'json'
ke_hash.to_json
end
def ke_csv_baris
ke_hash.values.join(",")
end
def ke_xml
pasangan = ke_hash.map { |k, v| "<#{k}>#{v}</#{k}>" }.join
"<#{self.class.name.downcase}>#{pasangan}</#{self.class.name.downcase}>"
end
end
class Produk
include Dapat_Diekspor
attr_reader :nama, :harga, :kategori
def initialize(nama, harga, kategori)
@nama = nama
@harga = harga
@kategori = kategori
end
# Hanya wajib implement ke_hash — sisanya gratis dari modul
def ke_hash
{ nama: @nama, harga: @harga, kategori: @kategori }
end
end
class Pengguna
include Dapat_Diekspor
attr_reader :nama, :email
def initialize(nama, email)
@nama = nama
@email = email
end
def ke_hash
{ nama: @nama, email: @email }
end
end
p = Produk.new("Laptop", 15_000_000, "Elektronik")
puts p.ke_json # => {"nama":"Laptop","harga":15000000,"kategori":"Elektronik"}
puts p.ke_csv_baris # => Laptop,15000000,Elektronik
puts p.ke_xml # => <produk><nama>Laptop</nama>...</produk>
u = Pengguna.new("Rina", "[email protected]")
puts u.ke_json # => {"nama":"Rina","email":"[email protected]"}
Duck Typing — Filosofi Utama Ruby #
Duck typing bukan sekadar “tidak punya interface” — ia adalah pendekatan yang berbeda secara fundamental untuk polimorfisme. Daripada memeriksa tipe objek, kamu memeriksa kemampuannya:
# Pendekatan berbasis tipe (ANTI-PATTERN di Ruby)
def proses_pembayaran(metode_bayar)
if metode_bayar.is_a?(KartuKredit)
metode_bayar.charge_kartu(total)
elsif metode_bayar.is_a?(Transfer)
metode_bayar.transfer_bank(total)
elsif metode_bayar.is_a?(Dompet)
metode_bayar.potong_saldo(total)
end
end
# Pendekatan duck typing (BENAR di Ruby)
def proses_pembayaran(metode_bayar, total)
metode_bayar.bayar(total) # semua metode pembayaran harus respond to :bayar
end
# Setiap kelas hanya perlu punya method :bayar
class KartuKredit
def bayar(total)
puts "Charging Rp #{total} ke kartu kredit"
true
end
end
class TransferBank
def bayar(total)
puts "Transfer Rp #{total} ke rekening tujuan"
true
end
end
class DompetDigital
def bayar(total)
puts "Memotong Rp #{total} dari saldo dompet"
true
end
end
class KriptoCoin
def bayar(total)
puts "Konversi dan bayar Rp #{total} dalam crypto"
true
end
end
# Semua bekerja tanpa interface, tanpa deklarasi formal
[KartuKredit, TransferBank, DompetDigital, KriptoCoin].each do |kelas|
proses_pembayaran(kelas.new, 150_000)
end
flowchart TD
A[Metode Pembayaran] --> B{Bagaimana memverifikasi\nbahwa objek bisa digunakan?}
B --> C["Pendekatan Java/C#\nInterface formal\n+ kompilasi"]
B --> D["Pendekatan Ruby\nDuck Typing\nruntime check"]
C --> C1["class KartuKredit\nimplements Pembayaran"]
D --> D1["Objek apapun yang\npunya method .bayar"]
C1 --> E["Verifikasi saat\nkompilasi"]
D1 --> F["Verifikasi saat\nruntime — NoMethodError\njika tidak ada"]
E --> G["Ketat, aman\ntapi kaku"]
F --> H["Fleksibel, ekspresif\ntapi butuh test yang baik"]respond_to? — Duck Typing yang Eksplisit #
Terkadang kamu perlu memeriksa secara eksplisit apakah sebuah objek punya method tertentu sebelum memanggilnya — terutama ketika method tersebut opsional:
def render(konten)
# Cek kemampuan objek, bukan tipenya
if konten.respond_to?(:ke_html)
puts konten.ke_html
elsif konten.respond_to?(:to_s)
puts "<p>#{konten.to_s}</p>"
else
puts "<p>Konten tidak bisa dirender</p>"
end
end
class ArtikelHtml
def ke_html
"<article><h1>Artikel</h1><p>Isi artikel...</p></article>"
end
end
class TeksPolos
def to_s
"Ini teks biasa tanpa HTML"
end
end
render(ArtikelHtml.new) # => <article>...</article>
render(TeksPolos.new) # => <p>Ini teks biasa tanpa HTML</p>
render(42) # => <p>42</p> (Integer punya to_s)
respond_to_missing? — Metode Dinamis #
Ketika kelas menggunakan method_missing untuk menangani method secara dinamis, respond_to? tidak akan mendeteksinya secara otomatis. Definisikan respond_to_missing? untuk membuatnya konsisten:
class ProksiFleksibel
def initialize(target)
@target = target
end
def method_missing(nama, *args, &blok)
if @target.respond_to?(nama)
@target.send(nama, *args, &blok)
else
super
end
end
# WAJIB didefinisikan jika pakai method_missing
def respond_to_missing?(nama, include_private = false)
@target.respond_to?(nama, include_private) || super
end
end
class Kalkulator
def tambah(a, b) = a + b
def kali(a, b) = a * b
end
proxy = ProksiFleksibel.new(Kalkulator.new)
puts proxy.tambah(3, 4) # => 7
puts proxy.respond_to?(:tambah) # => true (berkat respond_to_missing?)
puts proxy.respond_to?(:tidak_ada) # => false
Pola Interface yang Lebih Ketat #
Ketika kamu ingin memastikan semua method wajib diimplementasikan — tidak hanya saat method dipanggil, tapi segera saat kelas di-include — kamu bisa menggunakan callback included atau self.included:
module InterfaceKetat
def self.included(kelas)
# Callback ini dipanggil saat modul di-include ke kelas
kelas.instance_variable_set(:@method_wajib, [])
kelas.extend(ClassMethods)
end
module ClassMethods
def method_wajib(*nama_method)
@method_wajib = nama_method
# Tambahkan hook untuk verifikasi setelah kelas selesai didefinisikan
TracePoint.trace(:end) do |tp|
next unless tp.self == self
belum_diimplementasikan = @method_wajib.reject do |m|
method_defined?(m) && instance_method(m).owner == self
end
unless belum_diimplementasikan.empty?
raise NotImplementedError,
"#{self} belum mengimplementasikan: #{belum_diimplementasikan.join(', ')}"
end
tp.disable
end
end
end
end
Pendekatan di atas cukup kompleks untuk produksi. Alternatif yang lebih sederhana dan lebih umum digunakan di komunitas Ruby adalah pola abstract_method yang disimulasikan dengan raise:
module Notifikasi
# Definisikan "abstract method" yang wajib diimplementasikan
def kirim(penerima, pesan)
raise NotImplementedError, <<~MSG
#{self.class}#kirim belum diimplementasikan.
Kelas yang include Notifikasi harus mendefinisikan:
def kirim(penerima, pesan)
# implementasi pengiriman notifikasi
end
MSG
end
# "Abstract method" lain
def status_pengiriman(id_notifikasi)
raise NotImplementedError, "#{self.class}#status_pengiriman harus diimplementasikan"
end
# Method dengan implementasi default — tidak wajib di-override
def kirim_massal(daftar_penerima, pesan)
daftar_penerima.map { |penerima| kirim(penerima, pesan) }
end
def dapat_dikirim?
true # default: selalu bisa kirim; override jika ada kondisi
end
end
class NotifikasiEmail
include Notifikasi
def kirim(penerima, pesan)
puts "Email ke #{penerima}: #{pesan}"
{ status: :terkirim, channel: :email, penerima: penerima }
end
def status_pengiriman(id)
puts "Mengecek status email ##{id}"
:terkirim
end
end
class NotifikasiSMS
include Notifikasi
def kirim(penerima, pesan)
# SMS hanya bisa 160 karakter
pesan_dipotong = pesan[0..159]
puts "SMS ke #{penerima}: #{pesan_dipotong}"
{ status: :terkirim, channel: :sms, penerima: penerima }
end
def status_pengiriman(id)
:terkirim
end
def dapat_dikirim?
# Cek kuota SMS
sisa_kuota > 0
end
private
def sisa_kuota
1000 # disederhanakan
end
end
class NotifikasiPush
include Notifikasi
def kirim(penerima, pesan)
puts "Push notification ke device #{penerima}: #{pesan}"
{ status: :diantri, channel: :push, penerima: penerima }
end
def status_pengiriman(id)
:diantri
end
end
# Polimorfisme — semua diperlakukan sama
notifikasi = [NotifikasiEmail.new, NotifikasiSMS.new, NotifikasiPush.new]
notifikasi.each do |n|
n.kirim("[email protected]", "Pesanan kamu telah dikirim!")
end
# kirim_massal bekerja untuk semua karena ada implementasi default di modul
email = NotifikasiEmail.new
email.kirim_massal(["[email protected]", "[email protected]", "[email protected]"], "Promo akhir tahun!")
Polimorfisme Melalui Shared Interface #
Kekuatan duck typing dan modul sebagai interface paling terlihat saat kamu menulis kode yang tidak peduli dengan tipe konkret objek — hanya peduli dengan kemampuannya:
module Dapat_Dirender
def render
raise NotImplementedError, "#{self.class}#render harus diimplementasikan"
end
def render_dengan_wrapper(tag)
"<#{tag}>#{render}</#{tag}>"
end
end
class TeksComponent
include Dapat_Dirender
def initialize(teks)
@teks = teks
end
def render
"<p>#{@teks}</p>"
end
end
class GambarComponent
include Dapat_Dirender
def initialize(src, alt)
@src = src
@alt = alt
end
def render
"<img src='#{@src}' alt='#{@alt}' />"
end
end
class TombolComponent
include Dapat_Dirender
def initialize(label, url)
@label = label
@url = url
end
def render
"<a href='#{@url}' class='btn'>#{@label}</a>"
end
end
class HalamanWeb
def initialize
@komponen = []
end
def tambah(komponen)
raise ArgumentError, "#{komponen.class} tidak bisa dirender" unless komponen.respond_to?(:render)
@komponen << komponen
self
end
def render_semua
@komponen.map(&:render).join("\n")
end
end
halaman = HalamanWeb.new
halaman
.tambah(TeksComponent.new("Selamat datang di toko kami!"))
.tambah(GambarComponent.new("/banner.jpg", "Banner Promo"))
.tambah(TombolComponent.new("Belanja Sekarang", "/produk"))
puts halaman.render_semua
# => <p>Selamat datang di toko kami!</p>
# => <img src='/banner.jpg' alt='Banner Promo' />
# => <a href='/produk' class='btn'>Belanja Sekarang</a>
Perbandingan: Ruby vs Java/C# untuk Interface #
| Aspek | Java / C# | Ruby |
|---|---|---|
| Sintaks | interface Foo { void bar(); } | Modul dengan raise NotImplementedError |
| Verifikasi | Saat kompilasi (compiler) | Saat runtime (NoMethodError) |
| Multiple interface | implements A, B, C | include A; include B; include C |
| Implementasi default | Java 8+: default method | Selalu bisa — method di modul punya body |
| Fleksibilitas | Tipe harus cocok persis | Objek apapun dengan method yang tepat |
| Keamanan tipe | Tinggi — dicek kompiler | Rendah — perlu test coverage yang baik |
| Kebutuhan test | Bisa mengandalkan compiler | Test yang baik sangat penting |
# Keunggulan Ruby: objek dari library pihak ketiga yang tidak include modul
# kamu tetap bisa digunakan jika punya method yang tepat
require 'ostruct'
# OpenStruct dari standard library — tidak include modul apapun
data = OpenStruct.new(render: "<span>OpenStruct bisa dirender!</span>")
halaman = HalamanWeb.new
halaman.tambah(data) # bekerja! karena data.respond_to?(:render) => true
puts halaman.render_semua
# => <span>OpenStruct bisa dirender!</span>
Kapan Gunakan Modul sebagai Interface vs Duck Typing Murni #
Gunakan modul dengan NotImplementedError jika:
✓ Kamu membuat library atau gem yang digunakan orang lain
✓ Butuh dokumentasi yang jelas tentang "kontrak" yang harus dipenuhi
✓ Ada banyak implementasi dan ingin memastikan konsistensi
✓ Method wajib tidak obvious — developer perlu panduan
Cukup duck typing tanpa modul jika:
✓ Kode internal yang hanya digunakan tim sendiri
✓ Method yang dibutuhkan sudah sangat obvious (to_s, each, call)
✓ Objek dari library pihak ketiga yang sudah punya method yang tepat
✓ Konteks sederhana di mana overhead modul tidak sepadan
Gunakan respond_to? jika:
✓ Method yang dibutuhkan opsional — ada perilaku fallback
✓ Ingin mendukung objek dari berbagai sumber tanpa memaksa include modul
✓ Metaprogramming atau proxy pattern
Ringkasan #
- Ruby tidak punya interface formal — dan itu disengaja — duck typing adalah filosofi inti yang lebih fleksibel dari interface statis.
- Modul dengan
NotImplementedErroradalah cara idiomatik untuk mendefinisikan kontrak perilaku — ia memberikan panduan yang jelas kepada implementor tanpa memerlukan compiler.- Modul bisa punya implementasi default — ini keunggulan besar dibanding interface Java tradisional; kelas yang include hanya perlu mengimplementasikan method yang benar-benar perlu dikustomisasi.
- Duck typing: periksa kemampuan, bukan tipe —
perdengarkan_suara(hewan)bekerja untuk objek apapun yang punya methodsuara, tanpa deklarasi formal apapun.respond_to?untuk duck typing yang eksplisit — gunakan ketika method opsional dan ada perilaku fallback yang berbeda.respond_to_missing?wajib didefinisikan jika menggunakanmethod_missing— tanpanya,respond_to?akan mengembalikan hasil yang menyesatkan.includedcallback memungkinkan modul menjalankan kode saat pertama kali di-include — berguna untuk validasi, konfigurasi, atau menambahkan class method otomatis.- Duck typing butuh test yang baik — ketiadaan compiler check berarti kamu harus menggantikannya dengan test coverage yang solid.
- Objek pihak ketiga bisa berpartisipasi — keunggulan terbesar duck typing: library yang tidak tahu tentang modul kamu tetap bisa digunakan selama punya method yang tepat.