Mixin

Berbagi perilaku dengan include dan extend, serta membaca jalur pencarian metode lewat ancestors.

Daftar isi

Modul di Ruby juga berperan sebagai wadah metode yang dapat dilekatkan ke kelas mana pun, dan mekanisme ini dikenal sebagai mixin. Topik ini membahas cara kerja include yang mengubah metode modul menjadi metode instans, extend yang menempelkan metode modul pada satu objek, pembacaan jalur pencarian metode lewat ancestors, serta pertimbangan memilih mixin atau pewarisan. Setelah membacanya, pembagian perilaku antarkelas dapat dirancang tanpa menumpuk hierarki kelas.

include: metode modul menjadi metode instans#

Metode yang didefinisikan di dalam modul menjadi milik instans pada setiap kelas yang menyertakannya lewat include. Kelas yang berbeda dapat menyertakan modul yang sama, sehingga satu definisi perilaku digunakan di banyak tempat tanpa saling mewarisi. Contoh berikut menempatkan metode deskripsi di dalam modul Deskriptif, lalu kelas Buku menyertakannya.

ruby deskriptif.rb
# Modul Deskriptif berbagi metode deskripsi kepada kelas mana pun yang menyertakannya.
module Deskriptif
  def deskripsi
    "#{judul} karya #{penulis}"
  end
end

class Buku
  include Deskriptif

  attr_reader :judul, :penulis

  def initialize(judul, penulis)
    @judul = judul
    @penulis = penulis
  end
end

buku = Buku.new("Kamus Kecil", "Dewi")
puts buku.deskripsi
puts buku.is_a?(Deskriptif)
text Keluaran
Kamus Kecil karya Dewi
true

Metode deskripsi berjalan mulus padahal badan metode itu membaca judul serta penulis yang tidak didefinisikan di dalam modul. Hal ini terjadi karena pemanggilan metode dijalankan pada objek penerima, sehingga metode pembaca judul dan penulis milik Buku tetap terjangkau dari badan modul. Metode is_a? pada baris terakhir mengonfirmasi keanggotaan itu, yaitu objek Buku juga tergolong instans dari modul Deskriptif. Pola seperti inilah yang menjadikan modul tempat berbagi kemampuan, dan bukan sekadar wadah konstanta seperti pada pembahasan namespace.

ancestors dan urutan pencarian metode#

Setiap pemanggilan metode disusuri menurut urutan tetap yang disebut method lookup, dan daftar lengkapnya dapat dibaca lewat ancestors. Pada kasus yang melibatkan mixin, Ruby memeriksa kelas milik objek terlebih dahulu, kemudian modul yang disertakan oleh kelas tersebut, lalu naik ke kelas induk beserta modul-modul yang disertakan kelas induk itu, sampai definisi ditemukan. Diagram berikut memperlihatkan jalur tersebut untuk kelas Suhu yang menyertakan DapatDihitung.

kelasmixinleluhur bawaanSuhuDapatDihitungObjectKernelBasicObjectPencarian bergerak dari kiri ke kanan dan berhenti pada kotak pertama yang memiliki metodenya.
Jalur pencarian metode dari kelas menuju leluhur bawaan

Program berikut menempatkan metode hitung di modul DapatDihitung, lalu membandingkan kelas yang menyertakan modul dengan subkelas yang mendefinisikan metode bernama sama.

ruby urutan-pencarian.rb
# Urutan pencarian metode: kelas terlebih dahulu, lalu modul yang disertakan.
module DapatDihitung
  def hitung
    :dari_modul
  end
end

class Suhu
  include DapatDihitung
end

class SuhuKalibrasi < Suhu
  def hitung
    :dari_kelas
  end
end

puts Suhu.new.hitung
puts SuhuKalibrasi.new.hitung
p Suhu.ancestors
p SuhuKalibrasi.ancestors
text Keluaran
dari_modul
dari_kelas
[Suhu, DapatDihitung, Object, Kernel, BasicObject]
[SuhuKalibrasi, Suhu, DapatDihitung, Object, Kernel, BasicObject]

Pemanggilan pertama menemukan hitung di dalam modul karena kelas Suhu tidak mendefinisikannya, sedangkan pemanggilan kedua menemukan versi kelas lebih dahulu sehingga menghasilkan :dari_kelas. Daftar ancestors menjelaskan kedua hasil itu secara persis, yaitu Suhu mendahului DapatDihitung pada daftar pertama, sedangkan pada daftar kedua SuhuKalibrasi dan Suhu mendahului DapatDihitung milik Suhu. Pemeriksaan pun bergerak dari kiri ke kanan, sehingga metode milik kelas selalu diperiksa lebih dahulu daripada metode milik modul yang menyertainya.

Metode modul juga dapat meneruskan pemanggilan dengan super, seolah-olah posisi modul berada di tengah rantai pencarian. Program berikut menyertakan DapatDihitung pada subkelas, lalu metode hitung dari modul meneruskan pemanggilan ke versi kelas induk lewat super.

ruby rantai-super.rb
# Metode modul dapat memanggil super untuk meneruskan ke rantai pencarian.
module DapatDihitung
  def hitung
    "modul + #{super}"
  end
end

class Suhu
  def hitung
    "hitungan dasar"
  end
end

class SuhuKalibrasi < Suhu
  include DapatDihitung
end

puts SuhuKalibrasi.new.hitung
p SuhuKalibrasi.ancestors
text Keluaran
modul + hitungan dasar
[SuhuKalibrasi, DapatDihitung, Suhu, Object, Kernel, BasicObject]

Keluarannya berupa gabungan dua tahap, yaitu tambahan dari modul diikuti hitungan dasar dari kelas Suhu. Daftar ancestors pada baris terakhir menunjukkan DapatDihitung terjepit di antara keduanya, dan super cukup mengikuti daftar itu ke kanan. Susunan tersebut memperlihatkan aturan umumnya, yaitu kelas dahulu, kemudian mixin milik kelas itu, baru induk beserta modul-modul induknya. Dengan mekanisme ini, mixin aman digunakan untuk membungkus perilaku di sekitar metode yang sudah ada tanpa menyentuh definisi aslinya.

extend: metode modul menjadi metode kelas#

extend melekatkan metode modul pada satu objek tertentu, dan apabila objek itu sebuah kelas, metode tersebut menjadi metode kelas. Perbedaan utama dengan include terletak pada penerimanya, yaitu include bekerja pada kelas dan menggolongkan metode ke sisi instans, sedangkan extend bekerja pada objek langsung dan menempelkan metodenya pada sisi kelas dari objek tersebut.

ruby extend.rb
# extend menempelkan metode modul sebagai metode milik objek penerimanya.
module EksporCsv
  def ekspor_csv(daftar)
    daftar.join(",")
  end
end

class Sensor
  extend EksporCsv
end

puts Sensor.ekspor_csv([20, 21, 22])

begin
  Sensor.new.ekspor_csv([20])
rescue NoMethodError => e
  puts e.class
  puts e.message
end

satuan = Sensor.new
satuan.extend(EksporCsv)
puts satuan.ekspor_csv([1, 2, 3])
text Keluaran
20,21,22
NoMethodError
undefined method 'ekspor_csv' for an instance of Sensor
1,2,3

Pemanggilan Sensor.ekspor_csv berhasil karena extend di dalam badan kelas menempelkan metode itu pada kelas Sensor. Pemanggilan kedua gagal dan menunjukkan sisi lainnya, yaitu instans Sensor tidak menerima metode tersebut, karena extend pada kelas tidak memasukkan apa pun ke rantai pencarian metode instans. Pemanggilan terakhir memperlihatkan bentuk umumnya, yaitu extend dapat dipanggil pada objek mana pun bahkan setelah objek dibuat, sehingga satu instans tertentu memperoleh kemampuan tambahan tanpa mengubah instans lainnya.

Mixin atau pewarisan#

Pewarisan menggambarkan hubungan sejenis, yaitu SuhuKalibrasi memang sejenis Suhu, sedangkan mixin menggambarkan hubungan memiliki kemampuan, sehingga kelas mana pun dapat mendeskripsikan dirinya lewat modul Deskriptif. Aturan praktisnya, pewarisan dipilih apabila dua kelas memang sejenis dan berbagi keadaan, sedangkan mixin dipilih apabila kemampuannya berdiri sendiri dan layak dilekatkan ke banyak kelas yang tidak saling berhubungan. Sebuah kelas juga dapat menyertakan lebih dari satu modul, sedangkan Ruby hanya mengizinkan satu kelas induk, sehingga mixin menyelesaikan kebutuhan berbagi perilaku dari beberapa arah sekaligus.

KebutuhanPilihan yang tepat
Hubungan sejenis antara dua kelaspewarisan dengan <
Perilaku tambahan untuk instans dari banyak kelas yang tidak saling berhubunganinclude pada modul bersama
Perilaku tambahan pada sisi kelas atau pada satu objek tertentuextend

Ketiga pilihan itu saling melengkapi dan sering digunakan bersama dalam satu kelas, sebagaimana daftar ancestors pada contoh di atas yang memuat kelas, mixin, dan leluhur bawaan sekaligus. Pustaka standar Ruby juga dibangun dengan pola yang sama, dan penerapannya yang paling terkenal adalah modul Comparable dan Enumerable.

Kesalahan umum#

Kesalahan yang sering terjadi adalah menganggap seluruh metode modul ikut pindah lewat include, padahal metode yang didefinisikan dengan def self. tetap menempel pada modulnya. Program berikut mendefinisikan metode rata_rata dengan def self. di modul Statistik, lalu kelas NilaiUjian menyertakan modul tersebut dan pemanggilan dari instans gagal.

ruby metode-self.rb
# Metode berawalan def self. tidak ikut saat modul disertakan lewat include.
module Statistik
  def self.rata_rata(daftar)
    daftar.sum / daftar.length
  end
end

class NilaiUjian
  include Statistik
end

begin
  NilaiUjian.new.rata_rata([80, 90])
rescue NoMethodError => e
  puts e.class
  puts e.message
end

puts Statistik.rata_rata([80, 90])
text Keluaran
NoMethodError
undefined method 'rata_rata' for an instance of NilaiUjian
85

Metode rata_rata tetap dapat dipanggil langsung pada modulnya, sebagaimana baris terakhir program, karena def self. memang mendefinisikan metode milik objek Statistik sendiri. Perbaikannya mengikuti maksud definisi, yaitu menggunakan def biasa di dalam modul apabila metode ditujukan untuk instans lewat include, atau menggunakan extend apabila metode dimaksudkan sebagai metode kelas. Membedakan keduanya sejak awal menjaga modul tetap netral terhadap cara penyertaannya.

Ringkasan#

  • include menyertakan modul ke dalam kelas sehingga metode modul menjadi metode instans yang dapat membaca keadaan objek.
  • extend menempelkan metode modul pada satu objek, dan hasilnya menjadi metode kelas apabila penerimanya sebuah kelas.
  • ancestors menampilkan urutan pencarian metode, yaitu kelas dahulu, lalu mixin, kemudian induk beserta mixin induknya, sampai BasicObject.
  • Metode dengan def self. tidak berpindah lewat include, dan mixin dipilih apabila kemampuan berdiri sendiri, sedangkan pewarisan dipilih apabila kedua kelas memang sejenis.

Referensi#

  • Module — Ruby — Dokumentasi include, ancestors, dan konstanta modul pada kelas Module.
  • Object#extend — Ruby — Dokumentasi extend beserta penjelasan penempelannya pada satu objek.