Menerapkan MVC di Swing Dengan Griffon


Walaupun terkenal, MVC adalah sebuah paradigma yang dapat diterapkan dengan berbagai cara. Bahkan hingga saat ini, masih tidak ada sebuah arsitektur universal untuk MVC yang disepakati oleh semua developer. Sebagai contoh, paradigma MVC memang mensyaratkan pemisahan tanggung jawab ke dalam model, view dan controller, tapi sering ada pertanyaan seperti “apa sih yang masuk di bagian model?”. Pada artikel ini, saya menuliskan penerapan MVC yang selama ini terbukti efektif bagi kebutuhan saya. Penerapan ini sehari-hari saya pakai pada aplikasi desktop yang dikembangkan dengan menggunakan Java Swing, Griffon dan simple-jpa. ‘Efektif’ yang saya maksud adalah selama ini saya dapat mengubah presentation layer dengan cepat. ‘Efektif’ juga berarti saya tidak pernah pusing dalam menjelajahi kembali kode presentation layer setelah istirahat berbulan-bulan.

Anggap saja saya ingin membuat presentation layer yang memiliki tampilan seperti yang terlihat pada gambar berikut ini:

Screen yang hendak dibuat

Screen yang hendak dibuat

Ini adalah screen yang umum dijumpai untuk mewakili kartu stok pada sebuah produk. Apa yang saya lakukan untuk membuat screen tersebut?

Langkah pertama yang saya lakukan adalah membuat model. Pada MVC untuk aplikasi desktop, model adalah semua data yang berhubungan dengan layar, bukan saja domain model. Sebagai contoh, pada screen yang hendak saya buat, saya perlu nilai di model untuk mewakili JComboBox. Yang pasti, juga butuh nilai di model untuk mewakili isi tabel. Selain itu, saya juga perlu memiliki nilai di model untuk mewakili masing-masing JCheckBox. Dengan demikian, saya membuat kode program model sehingga terlihat seperti berikut ini:

class ItemStokModel {

    @Bindable boolean showReferensiFinance
    @Bindable boolean showReferensiGudang
    @Bindable boolean showPembuat
    @Bindable boolean showKeterangan

    BasicEventList<ItemStok> itemStokList = new BasicEventList<>()

    BasicEventList<PeriodeItemStok> periodeItemStokList = new BasicEventList<>()
    DefaultEventComboBoxModel<PeriodeItemStok> periodeItemStok = 
        GlazedListsSwing.eventComboBoxModelWithThreadProxyList(periodeItemStokList)


}

Setiap atribut di model mewakili nilai untuk setiap komponen di view. Tidak semua komponen di view perlu memiliki nilai di model. Sebagai contoh, di view terdapat 2 JButton, tetapi karena saya tidak tertarik pada nilai-nya, maka saya tidak perlu membuat atribut untuk mewakili nilai JButton tersebut di model.

Langkah berikutnya adalah membuat view. Sebagai contoh, saya membuat view dengan SwingBuilder seperti berikut ini:

panel(id: 'mainPanel') {
    borderLayout()

    panel(constraints: PAGE_START) {
        flowLayout(alignment: FlowLayout.LEADING)
        comboBox(id: 'periodeItemStok', model: model.periodeItemStok,
            templateRenderer: "${it.tanggalMulai.toString('MMMM YYYY')} (Jumlah: ${it.jumlah})")
        button(app.getMessage('simplejpa.search.label'), actionPerformed: controller.search)
        checkBox('Referensi Finance', selected: bind('showReferensiFinance', target: model, mutual: true))
        checkBox('Referensi Gudang', selected: bind('showReferensiGudang', target: model, mutual: true))
        checkBox('Pembuat', selected: bind('showPembuat', target: model, mutual: true))
        checkBox('Keterangan', selected: bind('showKeterangan', target: model, mutual: true))
    }

    scrollPane(constraints: CENTER) {
        glazedTable(id: 'table', list: model.itemStokList, sortingStrategy: SINGLE_COLUMN) {
            glazedColumn(name: 'Tanggal', property: 'tanggal', width: 100) {
                templateRenderer("${it.toString('dd-MM-yyyy')}")
            }
            glazedColumn(name: 'Qty', property: 'jumlah', columnClass: Integer, width: 40)
            glazedColumn(name: 'Pihak Terkait', expression: {it.referensiStok?.pihakTerkait?: ''})
            glazedColumn(name: 'Referensi Finance', expression: {it.referensiStok?.deskripsiFinance()?: ''},
                visible: bind {model.showReferensiFinance})
            glazedColumn(name: 'Referensi Gudang', expression: {it.referensiStok?.deskripsiGudang()?: ''},
                visible: bind {model.showReferensiGudang})
            glazedColumn(name: 'Dibuat', expression: {it.referensiStok?.dibuatOleh?:''},
                visible: bind {model.showPembuat})
            glazedColumn(name: 'Diubah', expression: {it.referensiStok?.diubahOleh?:''},
                visible: bind {model.showPembuat})
            glazedColumn(name: 'Keterangan', property: 'keterangan', visible: bind {model.showKeterangan})
        }
    }

    panel(constraints: PAGE_END) {
        flowLayout(alignment: FlowLayout.LEADING)
        button(app.getMessage("simplejpa.dialog.close.button"), actionPerformed: controller.tutup)
    }
}

Bila model berisi nilai untuk sebuah komponen, maka view berisi deklarasi komponen itu sendiri. Sebagai contoh, saya membuat beberapa JCheckBox disini. Tapi saya sama sekali tidak tertarik pada JCheckBox itu sendiri (saya bahkan tidak menyimpannya di variabel untuk mengaksesnya kembali nanti!). Yang paling saya butuhkan adalah nilai boolean seperti showReferensiFinance atau showKeterangan di model.

Bagaimana caranya supaya perubahan komponen di view bisa memperbaharui nilai di model secara otomatis? Jawabannya adalah dengan menggunakan observer pattern. Griffon/Groovy mempermudah proses ini dengan fasilitas bind(). Sebagai contoh, pada checkBox(), saya membuat deklarasi seperti selected: bind('showPembuat', target: model, mutual: true). Ini akan menciptakan binding pada nilai atribut JCheckBox.selected dan nilai model.showPembuat. Karena atribut JCheckBox.selected akan bernilai true bila checbox tersebut diberi tanda centang, maka nilai showPembuat akan menjadi true bila checkbox diberi tanda centang, seperti yang terlihat pada gambar berikut ini:

Binding dari view ke model

Binding dari view ke model

Dengan observer pattern, saya tidak perlu memanggil setter secara manual lagi karena ini dilakukan secara otomatis setiap kali JCheckBox di-klik oleh pengguna. Karena saya menambahkan deklarasi mutual: true pada bind(), maka proses binding juga berlaku dari arah yang sebaliknya. Bila saya mengubah nilai showPembuat di model menjadi true atau false, maka nilai selected di JCheckBox juga akan ikut berubah, seperti yang terlihat pada gambar berikut ini:

Binding dari model ke view

Binding dari model ke view

Pada saat sebuah JCheckBox yang ada di-klik oleh pengguna, saya ingin kolom di tabel dimunculkan atau disembunyikan. Hal ini saya lakukan agar tidak menampilkan terlalu banyak kolom yang dapat memusingkan pengguna, tetapi saya juga tidak ingin pengguna kekurangan informasi dalam melakukan troubleshooting (misalnya, bila ada selisih stok di gudang). Untuk itu, saya hanya perlu melakukan binding di kolom tabel seperti:

glazedTable() {
  glazedColumn(..., visible: bind {model.showReferensiFinance})
  glazedColumn(..., visible: bind {model.showReferensiGudang})
  glazedColumn(..., visible: bind {model.showDibuatOleh})
  glazedColumn(..., visible: bind {model.showDibuatOleh})
  glazedColumn(..., visible: bind {model.showKeterangan})
}

Berkat observer pattern, setiap checkbox akan bekerja dengan baik tanpa perlu tambahan kode program lain sama sekali. Proses yang terjadi dibalik layar yang menyebabkan checkbox bisa bekerja akan terlihat seperti pada UML Communication Diagram berikut ini:

Perubahan view secara otomatis melalui binding

Perubahan view secara otomatis melalui binding

Pada pemograman web, anjuran yang paling sering didengar adalah memisahkan antara HTML dan JavaScript pada file yang berbeda. HTML hanya perlu berisi informasi yang dibutuhkan sebagai struktur halaman, sementara file JavaScript (.js) berisi semua kode program JavaScript yang dipakai oleh file HTML. Berdasarkan analogi ini, saya melakukan pemisahan antara view dan controller. Kode program seperti aksi yang akan dikerjakan bila sebuah JButton di-klik harus terletak di controller bukan di view. Untuk menghubungkan view dan controller, saya membuat deklarasi seperti berikut ini di view:

button('Cari', actionPerformed: controller.search)
...
button('Tutup', actionPerformed: controller.tutup)

Kode program controller yang saya buat terlihat seperti berikut ini:

class ItemStokController {

    def model
    def view

    ProdukRepository produkRepository

    void mvcGroupInit(Map args) {
        model.parent = args.'parent'
        model.showReferensiFinance = true
        model.showReferensiGudang = false
        model.showPembuat = false
        model.showKeterangan = true
        execInsideUISync {
            model.periodeItemStokList.clear()
        }
        List periodeItemStok = ...
        execInsideUISync {
            model.periodeItemStokList.addAll(periodeItemStok)
        }
    }

    def cari = {
        ...
    }

    def tutup = {
        ...
    }

}

Kode program controller akan mengisi nilai pada model. Ia juga akan memanggil repository untuk membaca data dari database, memanggil method dari domain class untuk mengerjakan business logic, dan sebagainya.

Selain mengisi nilai model, apa saja kode program yang boleh diletakkan di controller? Saya hanya meletakkan kode program yang berkaitan dengan presentation layer, dalam hal ini adalah Swing, seperti kode program untuk menutup dialog, melakukan resizing tampilan, dan sejenisnya. Kode program untuk membaca dan menulis ke database harus berada di class lain. Kode program yang berkaitan dengan business logic juga harus berada di class lain, tepatnya berada di domain class.

Perihal Solid Snake
I'm nothing...

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: