Rancang Tipe Lebih Dulu

Menyusun arsitektur tipe dan dispatch terlebih dahulu sebelum menulis implementasi.

Daftar isi

Rancangan tipe menetapkan bentuk data dan daftar fungsi beserta metodenya sebelum badan implementasi ditulis. Topik ini membahas alasan menyusun arsitektur tipe lebih dahulu dan mempraktikkannya pada proyek kecil penghitung tarif parkir, mulai dari definisi struct, fungsi biaya dengan multiple dispatch, hingga pengujian yang menjaga kontrak rancangan tetap terpenuhi.

Mengapa tipe dirancang lebih dahulu#

Definisi struct dan signature fungsi menentukan hampir seluruh keputusan implementasi, sehingga keduanya layak ditetapkan lebih dahulu. Struct mendefinisikan field dan tipe tiap field, yaitu kontrak data yang dipegang seluruh program. Daftar metode sebuah fungsi mendefinisikan kontrak perilaku, yaitu kombinasi tipe mana yang dilayani dan kombinasi mana yang memunculkan MethodError. Implementasi hanya mengisi badan fungsi di atas dua kontrak tersebut.

Apabila implementasi ditulis lebih dahulu, kebutuhan tipe baru sering terlihat di tengah jalan. Menambah field pada struct yang sudah dipakai di banyak tempat, atau mengubah tipe fieldnya, memaksa perombakan pada pemanggilan yang sudah ditulis. Keputusan tipe yang dibuat lebih dahulu menempatkan perubahan pada tahap yang termurah, yaitu tahap ketika belum ada pemanggil yang bergantung.

Rancangan tipe tidak menuntut dokumen formal. Cukup satu berkas Julia yang berisi definisi struct, definisi fungsi beserta metodenya, dan beberapa contoh pemanggilan. Berkas itu dijalankan untuk memastikan rancangannya berjalan, lalu menjadi titik awal implementasi sesungguhnya. Bentuk rancangan semacam ini juga langsung dapat diuji, sebagaimana ditunjukkan pada bagian akhir topik ini.

Rancangan tipe penghitung tarif parkir#

Proyek kecil pada topik ini menghitung biaya parkir dari durasi dan tarif per jam per jenis kendaraan. Rancangannya menetapkan tiga keputusan. Durasi disimpan sebagai Int karena tarif dihitung untuk jam bulat. Tarif dipisahkan dari kendaraan dan disimpan pada struct TarifJam, sehingga perubahan tarif tidak menyentuh tipe kendaraan mana pun. Jenis kendaraan dinyatakan sebagai tipe, bukan sebagai string, sehingga pemilihan metode diserahkan kepada dispatch.

julia rancangan.jl
abstract type Kendaraan end

struct Motor <: Kendaraan
    jam::Int
end

struct Mobil <: Kendaraan
    jam::Int
end

struct TarifJam
    motor::Int
    mobil::Int
end

tarif = TarifJam(1000, 2000)

biaya(t::TarifJam, k::Motor) = t.motor * k.jam
biaya(t::TarifJam, k::Mobil) = t.mobil * k.jam

println(biaya(tarif, Motor(3)))
println(biaya(tarif, Mobil(2)))
println(length(methods(biaya)))
text Keluaran
3000
4000
2

Definisi di atas memperlihatkan rancangan dalam bentuk paling ringkasnya. Tipe abstrak Kendaraan menjadi induk yang mengelompokkan jenis kendaraan, sedangkan Motor dan Mobil adalah tipe konkret dengan field jam beranotasi Int. Fungsi biaya memuat dua metode, satu untuk setiap jenis, dan tidak ada pencabangan di dalam badannya. Pemanggilan terakhir menampilkan 2, yaitu jumlah metode biaya, sebagai penegasan bahwa dispatch-nya terdefinisi per jenis. Menambahkan jenis baru, misalnya bus, berarti menambah satu struct dan satu metode tanpa menyentuh metode yang sudah ada.

Kekonkretan field dan stabilitas tipe#

Rancangan tipe menentukan kecepatan program melalui stabilitas tipe yang dibahas pada modul Performa. Kompiler Julia menghasilkan kode mesin khusus untuk setiap kombinasi tipe konkret, sehingga fungsi yang menerima Motor dengan field Int bekerja tanpa pemeriksaan tipe saat berjalan. Kontrak itu terpenuhi hanya apabila seluruh field beranotasi tipe konkret sejak definisi, dan anotasi itulah yang menjadi bagian rancangan.

Pemeriksaan kekonkretan murah, sehingga layak menjadi bagian dari rancangan. Fungsi isconcretetype, isabstracttype, dan fieldtypes menyatakan sifat tipe rancangan secara eksplisit, sebagaimana ditunjukkan pada blok berikut.

julia Contoh: pemeriksaan kekonkretan tipe rancangan
# berkas rancangan dimuat sekali; isdefined mencegah definisi ulang
isdefined(Main, :Motor) || include("rancangan.jl")

println(isconcretetype(Motor))
println(isabstracttype(Kendaraan))
println(fieldtypes(Motor))
text Keluaran
true
true
(Int64,)

Keluaran menyatakan bahwa Motor konkret, Kendaraan abstrak, dan field Motor berupa (Int64,). Ketiga baris itu adalah bentuk paling sederhana dari pemeriksaan yang nanti dilakukan secara lebih rinci oleh alat seperti @code_warntype pada modul Performa. Pemeriksaan semacam ini dijalankan pada tahap rancangan, jauh sebelum pengukuran performa dilakukan, sehingga kesalahan tipe tertangkap ketika memperbaikinya masih murah.

Pengujian yang menjaga kontrak rancangan#

Test set ditulis terhadap kontrak rancangan, bukan terhadap detail implementasi. Kontrak fungsi biaya menyatakan nilai untuk jam bulat per jenis kendaraan dan penolakan terhadap jenis yang tidak dilayani, sehingga ujiannya memeriksa hal itu secara tepat. Ujian semacam ini tetap sah ketika badan fungsi ditulis ulang, misalnya ketika rumus tarif ditambah biaya minimum, karena kontraknya tidak berubah. Sebaliknya, perubahan implementasi yang melanggar kontrak langsung terlihat pada pengujian.

julia Contoh: pengujian atas rancangan tipe
using Test

# berkas rancangan dimuat sekali; isdefined mencegah definisi ulang
isdefined(Main, :Motor) || include("rancangan.jl")

hasil = redirect_stdout(devnull) do
    @testset "Rancangan tarif parkir" begin
        @test biaya(tarif, Motor(3)) == 3000
        @test biaya(tarif, Mobil(2)) == 4000
        @test_throws MethodError biaya(tarif, "bus")
    end
end
println("ujian lulus: ", hasil.n_passed)
text Keluaran
ujian lulus: 3

Pola redirect_stdout(devnull) meredam ringkasan test set yang memuat kolom waktu, sehingga yang tampil hanyalah jumlah ujian yang lulus. Pola ini sama dengan yang digunakan pada topik Pengujian. Ujian ketiga menuntut MethodError untuk argumen String, karena jenis kendaraan yang tidak dinyatakan sebagai tipe memang tidak dilayani. Kontrak penolakan itu bagian dari rancangan, sehingga kehadirannya diuji, bukan hanya kehadiran metode yang dilayani.

Kesalahan umum#

Kesalahan pertama adalah menulis implementasi panjang tanpa rancangan tipe. Struct yang lahir dari kode yang sudah ada cenderung mencampur tanggung jawab, misalnya tarif ikut disimpan pada setiap objek kendaraan sehingga perubahan tarif menyentuh seluruh data lama. Perbaikannya adalah menetapkan struct beserta field dan tipenya lebih dahulu, lalu menulis implementasi di atas rancangan itu.

Kesalahan kedua adalah menyimpan jenis kendaraan sebagai String dan memilih tarif dengan pencabangan if di dalam satu fungsi. Bentuk seperti ini menghilangkan dispatch, sehingga penambahan jenis baru menyentuh fungsi yang sudah teruji dan tidak ada MethodError yang bisa diuji. Perbaikannya adalah menjadikan jenis sebagai tipe dan menulis satu metode untuk setiap jenis. Kesalahan ketiga adalah membiarkan field tanpa anotasi tipe, yang merusak stabilitas tipe dan membebani setiap pemanggilan dengan dispatch dinamis. Anotasi konkret ditetapkan pada rancangan, bukan ditambahkan setelah performa menjadi masalah.

Ringkasan#

  • Rancangan tipe menetapkan struct dengan field beranotasi dan daftar metode sebelum implementasi, sehingga perubahan tipe terjadi pada tahap yang termurah.
  • Pada penghitung tarif parkir, jenis kendaraan dinyatakan sebagai tipe, tarif dipisahkan ke TarifJam, dan fungsi biaya memuat satu metode per jenis.
  • Field beranotasi konkret menjaga stabilitas tipe, sedangkan test set yang ditulis dari kontrak rancangan tetap sah selama implementasi disempurnakan.

Referensi#