Interface

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 #

AspekJava / C#Ruby
Sintaksinterface Foo { void bar(); }Modul dengan raise NotImplementedError
VerifikasiSaat kompilasi (compiler)Saat runtime (NoMethodError)
Multiple interfaceimplements A, B, Cinclude A; include B; include C
Implementasi defaultJava 8+: default methodSelalu bisa — method di modul punya body
FleksibilitasTipe harus cocok persisObjek apapun dengan method yang tepat
Keamanan tipeTinggi — dicek kompilerRendah — perlu test coverage yang baik
Kebutuhan testBisa mengandalkan compilerTest 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 NotImplementedError adalah 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 tipeperdengarkan_suara(hewan) bekerja untuk objek apapun yang punya method suara, 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 menggunakan method_missing — tanpanya, respond_to? akan mengembalikan hasil yang menyesatkan.
  • included callback 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.

← Sebelumnya: Kelas   Berikutnya: Eksepsi →

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