Pewarisan dan class abstrak

Dispatch subtipe dan kontrak metode abstrak pada hierarki class.

Daftar isi

Pewarisan memungkinkan class baru dibangun di atas class yang sudah ada sambil mewarisi metodenya, sedangkan class abstrak menetapkan kontrak yang wajib dipenuhi turunannya. Topik ini membahas penulisan pewarisan dengan tanda <, kewajiban mengimplementasikan metode abstrak, serta dispatch subtipe (subtype dispatch), yaitu pemanggilan metode milik class asli dari variabel yang bertipe class dasar. Studi kasusnya adalah hierarki class Bentuk dengan perhitungan luasnya.

Pewarisan dengan tanda lebih kecil#

Relasi pewarisan ditulis dengan tanda < pada baris class, misalnya class PersegiPanjang < Bentuk yang menyatakan PersegiPanjang sebagai anak dari Bentuk. Class anak menerima seluruh metode milik class dasarnya, boleh menambah metode baru, dan boleh mendefinisikan ulang metode yang sudah ada dengan nama serta tipe yang sama. Definisi ulang semacam itu disebut override, dan kata kunci super memanggil versi milik class dasar dari dalamnya.

crystal Contoh: memanggil versi class dasar dengan super
# tidak-dijalankan
class PersegiPanjang < Bentuk
  def deskripsi : String
    super + " (empat sisi)"
  end
end

Metode yang tidak didefinisikan ulang diwariskan tanpa perubahan apa pun, sehingga perilaku yang berlaku umum cukup ditulis satu kali pada class dasar. Konstanta milik class dasar ikut tersedia bagi turunannya, sehingga konstanta RODA yang didefinisikan di class Kendaraan dapat digunakan langsung di dalam class Mobil tanpa awalan. Satu class hanya boleh memiliki satu class dasar langsung, tetapi rantai pewarisannya boleh panjang, misalnya class yang menurunkan class yang menurunkan class lain. Relasi ini membentuk hierarki, dan hierarki itulah yang menjadi dasar dispatch subtipe pada bagian akhir topik.

Class abstrak dan abstract def#

Class dasar pada banyak hierarki tidak dirancang untuk dibuat objeknya, misalnya Bentuk yang tanpa panjang, lebar, atau jari-jari tidak memiliki arti luas. Crystal menyediakan abstract class untuk keperluan itu. Pembuatan objek dari class abstrak ditolak compiler dengan pesan can't instantiate abstract class Bentuk, sedangkan turunan konkret dari class tersebut tetap bebas dibuat. Peran class abstrak adalah menjadi penanda tipe sekaligus pemegang kontrak.

Kontrak itu ditulis sebagai abstract def, yaitu deklarasi metode dengan nama dan tipe lengkap tetapi tanpa badan. Setiap class turunan yang konkret wajib menyediakan implementasinya, dan compiler menolak program yang melanggarnya. Program berikut mendeklarasikan abstract def luas : Float64 pada class abstrak, lalu mendefinisikan class Segitiga yang tidak menyediakan metode itu.

crystal bentuk_rusak.cr
abstract class Bentuk
  abstract def luas : Float64
end

class Segitiga < Bentuk
  def initialize(@alas : Float64, @tinggi : Float64)
  end
end

Segitiga.new(6.0, 4.0)
text Keluaran
Showing last frame. Use --error-trace for full trace.

In bentuk_rusak.cr:5:1

 5 | class Segitiga < Bentuk
     ^
Error: abstract `def Bentuk#luas()` must be implemented by Segitiga

Pesan error menunjuk langsung ke baris class yang menyalahi kontrak, sehingga pelanggarannya ketahuan sebelum program dijalankan. Perbaikannya satu-satunya, yaitu mendefinisikan luas di dalam Segitiga dengan tipe balik Float64. Mekanisme ini membuat kontrak pada class abstrak menjadi jaminan, bukan sekadar dokumen: selama program berhasil di-compile, setiap objek konkret di bawah Bentuk pasti dapat menghitung luasnya.

Dispatch subtipe#

Variabel yang bertipe class dasar boleh diisi objek dari salah satu turunannya. Compiler menangani keadaan itu dengan membentuk tipe virtual, yang ditulis Bentuk+ dan berarti objek bertipe Bentuk atau salah satu turunannya. Metode yang dipanggil melalui variabel semacam itu diarahkan ke implementasi milik class asli objeknya, sehingga pemanggilan yang sama menghasilkan perilaku berbeda sesuai isi variabel.

crystal bentuk.cr
abstract class Bentuk
  abstract def luas : Float64

  def deskripsi : String
    "#{self.class.name} dengan luas #{luas}"
  end
end

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

  def luas : Float64
    @panjang * @lebar
  end
end

class Lingkaran < Bentuk
  PI = 3.14159

  def initialize(@jari : Float64)
  end

  def luas : Float64
    PI * @jari * @jari
  end
end

bentuk : Bentuk = PersegiPanjang.new(4.0, 5.0)
puts bentuk.deskripsi

bentuk = Lingkaran.new(2.0)
puts bentuk.deskripsi

koleksi = [] of Bentuk
koleksi << PersegiPanjang.new(10.0, 2.0)
koleksi << Lingkaran.new(1.0)
koleksi.each do |b|
  puts b.deskripsi
end
bash
# jalankan program bentuk.cr
crystal run bentuk.cr
text Keluaran
PersegiPanjang dengan luas 20.0
Lingkaran dengan luas 12.56636
PersegiPanjang dengan luas 20.0
Lingkaran dengan luas 3.14159

Variabel bentuk dinyatakan bertipe Bentuk, tetapi dua pemanggilan pertamanya menghasilkan deskripsi berbeda karena isinya berganti dari PersegiPanjang menjadi Lingkaran. Pada baris koleksi = [] of Bentuk, tipe elemen array menjadi tipe virtual yang sama, sehingga satu array menampung kedua class tanpa konversi apa pun. Metode deskripsi sendiri hanya ditulis satu kali di class dasar, sedangkan luas yang dipanggilnya diarahkan ke implementasi masing-masing, dan self.class.name menuliskan nama class asli objeknya.

Dispatch yang longgar di satu sisi tetap diperiksa ketat di sisi lain. Metode yang dipanggil melalui tipe virtual harus tersedia pada seluruh anggotanya, karena compiler tidak menjamin objek mana yang sedang dibaca. Program berikut menambahkan metode keliling hanya pada class PersegiPanjang, lalu memanggilnya melalui elemen array bertipe [] of Bentuk yang juga menampung Lingkaran.

crystal bentuk_keliling.cr
abstract class Bentuk
  abstract def luas : Float64
end

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

  def luas : Float64
    @panjang * @lebar
  end

  def keliling : Float64
    2 * (@panjang + @lebar)
  end
end

class Lingkaran < Bentuk
  def initialize(@jari : Float64)
  end

  def luas : Float64
    3.14159 * @jari * @jari
  end
end

koleksi = [] of Bentuk
koleksi << PersegiPanjang.new(4.0, 5.0)
koleksi << Lingkaran.new(2.0)
koleksi.each do |b|
  puts b.keliling
end
text Keluaran
Showing last frame. Use --error-trace for full trace.

In bentuk_keliling.cr:31:10

 31 | puts b.keliling
             ^-------
Error: undefined method 'keliling' for Lingkaran (compile-time type is Bentuk+)

Pesan error menyebut tipe virtual Bentuk+ secara eksplisit sebagai tipe waktu compile. Compiler menolak pemanggilan itu karena Lingkaran tidak mengenal keliling, dan penolakan ini mencegah error yang biasanya baru muncul saat program berjalan pada bahasa yang tidak memeriksa tipe. Perbaikannya mengikuti maksud desain, yaitu mendeklarasikan keliling pada class dasar bersama metode abstrak lainnya apabila seluruh bentuk memang memilikinya, atau memeriksa tipe objek terlebih dahulu sebelum memanggil metode khususnya.

Diagram berikut merangkum hierarki yang digunakan sepanjang topik ini beserta metode yang menjadi tanggung jawab masing-masing class.

Bentukabstract def luas : Float64PersegiPanjangluas = panjang × lebarLingkaranluas = π × jari × jari
Hierarki class Bentuk beserta dua subclass-nya

Ringkasan#

  • Pewarisan ditulis dengan tanda <, dan metode yang tidak didefinisikan ulang diwariskan dari class dasar.
  • Class abstrak tidak dapat dibuat objeknya, sedangkan abstract def menjadi kontrak yang wajib diimplementasikan setiap turunan konkret.
  • Variabel bertipe class dasar memuat tipe virtual seperti Bentuk+ dan memanggil metode milik class asli objeknya.
  • Dispatch subtipe memungkinkan satu kumpulan objek beragam diperlakukan seragam tanpa menghilangkan perilaku spesifiknya.

Referensi#