Trait

Mixin perilaku dengan urutan stacking dan metode bawaan yang dapat diwariskan.

Daftar isi

Trait adalah abstraksi yang mengelompokkan deklarasi metode dan anggota lain agar dapat diwariskan ke banyak kelas, baik sebagai kontrak yang wajib diisi maupun sebagai implementasi bawaan yang langsung dapat digunakan. Selain diwariskan ke kelas, trait juga dapat dicampur (mixin) pada saat sebuah objek dibuat, dan apabila beberapa trait menimpa metode yang sama, implementasinya menumpuk (stacking) menurut urutan pencampurannya. Topik ini membahas deklarasi trait beserta metode bawaannya, pencampuran pada saat instansiasi, urutan penumpukan yang diuji melalui keluaran program, serta linearisasi (linearization) yang menjadi dasar urutan tersebut.

Trait sebagai kontrak dengan metode bawaan#

Trait mendeklarasikan anggota tanpa isi sebagai kontrak, yaitu anggota abstrak yang isinya ditentukan kelas yang mewarisinya. Selain itu, trait boleh memuat metode dengan isi, yaitu metode bawaan yang langsung diwariskan tanpa ditulis ulang. Kombinasi keduanya membuat trait berbeda dari antarmuka murni pada banyak bahasa lain, karena satu deklarasi dapat membagikan perilaku konkret ke semua pewarisnya sekaligus.

Program berikut mendeklarasikan trait QueueEntry untuk entitas antrean loket. Anggota label bersifat abstrak, sedangkan summary() sudah mempunyai isi. Kelas Client mewarisi trait tersebut dan cukup mengisi label, sedangkan kelas VipClient menimpa summary() dengan formatnya sendiri.

scala queue-trait.scala
// Trait untuk entitas antrean: label wajib diisi, ringkasan sudah bawaan
trait QueueEntry:
  // anggota abstrak: isi ditentukan kelas yang mewarisinya
  def label: String

  // metode bawaan: langsung dipakai tanpa ditulis ulang
  def summary(): String = s"Menunggu: $label"

end QueueEntry

// Client mewarisi trait lewat extends dan mengisi anggota abstraknya
class Client(val label: String) extends QueueEntry

// VipClient menimpa metode bawaan dengan implementasi sendiri
class VipClient(val label: String) extends QueueEntry:
  override def summary(): String = s"Prioritas: $label"

end VipClient

@main def queue(): Unit =
  val entries = Vector(Client("A-01"), VipClient("V-01"))
  for e <- entries do println(e.summary())
bash
# menjalankan program queue-trait.scala
scala-cli run queue-trait.scala
text Keluaran
Menunggu: A-01
Prioritas: V-01

Baris pertama berasal dari Client yang menggunakan metode bawaan tanpa perubahan apa pun, sedangkan baris kedua berasal dari VipClient yang menimpanya. Penimpaan metode yang sudah memiliki isi wajib menuliskan kata override, sehingga compiler menolak program yang mengubah metode bawaan secara tidak sengaja. Pola kontrak dan implementasi bawaan ini menjaga seluruh pengguna trait tetap berpegang pada persyaratan yang sama.

Mixin dan urutan penumpukan metode#

Mixin menambahkan perilaku trait ke sebuah kelas atau objek melalui kata kunci with. Satu kelas hanya mewarisi satu kelas induk, tetapi boleh mencampur banyak trait, dan trait juga dapat dicampur langsung pada ekspresi new, sehingga objek yang dihasilkan bertipe gabungan kelas induk dan trait yang dicampur. Apabila lebih dari satu trait menimpa metode yang sama dengan panggilan super, implementasinya tidak saling menggantikan, melainkan membentuk modifikasi berlapis yang dikenal sebagai stacking. Setiap lapisan mengubah data sebelum meneruskannya ke lapisan berikutnya, sehingga hasil akhirnya bergantung pada urutan pencampuran trait.

Program berikut menggunakan antrean bilangan bulat IntQueue beserta dua trait yang menumpuk pada metode put. Trait Incrementing menambah satu pada setiap angka sebelum meneruskannya, sedangkan NegativeFilter menahan angka negatif.

scala queue-stacking.scala
// Antrean angka dasar: menyimpan urutan masukan apa adanya
class IntQueue:
  private var items: List[Int] = List.empty

  // metode yang akan ditumpuk oleh trait lain
  def put(x: Int): Unit = items = items :+ x

  def all: List[Int] = items

end IntQueue

// menambah 1 pada setiap angka sebelum diteruskan ke super
trait Incrementing extends IntQueue:
  abstract override def put(x: Int): Unit =
    super.put(x + 1)

// menolak angka negatif
trait NegativeFilter extends IntQueue:
  abstract override def put(x: Int): Unit =
    if x >= 0 then super.put(x)

end NegativeFilter

@main def stacking(): Unit =
  // trait paling kanan dieksekusi lebih dulu: filter sebelum penambah
  val a = new IntQueue with Incrementing with NegativeFilter
  a.put(1)
  a.put(-1)
  a.put(3)
  println(a.all)

  // urutan mixin dibalik: penambah dieksekusi lebih dulu
  val b = new IntQueue with NegativeFilter with Incrementing
  b.put(1)
  b.put(-1)
  b.put(3)
  println(b.all)
bash
# menjalankan program queue-stacking.scala
scala-cli run queue-stacking.scala
text Keluaran
List(2, 4)
List(2, 0, 4)

Pada baris pertama keluaran, trait dicampur dengan urutan with Incrementing with NegativeFilter. Panggilan put(-1) ditahan oleh filter karena filter berjalan sebelum penambah, sehingga angka itu tidak pernah masuk antrean. Pada baris kedua urutannya dibalik menjadi with NegativeFilter with Incrementing, sehingga penambah berjalan lebih dulu dan nilai -1 berubah menjadi 0 yang lolos filter. Dua keluaran yang berbeda itu murni berasal dari perubahan urutan mixin, tanpa satu baris logika pun yang diubah.

Linearisasi: sumber urutan yang pasti#

Urutan penumpukan pada bagian sebelumnya bukan kebetulan, melainkan akibat dari linearisasi, yaitu urutan tunggal seluruh tipe pembentuk sebuah kelas atau objek yang dihitung compiler secara deterministik. Panggilan super pada metode yang ditumpuk tidak menuju kelas induk secara langsung, tetapi menuju anggota berikutnya dalam linearisasi. Pada new IntQueue with Incrementing with NegativeFilter, eksekusi masuk melalui NegativeFilter sebagai lapisan terluar, lalu bergerak ke Incrementing, dan berakhir pada IntQueue.

Diagram berikut menggambarkan alur tiga lapis tersebut beserta transformasi yang dilakukan setiap lapisannya.

Urutan pemanggilan put pada mixin tiga lapisdijalankan lebih duludijalankan terakhirput(x)NegativeFilterif x >= 0Incrementingx + 1IntQueueitems :+ xsuper.put(x)super.put(x+1)new IntQueue with Incrementing with NegativeFilter
Urutan pemanggilan put pada mixin tiga lapis

Anotasi “dijalankan lebih dulu” dan “dijalankan terakhir” menegaskan bahwa lapisan paling kanan pada deklarasi mixin justru dieksekusi paling awal, sedangkan kelas dasarnya dieksekusi paling akhir. Sifat deterministik ini penting karena urutan yang sama selalu menghasilkan perilaku yang sama. Keluaran program pada bagian sebelumnya adalah buktinya, yaitu mixin tertentu selalu menghasilkan List(2, 4), sedangkan mixin terbaliknya selalu menghasilkan List(2, 0, 4).

Kesalahan umum#

Tiga kesalahan yang sering muncul pada trait yang menumpuk adalah sebagai berikut. Pertama, urutan eksekusi dianggap mengikuti urutan penulisan kiri ke kanan. Penyebabnya adalah panggilan super yang bergerak ke kiri dalam linearisasi, sehingga trait paling kanan justru berjalan lebih dulu, dan perbaikannya adalah menguji urutan dengan keluaran program seperti pada contoh di bagian sebelumnya. Kedua, trait penumpuk tidak mewarisi tipe dasar yang sama dengan metode yang ditumpuknya. Compiler menolak program dengan pesan error sebelum program dijalankan, dan perbaikannya adalah membuat trait mewarisi tipe dasar yang sama. Ketiga, panggilan super.put ditulis pada trait yang tipe dasarnya tidak mendeklarasikan metode tersebut. Kasus ini juga ditolak compiler karena tidak ada implementasi yang dapat ditumpuk, sehingga metode yang ditumpuk wajib sudah ada pada tipe dasarnya.

Ringkasan#

  • Trait menggabungkan anggota abstrak sebagai kontrak dan metode bawaan yang langsung dapat diwariskan ke kelas turunannya.
  • Mixin melalui with menambahkan perilaku trait pada deklarasi kelas maupun pada ekspresi new, dan beberapa trait dapat dicampur sekaligus.
  • Ketika beberapa trait menimpa metode yang sama, implementasinya menumpuk menurut urutan mixin, dan lapisan paling kanan dieksekusi lebih dulu.
  • Urutan itu ditetapkan oleh linearisasi yang dihitung compiler secara deterministik, sehingga keluaran program untuk mixin yang sama selalu sama.

Referensi#