Pewarisan

Pewarisan dengan <, memanggil super, dan overriding metode milik kelas induk.

Daftar isi

Pewarisan (inheritance) memungkinkan sebuah kelas menerima metode milik kelas lain, lalu menambahkan atau mengganti sebagiannya. Topik ini membahas pembuatan kelas anak dengan tanda <, penimpaan metode milik induk, penggunaan super dengan dan tanpa argumen, serta urutan pencarian metode yang menyusuri rantai kelas. Setelah membacanya, hierarki kelas seperti Bentuk beserta turunannya dapat dibangun tanpa menyalin kode yang sama berulang kali.

Pewarisan dengan tanda <#

Kelas anak didefinisikan dengan menuliskan class Anak < Induk, dan sejak baris itu seluruh metode milik Induk diwarisi oleh Anak. Ruby menganut pewarisan tunggal, yaitu setiap kelas hanya memiliki satu kelas induk langsung, sehingga rantai ke atasnya selalu berupa satu garis. Kelas anak bebas menambahkan metode baru, dan metode yang tidak didefinisikan ulang tetap dipinjam dari induknya.

Program berikut mendefinisikan kelas Bentuk dengan satu metode deskripsi, lalu dua kelas anak yang masing-masing menyediakan nama dan luas.

ruby bentuk.rb
# Dua kelas anak mewarisi deskripsi dari kelas Bentuk.
class Bentuk
  def deskripsi
    "Ini adalah sebuah #{nama}"
  end
end

class PersegiPanjang < Bentuk
  def initialize(panjang, lebar)
    @panjang = panjang
    @lebar = lebar
  end

  def luas
    @panjang * @lebar
  end

  def nama
    "persegi panjang"
  end
end

class Lingkaran < Bentuk
  def initialize(jari_jari)
    @jari_jari = jari_jari
  end

  def luas
    (Math::PI * @jari_jari**2).round(2)
  end

  def nama
    "lingkaran"
  end
end

segi = PersegiPanjang.new(4, 3)
bundar = Lingkaran.new(2)

puts segi.deskripsi
puts "Luasnya #{segi.luas}"
puts bundar.deskripsi
puts "Luasnya #{bundar.luas}"
text Keluaran
Ini adalah sebuah persegi panjang
Luasnya 12
Ini adalah sebuah lingkaran
Luasnya 12.57

Metode deskripsi hanya ditulis sekali di Bentuk, tetapi segi.deskripsi dan bundar.deskripsi sama-sama berhasil, karena pencarian metode naik ke induk ketika definisinya tidak ditemukan di kelas milik objek. Di dalam deskripsi, pemanggilan nama dieksekusi pada setiap instans, sehingga teks yang tercetak mengikuti kelas masing-masing. Inilah keuntungan langsung pewarisan, yaitu perilaku yang sama ditulis satu kali dan datanya disediakan oleh setiap kelas anak.

Kelas anak mewarisi metode, tetapi tidak mewarisi keadaan instans yang sudah terbentuk. Setiap instans baru tetap menjalankan initialize milik kelasnya sendiri, dan variabel instans yang diisi di kelas anak tidak dimiliki kelas induknya. Dengan pembagian ini, induk menyimpan perilaku bersama, sedangkan setiap anak menyimpan data khasnya masing-masing.

Menimpa metode dan memanggil super#

Metode yang didefinisikan ulang di kelas anak menimpa versi milik induk, dan mekanisme ini disebut overriding. Terkadang versi anak tidak mengganti seluruh pekerjaan induknya, melainkan menambahkan sesuatu, dan di sinilah super digunakan. Kata super di dalam sebuah metode memanggil metode bernama sama yang berada di kelas induk, sehingga pekerjaan induk tidak perlu ditulis ulang.

ruby super-bentuk.rb
# super dengan argumen eksplisit dan super tanpa argumen.
class Bentuk
  def initialize(nama)
    @nama = nama
  end

  def deskripsi(penutup)
    "Sebuah #{@nama}, #{penutup}"
  end
end

class PersegiPanjang < Bentuk
  def initialize(panjang, lebar)
    super("persegi panjang")
    @panjang = panjang
    @lebar = lebar
  end

  def luas
    @panjang * @lebar
  end

  def deskripsi(penutup)
    super("dengan luas #{luas} satuan persegi")
  end
end

class Persegi < PersegiPanjang
  def initialize(sisi)
    super(sisi, sisi)
  end

  def deskripsi(penutup)
    super + ", berupa persegi"
  end
end

persegi = Persegi.new(5)

puts persegi.deskripsi("yang tersusun rapi")
text Keluaran
Sebuah persegi panjang, dengan luas 25 satuan persegi, berupa persegi

Keluaran itu adalah hasil kerja tiga metode yang berantai. Persegi#deskripsi memanggil super tanpa tanda kurung, yang berarti meneruskan argumen penutup apa adanya ke PersegiPanjang#deskripsi, dan PersegiPanjang#deskripsi selanjutnya memanggil super dengan argumen eksplisit berisi hitungan luas menuju Bentuk#deskripsi. Pada initialize, Persegi memanggil super(sisi, sisi) karena parameternya berbeda dari induknya, sedangkan PersegiPanjang memanggil super("persegi panjang") agar @nama diisi oleh Bentuk. Aturannya ringkas, yaitu super tanpa tanda kurung meneruskan argumen yang diterima metode anak, sedangkan super(...) mengirim argumen yang ditulis.

Pemanggilan super sah dari metode mana pun yang menimpa versi induknya, tidak terbatas pada initialize. Sebaliknya, super di dalam metode yang tidak memiliki padanan di rantai ke atas memunculkan NoMethodError, sehingga super hanya ditulis pada metode yang benar-benar menimpa sesuatu. Kebiasaan yang membantu adalah membaca metode induknya terlebih dahulu sebelum menulis ulang versi anaknya, karena pemisahan pekerjaan antara induk dan anak menjadi jelas dari situ.

Rantai pencarian metode#

Pewarisan memperpanjang rantai pencarian metode yang sudah dikenal sebelumnya. Pencarian tetap berangkat dari kelas milik objek penerima, lalu menyusuri kelas induk demi kelas induk sampai definisi yang cocok ditemukan. Metode ancestors melaporkan rantai tersebut secara utuh, mulai dari kelas milik objek hingga ujung paling atas.

ruby Rantai pencarian metode pada pewarisan
class Bentuk
  def deskripsi
    "sebuah bentuk"
  end
end

class PersegiPanjang < Bentuk
end

persegi = PersegiPanjang.new

p persegi.deskripsi
p persegi.method(:deskripsi).owner
p PersegiPanjang.ancestors
text Keluaran
"sebuah bentuk"
Bentuk
[PersegiPanjang, Bentuk, Object, Kernel, BasicObject]

Daftar [PersegiPanjang, Bentuk, Object, Kernel, BasicObject] adalah urutan yang dilalui Ruby pada setiap pemanggilan, dan definisi pertama yang ditemukan langsung digunakan. Metode deskripsi tidak ada di PersegiPanjang, lalu ditemukan di Bentuk, sebagaimana dikonfirmasi oleh owner yang melaporkan Bentuk. Karena definisi pertama menang, metode yang ditimpa di kelas anak selalu mengalahkan versi induknya pada rantai yang sama.

Rantai pada contoh pendek karena belum ada modul campuran di dalamnya. Pada program yang menggunakan include, daftar ancestors juga menyisipkan modul tersebut sesuai letaknya, dan aturan pencariannya tetap sama, yaitu definisi pertama yang ditemukan ketika daftar disusuri dari kiri ke kanan. Membiasakan diri membaca ancestors membantu menjelaskan asal metode yang tidak didefinisikan di kelas itu sendiri.

Kesalahan umum#

Kesalahan yang lazim pada pewarisan adalah mendefinisikan initialize di kelas anak tanpa memanggil super. Definisi baru itu menimpa seluruh initialize milik induk, sehingga penyiapan yang dilakukan induk tidak pernah berjalan dan variabel instansnya dibiarkan kosong.

ruby initialize anak tanpa memanggil super
class Bentuk
  def initialize(nama)
    @nama = nama
  end

  def nama
    @nama
  end
end

class Lingkaran < Bentuk
  def initialize(jari_jari)
    @jari_jari = jari_jari
  end

  def jari_jari
    @jari_jari
  end
end

bundar = Lingkaran.new(2)

p bundar.jari_jari
p bundar.nama
text Keluaran
2
nil

Nilai nil pada baris terakhir muncul karena @nama tidak pernah diisi, dan kerusakan semacam ini sering baru terlihat pada bagian program yang jauh dari penyebabnya. Perbaikannya adalah memanggil super di awal initialize kelas anak dengan argumen yang diminta induknya, misalnya super("lingkaran"), sehingga penyiapan milik induk tetap berjalan sebelum data milik anak diisi. Apabila metode lain milik induk bergantung pada penyiapan itu, kerusakannya menyebar ke metode lain yang dipanggil pada instans tersebut. Karena itu, setiap initialize pada kelas anak diawali pemanggilan super, lalu data khas anak diisi sesudahnya. Pemeriksaan sederhana dilakukan segera setelah kelas anak ditulis, yaitu memanggil metode milik induk pada instans barunya, karena nilai yang keliru akan terlihat pada percobaan pertama.

Ringkasan#

  • Kelas anak ditulis dengan class Anak < Induk dan menerima seluruh metode milik induknya, dengan satu induk langsung per kelas.
  • Metode yang didefinisikan ulang pada anak menimpa versi induk karena definisi pertama pada rantai pencarian yang menang.
  • super tanpa tanda kurung meneruskan argumen metode anak apa adanya, sedangkan super(...) mengirim argumen yang ditulis.
  • ancestors melaporkan rantai kelas tempat metode dicari, dan initialize pada anak perlu memanggil super agar penyiapan induk tetap berjalan.

Referensi#