Mengimplementasikan Interface

Mengimplementasikan interface Java secara anonim dengan proxy dan reify.

Daftar isi

Sebagian besar pustaka Java dibangun di atas interface, yaitu kontrak berisi daftar metode yang harus dipenuhi oleh objek apa pun yang menyebut interface tersebut. Pengurutan, pemrosesan latar, dan penanganan peristiwa pada Java semuanya menerima objek pemenuh kontrak, bukan fungsi tunggal. Topik ini membahas dua bentuk implementasi anonim Clojure untuk kebutuhan itu: reify yang mengimplementasikan interface dan protocol, serta proxy yang memperluas sebuah kelas. Keduanya memperlihatkan cara mengirim objek yang memenuhi kontrak Java tanpa mendefinisikan satu pun kelas bernama.

Kebutuhan implementasi anonim#

Pada Java, memenuhi kontrak interface berarti menulis sebuah kelas dengan pernyataan implements, memberi nama kelas itu, lalu mengonstruksi objeknya di tempat lain. Pada Clojure, implementasi tersebut dapat ditulis langsung di tempat objek dibutuhkan, sehingga tidak ada nama kelas baru yang harus dikelola. Bentuk seperti ini disebut implementasi anonim karena objeknya dibuat tanpa deklarasi nama kelas. Clojure menyediakan dua macro untuknya. reify, dari kata yang berarti menjadikan nyata, menghasilkan objek dari satu atau beberapa interface sekaligus tanpa kelas basis. proxy menghasilkan objek yang memperluas satu kelas tertentu dan dapat memanggil metode warisannya. Keduanya menghasilkan objek JVM yang sesungguhnya, sehingga dapat diperiksa dengan instance? dan dikirim ke API Java mana pun. Nilai dari lingkup sekitar yang dirujuk di dalam implementasi ikut tertangkap sebagai closure, kumpulan nilai yang terikat pada fungsi saat fungsi itu dibuat.

reify untuk interface#

Bentuk dasar reify adalah (reify Interface (metode [this arg] isi)), yaitu nama interface diikuti satu blok implementasi per metode. Parameter pertama setiap implementasi adalah objeknya sendiri dan lazim diberi nama this, mengikuti konvensi Java. Contoh berikut menggunakan java.util.Comparator, interface yang menentukan urutan dua nilai, untuk mengurutkan ArrayList berisi map siswa berdasarkan field skor.

clojure Comparator reify untuk mengurutkan ArrayList
(def berdasarkan-skor
  (reify java.util.Comparator
    (compare [this a b]
      (compare (:skor a) (:skor b)))))

(def daftar
  (java.util.ArrayList.
    [{:nama "Citra" :skor 91}
     {:nama "Ali" :skor 82}
     {:nama "Dewi" :skor 66}
     {:nama "Budi" :skor 74}]))

(java.util.Collections/sort daftar berdasarkan-skor)
(println (str daftar))
text Keluaran
[{:nama "Dewi", :skor 66}, {:nama "Budi", :skor 74}, {:nama "Ali", :skor 82}, {:nama "Citra", :skor 91}]

Metode compare wajib mengembalikan bilangan negatif bila a lebih dahulu, nol bila setara, dan positif bila b lebih dahulu. Fungsi compare milik Clojure sudah mengikuti kontrak itu, sehingga badan implementasi cukup mendelegasikan perbandingan nilai :skor. Collections/sort mengurutkan ArrayList di tempat sesuai semantik Java, dan keluarannya memperlihatkan urutan skor naik dari 66 hingga 91. Map di dalam daftar dicetak dengan gaya Clojure karena toString milik koleksi Clojure menggunakan pr-str, sedangkan pemisah koma berasal dari toString milik List Java.

Nilai lingkup sekitar melalui closure#

reify tidak menyediakan field, dan inilah yang membedakannya dari kelas Java. Kebutuhan akan nilai yang dipegang implementasi dijawab dengan closure: sebuah fungsi pembuat menerima parameter, lalu reify di dalamnya merujuk parameter tersebut. Setiap pemanggilan fungsi pembuat menghasilkan pembanding dengan nilai terikat yang berbeda, dan tidak ada field yang harus dikelola.

clojure Pembanding dengan batas yang ditangkap closure
(defn pembagi-nilai
  [batas]
  (reify java.util.Comparator
    (compare [this a b]
      (compare (mod a batas) (mod b batas)))))

(def angka (java.util.ArrayList. [7 12 3 18 5]))
(java.util.Collections/sort angka (pembagi-nilai 4))
(println (str angka))
text Keluaran
[12, 5, 18, 7, 3]

Contoh ini mengurutkan berdasarkan sisa pembagian empat. Nilai 12 menghasilkan sisa 0 sehingga berada paling awal, disusul 5 dengan sisa 1 dan 18 dengan sisa 2. Nilai 7 dan 3 sama-sama bersisa 3, dan keduanya mempertahankan urutan aslinya karena pengurutan pada Java bersifat stabil. Apabila fungsi pembuatnya dipanggil lagi dengan batas lain, pembanding yang baru dibuat menggunakan nilai barunya, sedangkan pembanding lama tidak tersentuh. Immutability closure menjadikan perilaku ini terprediksi tanpa penguncian tambahan.

proxy untuk kelas#

proxy digunakan ketika implementasi harus berbasis pada sebuah kelas, misalnya memperluas perilaku ArrayList dengan mengawasi satu metodenya. Bentuknya (proxy [Kelas] [arg-konstruktor] (metode [this arg] isi)). Vector pertama memuat kelas basis, dan vector kedua memuat argumen konstruktornya yang boleh kosong. Di dalam badan implementasi, proxy-super memanggil versi metode milik kelas induk.

clojure proxy dengan override metode add
(def daftar-dikawal
  (proxy [java.util.ArrayList] []
    (add [x]
      (if (pos? x)
        (proxy-super add x)
        false))))

(println (.add daftar-dikawal 5))
(println (.add daftar-dikawal 12))
(println (.add daftar-dikawal -2))
(println (str daftar-dikawal))
text Keluaran
true
true
false
[5, 12]

Implementasi add di atas hanya menerima bilangan positif. Pemanggilan dengan 5 dan 12 melanjutkan ke proxy-super add, sehingga elemen benar-benar masuk dan metode mengembalikan true. Pemanggilan dengan -2 langsung mengembalikan false tanpa menyentuh isi daftar. Nilai balik boolean dipertahankan karena kontrak List.add menuntutnya, dan API Java yang memanggil metode ini mengandalkan nilai tersebut. Bila kelas basis memerlukan argumen konstruktor, misalnya kapasitas awal, argumen itu ditulis pada vector kedua seperti (proxy [java.util.ArrayList] [16] ...).

Memilih reify atau proxy#

Tabel berikut merangkum perbedaan keduanya.

Aspekreifyproxy
Basissatu atau beberapa interfacesatu kelas basis, boleh ditambah interface
Metode warisantidak tersediadapat dipanggil lewat proxy-super
Argumen konstruktortidak didukungdidukung pada vector kedua
Stateclosure yang immutableclosure yang immutable

reify adalah pilihan yang disarankan karena bentuknya lebih ringkas dan tidak menarik perilaku tersembunyi dari kelas basis. Pilihan jatuh pada proxy ketika API Java menuntut subclass tertentu, ketika implementasi perlu memanggil implementasi induk, atau ketika konstruktor kelas basis harus diberi argumen. Keduanya sama-sama tidak membawa field, sehingga pola fungsi pembuat dengan closure berlaku sama. Apabila kebutuhannya belum jelas, mulailah dari reify dan pindah ke proxy hanya ketika kendala kelas basis benar-benar muncul.

Kesalahan umum#

  • Menulis parameter implementasi tanpa this, misalnya (compare [a b] ...). Signature interface tidak terpenuhi, dan error baru muncul saat objek digunakan, biasanya berupa AbstractMethodError. Cantumkan selalu objek itu sendiri sebagai parameter pertama.
  • Mengurutkan koleksi Clojure dengan Collections/sort, misalnya atas [3 1 2]. Vector Clojure immutable, sehingga pengurutan di tempat menimbulkan error saat dijalankan. Salin dahulu ke ArrayList, urutkan, lalu konversi kembali bila perlu.
  • Mengembalikan nilai dengan tipe yang salah dari implementasi, misalnya angka dari metode add yang menuntut boolean. Pemanggil dari sisi Java menerima ClassCastException. Cocokkan tipe nilai balik dengan kontrak metodenya.
  • Mengira reify membawa field seperti kelas. Implementasi hanya dapat merujuk parameter metodenya dan nilai yang ditangkap closure. Kebutuhan state yang berubah dijawab dengan atom, bukan field.

Ringkasan#

  • reify menghasilkan objek anonim yang mengimplementasikan interface dan protocol, dengan this sebagai parameter pertama setiap metode.
  • Nilai lingkup sekitar tertangkap sebagai closure, sehingga implementasi dapat diparameterisasi lewat fungsi pembuatnya.
  • proxy memperluas satu kelas basis, menerima argumen konstruktor, dan menyediakan proxy-super untuk memanggil implementasi induk.
  • reify menjadi pilihan default, sedangkan proxy digunakan ketika kelas basis atau metode warisannya benar-benar diperlukan.

Referensi#