Memakai Temporal Pattern Di Aplikasi Inventory

Pada artikel ini, saya melanjutkan pembahasan tentang kode program inventory yang ada di Belajar Menerapkan Domain Driven Design Pada Aplikasi Inventory. Salah satu kebutuhan proyek tersebut adalah pengguna harus bisa mengetahui riwayat perubahan stok untuk masing-masing produk yang ada (misalnya untuk keperluan kartu stok). Untuk itu, saya mengimplementasikan pola Temporal Pattern yang ada di http://martinfowler.com/eaaDev/timeNarrative.html. Pola ini dapat dipakai untuk rancangan yang berusaha memberikan informasi pada periode di masa lalu.

Langkah pertama yang saya lakukan adalah mendefinisikan sebuah class untuk mewakili periode. Untuk itu, saya akan menggunakan pola Range (http://martinfowler.com/eaaDev/Range.html). Saya menambahkan sedikit method sesuai kebutuhan saya, sehingga rancangan class Periode menjadi seperti berikut ini:

Class `Periode`

Class `Periode`

Berikutnya, saya akan mendefinisikan Temporal Pattern dalam bentuk abstract class sehingga bisa di-reuse nantinya. Hasilnya akan terlihat seperti pada gambar berikut ini:

Temporal patterns dalam bentuk abstract class

Temporal patterns dalam bentuk abstract class

Stok akan berkurang bila bagian gudang mengambil barang berdasarkan informasi dari penjualan (begitu juga sebaliknya). Bukan hanya itu, transaksi seperti retur, penyesuaian, mutasi, dan penukaran poin dengan barang juga bisa menyebabkan perubahan stok di gudang. Tentu saja, bila faktur dihapus, riwayat perubahan juga harus berubah. Melakukan pen-query-an untuk semua riwayat ini bisa membingungkan dan tidak pasti tergantung pada perubahan di masa depan! Oleh sebab itu, saya memutuskan untuk menerapkan pola Audit Log. Setiap perubahan stok akan menciptakan sebuah object ItemPeriodik baru. Walaupun pola Audit Log adalah yang paling mudah diterapkan, ia lebih susah dicari dan bisa mempengaruhi ukuran database secara signifikan (hal ini bisa diatasi dengan polyglot database dimana Audit Log disimpan pada database terpisah seperti MongoDB).

Pemisahan antara NilaiPeriodik dan ItemPeriodik dilakukan untuk mendukung lazy loading di Hibernate JPA. Pengguna umumnya tidak tertarik untuk melihat seluruh riwayat yang ada melainkan hanya periode tertentu saja. Dengan demikian, men-query seluruh ItemPeriodik sekaligus bukan saja membebani database tetapi juga tidak dibutuhkan. Pada rancangan ini, saya menganggap NilaiPeriodik berada dalam periode bulanan.

AggregatePeriodik menyediakanTemporal Property berupa saldoKumulatifSebelum() untuk mendapatkan jumlah (saldo) sebuah produk pada tanggal tertentu. Jumlah terakhir yang paling aktual sampai hari ini selalu tersedia sebagai property jumlah.

Agar sederhana, saya menerapkan Audit Log dengan rekaman yang hanya bersifat additive (penambahan). Penambahan ItemPeriodik (riwayat perubahan stok) selalu dilakukan pada akhir kartu stok. Pengguna boleh saja memproses transaksi untuk faktur di masa lampau, tetapi riwayat perubahannya akan selalu ditambahkan pada akhir kartu stok. Bila pengguna melakukan operasi yang mengurangi jumlah stok, maka sebuah ItemPeriodik dengan nilai negatif akan ditambahkan pada akhir katu stok. Seperti yang dituliskan oleh Fowler, mengizinkan hanya operasi additive membuat operasi menjadi sangat sederhana dan banyak perubahan di kasus nyata memang memiliki sifat additive.

Sekarang, saya siap untuk mengimplementasikan pola dalam bentuk abstract class tersebut ke dalam sebuah class yang konkrit, misalnya:

Penerapan pada stok produk

Penerapan pada stok produk

Pada rancangan di atas, StokProduk adalah implementasi dari AggregatePeriodik, PeriodeItemStok adalah implementasi dari NilaiPeriodik dan ItemStok adalah implementasi dari ItemPeriodik.

ItemStok memiliki referensi ke faktur yang berkaitan dengannya. Sebagai informasi, tanggal pada ItemStok selalu merujuk pada tanggal saat ItemStok tersebut dibuat, bukan tanggal yang berlaku di faktur. Sebagai contoh, anggap saja saya memiliki data seperti berikut ini:

tanggal        tanggal faktur      perubahan  saldo  keterangan
---------------------------------------------------------------------------------
10/01/2015     10/01/2015          100        100    Bertambah
20/01/2015     20/01/2015          -10         90    Berkurang
01/02/2015     20/01/2015           10        100    Hapus faktur tanggal 20/01

Bila saya memanggil stokProduk.saldoKumulatifSebelum(LocalDate.parse('2015-01-31')) pada bulan Januari, saya akan memperoleh nilai 90. Hal ini karena penghapusan faktur baru dilakukan pada bulan Februari. Bila saya memanggil method yang sama pada bulan Februari, saya akan akan memperoleh nilai 100 karena penghapusan faktur sudah dilakukan. Martin Fowler menyebut ini sebagai Dimensions of Time. Hal seperti ini penting untuk kebutuhan seperti laporan pajak dimana pengguna harus mengetahui sebuah nilai persis pada tanggal saat laporan lama dicetak namun sebelum perubahan di masa depan dilakukan.

Pola Temporal Pattern yang telah saya pakai disini tidak hanya bisa diterapkan untuk stok produk. Saya juga bisa menerapkannya pada entity lain, misalnya untuk mengisi kas. Untuk memakainya pada kas, saya hanya perlu menurunkan entity yang berkaitan dengan kas pada abstract class yang sudah saya buat sebelumnya, misalnya:

Penerapan pada kas

Penerapan pada kas

Pada rancangan di atas, Kas adalah sebuah AggregatePeriodik sehingga secara otomatis ia juga memiliki Temporal Property seperti saldoKumulatifSebelum() untuk mencari saldo kas pada posisi tanggal tertentu. Contoh ini juga memperlihatkan contoh penggunaan abstract class yang mempermudah penerapan design pattern.

Pada AggregatePeriodik, saya menambahkan method arsip() untuk menghapus daftar ItemPeriodik yang ada. Operasi ini tidak akan mempengaruhi jumlah terakhir karena mereka disimpan sebagai property di AggregatePeriodik. Mengapa arsip() perlu dilakukan? Hal ini karena semakin banyak transaksi yang ada maka jumlah ItemPeriodik akan semakin membengkak sehingga ruang kosong harddisk akan cepat habis. arsip() hanya akan menghapus ItemPeriodik tetapi tidak akan pernah menghapus NilaiPeriodik. Dengan asumsi program dipakai selama 100 tahun, maka NilaiPeriodik yang dibuat hanya berjumlah 12 * 100 = 120 record untuk masing-masing produk. Jumlah ini relatif sedikit dan aman untuk dipertahankan. Selain itu, karena NilaiPeriodik mengandung informasi jumlah dan saldo, pengguna tetap bisa melihat summary per bulan bahkan setelah arsip() dikerjakan.

Belajar Menerapkan Domain Driven Design Pada Aplikasi Inventory

Tidak semua developer memiliki pandangan yang sama tentang cara membuat software yang efektif. Walaupun mereka sama-sama bisa menulis kode program, pendekatan yang dipakai dalam memecahkan masalah bisa berbeda-beda. Domain driven design (DDD) adalah salah satu jenis pendekatan tersebut. Mengapa dibutuhkan teknik tersendiri dalam mengelola kerumitan? Mengapa tidak langsung menulis kode program secara spontan sesuai suasana hati? Pertanyaan serupa juga dapat ditanyakan pada banyak bidang ilmu lainnya: Mengapa designer Photoshop tidak membuat ratusan layer secara bebas? Mengapa mereka mengelola ratusan layer tersebut secara seksama dengan penamaan, grouping dan pewarnaan? Mengapa tidak membiarkan anggota tim basket berkeliaran secara bebas di lapangan? Mengapa masing-masing anggota tim basket harus dilatih dan dibatasi sesuai peran?

Pada artikel ini, saya akan menggunakan contoh kode program simple-jpa-demo-inventory yang bisa dijumpai di https://github.com/JockiHendry/simple-jpa-demo-inventory. Kode program ini dirilis sebagai demo untuk simple-jpa 0.8 (baca artikel ini). Pada proyek ini, saya menggunakan metode DDD yang disesuaikan dengan keterbatasan akibat teknologi yang saya pakai. Misalnya, Hibernate JPA tidak begitu efisien dalam mengelola @ElementCollection sehingga terkadang saya harus menggantinya dengan @OneToMany. Walaupun demikian, saya tetap berusaha menerapkan DDD secara konseptual pada rancangan UML saya.

Salah satu permasalahan yang sering dihadapi developer pemula saat merancang aplikasi nyata adalah komunikasi antar class (atau antar function bila masih prosedural) yang semakin lama semakin membingungkan seiring dengan perubahan dari waktu ke waktu. Ini akan menghasilkan dead code yang tidak berani disentuh karena takut akan terjadi kesalahan. Bila dead code semakin banyak, maka developer pemula tersebut akan semakin kehilangan semangatnya sampai proyek dinyatakan gagal.

DDD berusaha mengatasi ini dengan menggunakan apa yang disebut sebagai bounded context (http://www.martinfowler.com/bliki/BoundedContext.html). Bagi developer, rancangan aplikasi dibagi ke dalam context yang berbeda sesuai dengan pandangan domain expert. Setiap context yang berbeda berkomunikasi secara minimal dengan context lainnya! Sebagai contoh, pada aplikasi simple-jpa-demo-inventory, saya menggambarkan context melalui UML Package Diagram seperti berikut ini:

UML Package Diagram yang menunjukkan context aplikasi simple-jpa-demo-inventory

UML Package Diagram yang menunjukkan context aplikasi simple-jpa-demo-inventory

Pada diagram di atas, saya membedakan aplikasi ke dalam context seperti:

  1. Context Inventory: berisi class yang berkaitan dengan inventory seperti Produk, Gudang, Transfer (mutasi) dan sebagainya.
  2. Context Servis: berisi class yang berkaitan dengan perbaikan (servis) barang, dipisahkan tersendiri karena servis ditangani oleh divisi berbeda.
  3. Context Pembelian: berisi class yang berkaitan dengan pembelian barang yang dilakukan oleh pengguna.
  4. Context Penjualan: berisi class yang berkaitan dengan penjualan barang.
  5. Context Retur: berisi class yang berkaitan dengan retur penjualan maupun retur pembelian.
  6. Context LabaRugi: berisi class yang berkaitan dengan transaksi kas dan laporan laba rugi (seharusnya dua context berbeda, tetapi domain expert melihatnya sebagai sama mengingat tujuan utama aplikasi ini adalah untuk mengelola inventory bukan akuntasi).

Saya juga memiliki package seperti Faktur, User, dan General. Mereka lebih ditujukan untuk pengelompokan secara teknis (untuk kerapian dari sisi pemograman) dan tidak membentuk context pada sisi bisnis aplikasi simple-jpa-demo-inventory ini.

Untuk mencapai sifat bounded context dimana pintu gerbang atau komunikasi antar context harus dilakukan secara terbatas, saya menggunakan fasilitas event yang ditawarkan oleh framework yang saya pakai. Berikut ini adalah daftar class event di yang boleh dipanggil dari context lainnya:

  1. Event PerubahanStok, TransferStok, PesanStok, PerubahanRetur, dan PerubahanStokTukar akan ditangani oleh Context Inventory.
  2. Event BayarPiutang akan ditangani oleh ContextRetur.
  3. Event TransaksiSistem akan ditangani oleh Context LabaRugi.

Pada framework yang saya pakai, sebuah event dapat ditangani dengan membuat nama method yang diawali dengan on lalu diikuti dengan nama class event tersebut. Sebagai contoh, pada Context LabaRugi, saya menggambarkan UML Class Diagram untuk bagian yang menangani event TransaksiSistem seperti:

Contoh event listener

Contoh event listener

Implementasinya pada kode program berupa:

@Transaction @SuppressWarnings("GroovyUnusedDeclaration")
class LabaRugiEventListenerService {

    KasRepository kasRepository
    KategoriKasRepository kategoriKasRepository
    JenisTransaksiKasRepository jenisTransaksiKasRepository

    void onTransaksiSistem(TransaksiSistem transaksiSistem) {
        KategoriKas kategori = kategoriKasRepository.getKategoriSistem(transaksiSistem.kategori, transaksiSistem.invers)
        TransaksiKas transaksiKas = new TransaksiKas(tanggal: LocalDate.now(), jumlah: transaksiSistem.jumlah,
            pihakTerkait: transaksiSistem.nomorReferensi, kategoriKas: kategori, jenis: jenisTransaksiKasRepository.cariUntukSistem())
        kasRepository.cariUntukSistem().tambah(transaksiKas)
    }

}

Event TransaksiSistem dipakai oleh class di context lainnya untuk menambah transaksi otomatis ke kas. Sebagai contoh, PencairanPoinTukarUang adalah operasi pencairan poin bonus yang ditukar dengan sejumlah uang tunai. Dengan demikian, selain mengurangi jumlah poin pelanggan, operasi ini juga harus mengurangi jumlah kas. Karena PencairanPoinTukarUang berada dalam Context Penjualan sedangkan kas berada dalam Context LabaRugi, maka saya perlu menggunakan event TransaksiSistem seperti yang terlihat pada gambar berikut ini:

Contoh penggunaan domain event

Contoh penggunaan domain event

Implementasi pada kode program akan terlihat seperti:

@DomainClass @Entity
class PencairanPoinTukarUang extends PencairanPoin {

    public PencairanPoinTukarUang() {}

    @SuppressWarnings("GroovyUnusedDeclaration")
    public PencairanPoinTukarUang(LocalDate tanggal, Integer jumlahPoin, BigDecimal rate) {
        super(tanggal, jumlahPoin, rate)
    }

    @Override
    boolean valid() {
        true
    }

    @Override
    void proses() {
        ApplicationHolder.application?.event(new TransaksiSistem(getNominal(), nomor, KATEGORI_SISTEM.PENGELUARAN_LAIN))
    }

    @Override
    void hapus() {
        ApplicationHolder.application?.event(new TransaksiSistem(getNominal(), nomor, KATEGORI_SISTEM.PENGELUARAN_LAIN, true))
    }

}

Mengapa memakai event? Mengapa tidak langsung memanggil class yang ada di Context LabaRugi? Saya merasa bahwa penggunaan event lebih mengurangi ketergantungan. Class yang menangani event bisa diganti atau bahkan bisa dipindahkan ke context lainnya secara aman. Si pemanggil tidak perlu tahu hal tersebut karena ia cukup hanya me-raise event.

Masih dalam rangka melindungi class di dalam bounded context, DDD juga memperkenalkan apa yang disebut sebagai aggregate. Ini mirip seperti komposisi atau relasi one-to-many di database. Bedanya, class lain hanya boleh mengakses aggregate root (class yang berperan owner). Mereka tidak boleh mengakses isi di dalam aggregate root secara langsung. Sebagai contoh, perhatikan diagram berikut ini:

Contoh aggregate

Contoh aggregate

Pada diagram di atas, KewajibanPembayaran adalah sebuah aggregate root yang terdiri atas banyak Pembayaran. Selanjutnya, masing-masing Pembayaran bisa memiliki sebuah Referensi. Bila mengikuti pendekatan DDD, maka tidak boleh ada Pembayaran dan Referensi-nya yang bisa dibaca secara langsung (misalnya langsung melalui query SQL) tanpa memperoleh sebuah KewajibanPembayaran terlebih dahulu. DDD membolehkan Pembayaran memiliki referensi ke entity seperti BilyetGiro. Entity tetap bisa dibaca dibaca tanpa harus melalui Pembayaran karena ia berdiri sendiri dan memiliki repository-nya sendiri: BilyetGiroRepository.

Saya pernah mendiskusikan rancangan ini pada seorang teman dan ia langsung memikirkan konsekuensinya pada rancangan GUI. Sesungguhnya DDD tidak mengatur rancangan GUI, misalnya sebuah giro yang bisa di-isi berkali-kali untuk pembayaran di faktur berbeda atau sebaliknya. Walaupun bisa memberikan dampak pada GUI, yang sesungguhnya ditawarkan oleh aggregate root adalah perlindungan atas perubahan di masa depan atau dengan kata lain mencegah kerumitan yang tak terkendali 🙂

Mengapa bisa demikian? Hal ini berkaitan dengan Law of Demeter (LoD). Bunyi hukum ini kira-kira adalah: ‘Sebuah class hanya boleh berinteraksi dengan class disekitarnya yang berhubungan langsung dengannya’. Sebuah class hanya boleh berbicara dengan temannya, jangan bicara dengan orang asing. Lalu, apa keuntungannya? Anggap saja saya membolehkan class Pembayaran berbicara dengan siapa saja secara langsung, diantaranya:

  1. Method sisaPiutang() di FakturJualOlehSales akan men-query seluruh Pembayaran secara langsung dan menghitung total-nya untuk menentukan sisa piutang.
  2. Method jumlahDibayar() di FakturJualOlehSales akan men-query seluruh Pembayaran secara langsung dan menghitung total-nya untuk menentukan jumlah pembayaran yang telah dilakukan.
  3. Method sisaHutang() di PurchaseOrder akan men-query seluruh Pembayaran secara langsung dan menghitung total-nya untuk menentukan jumlah pembayaran yang telah dilakukan.
  4. Laporan sisa pembayaran akan men-query seluruh Pembayaran secara langsung dan menghitung total-nya.

Saya telah melanggar Law of Demeter karena Pembayaran berbicara dengan banyak orang asing seperti FakturJualOlehSales, PurchaseOrder dan laporan. Apa konsekuensinya? Pada suatu hari, domain expert ingin pembayaran melalui BilyetGiro dianggap belum lunas bila belum bisa dicairkan (karena terlalu banyak giro kosong). Dengan demikian, saya perlu menambahkan kondisi if untuk memeriksa nilai bilyetGiro.jatuhTempo di setiap kode program yang menghitung jumlah pembayaran. Pada contoh di atas, saya perlu melakukan perubahan di 4 class yang berbeda! Sebaliknya, bila saya mengikuti Law of Demeter, 4 class tersebut akan memanggil KewajibanPembayaran.jumlahDibayar() sehingga saya hanya perlu melakukan perubahan di class KewajibanPembayaran saja. Ini lebih meringankan beban dan berguna mengurangi kemungkinan terjadinya kesalahan bila dibandingkan solusi yang tidak mengikuti Law of Demeter.

Kasus di atas juga menggambarkan contoh prinsip Tell-Don’t-Ask (http://martinfowler.com/bliki/TellDontAsk.html). Saya sering kali melihat rancangan class diagram tanpa method. Biasanya, class yang tidak memiliki method dipakai dengan cara dikumpulkan lalu diolah lagi menjadi nilai oleh si pemanggil (dengan kode program yang ada di sisi pemanggil). Don’t-Ask disini berarti jangan meminta nilai untuk dihitung sendiri, tapi Tell yang berarti berikan instruksi dan biarkan dia yang kerjain.

Contoh lain dari penerapan Tell-Don’t-Ask dapat dilihat pada class Konsumen seperti gambar berikut ini:

Class `Konsumen`

Class `Konsumen`

Setiap konsumen memiliki atribut poinTerkumpul yang dapat mereka tukarkan dengan uang tunai atau potongan piutang. Saya menggunakan method seperti tambahPoin() dan hapusPoin() untuk menambah atau mengurangi poin. Method tersebut berisi kode program yang melakukan validasi jumlah poin dan juga menambahkan riwayat perubahan poin bila perlu. Developer yang datang dari latar belakang data-oriented cenderung hanya membuat atribut poinTerkumpul tetapi tidak menyediakan method seperti tambahPoin() dan hapusPoin(). Mereka cenderung membiarkan pemanggil untuk mengerjakan tugas seperti validasi, men-update nilai poinTerkumpul dan membuat riwayat.

Pada DDD, repository hanya bertugas untuk mencari dan menulis entity ke persitence storage seperti database. Masing-masing repository hanya menangani satu jenis entity. Untuk operasi yang mencakup lebih dari satu entity, perlu dibuat apa yang disebut sebagai service. Sebagai contoh, operasi untuk menghitung laba rugi melibatkan banyak entity seperti pembelian, penjualan, dan yang tersedia di gudang. Oleh sebab itu, saya mendefinisikannya ke dalam sebuah service seperti pada gambar berikut ini:

Contoh service

Contoh service

Pada rancangan di atas, LabaRugiService adalah sebuah service yang hanya berisi method tanpa atribut. Gunakan service sesuai kebutuhan saja karena bila hampir seluruh aplikasi terdiri atas service, maka lama-lama bisa menjadi pemograman prosedural.

Diagram di atas juga memperlihatkan bahwa tidak semua domain class adalah entity yang harus disimpan di persistence storage. NilaiInventory adalah sebuah aggregate root untuk menampung hasil perhitungan nilai inventory yang memiliki lebih dari satu item seperti metode FIFO dan LIFO. Saya tidak memakai sebuah List sederhana karena saya ingin menyediakan method seperti tambah() dan kurang() yang sangat berguna dalam perhitungan HPP nanti. Karena hanya berperan sebagai alat bantu dalam kalkulasi HPP, saya tidak akan menyimpannya ke database sehingga ia tidak memiliki repository.

Bicara soal repository, saya tidak menerapkan repository secara murni tetapi cenderung menggabungkan repository dan service karena saya sudah memakai simple-jpa. Ini bukanlah sesuatu yang standar pada dunia DDD, melainkan modifikasi yang saya lakukan sehubungan dengan teknologi yang saya pakai.

Menerapkan State Pattern Untuk Workflow Sederhana

Pada suatu hari, saya mencium sebuah bau tidak sedap (baca: smell code) pada rancangan domain model yang saya buat. Walaupun rancangan ini sudah berjalan dan bekerja dengan baik, setiap kali perlu melakukan modifikasi untuk bagian tersebut, saya selalu merasa ada yang menjanggal dan tidak elegan. Rancangan yang saya maksud adalah bagian yang terlihat seperti pada gambar berikut ini:

Rancangan awal

Rancangan awal

Pada awalnya, saya merancang dengan mengikuti prinsip OOP seperti umumnya, mencari kata kerja untuk faktur seperti kirim(), buatSuratJalan(), tambah(BuktiTerima), hapusPengeluaranBarang(), dan sebagainya. Kumpulan method seperti ini membuat class saya menjadi raksasa yang gendut (ini adalah salah satu bentuk smell code). Tapi yang lebih menjadi masalah adalah kumpulan method tersebut harus dikerjakan secara berurutan. Sebuah pemesanan harus dibuat sebelum bisa dikirim dan kemudian diterima. Setelah semua pembayaran dilakukan, baru pemesanan bisa dianggap lunas. Akibat business rule seperti ini, pada implementasi kode program, saya memiliki banyak if untuk memeriksa dan menjaga agar urutan operasional pemesanan tetap benar. Bukan hanya itu, pemeriksaan if untuk status juga dilakukan pada beberapa method lainnya. Sebagai contoh, prosedur retur akan berbeda tergantung pada status faktur, sehingga implementasi method tambahRetur() terlihat seperti:

if (status == StatusFakturJual.DIBUAT) {
  ...
} else if (status == StatusFakturJual.DITERIMA) {
  ...
}

Karena pemeriksaan status tersebar di berbagai method, saya menjadi semakin tidak berani mengubah workflow yang sudah ada. Tanpa menelusuri kode program, saya tidak bisa yakin bagian mana yang harus saya modifikasi bila workflow berubah. Ini menunjukkan bahwa rancangan saya tidak efektif (bila seandainya perubahan worflow adalah sebuah kebutuhan yang sering terjadi!). Masalah seperti ini ternyata juga dijumpai oleh perancang lainnya. Sebagai contoh, pada kumpulan jurnal Advanced Information Systems Engineering (CAiSE”99, 1999 Proceedings) terdapat paper A Process-Oriented Approach to Software Compoent Definition. Paper tersebut menunjukkan bahwa walaupun object-oriented (domain model sebagai model utama) lebih pintar dibandingkan dengan data-oriented (tabel database atau ERD sebagai model utama), object-oriented masih memiliki masalah dalam melakukan validasi urutan eksekusi method (dan juga masalah lainnya yang menurut si penulis lebih tepat diselesaikan secara process-oriented).

Karena waktu yang terbatas, saya tidak akan melakukan penelitian atau memakai workflow engine untuk mengatasi masalah yang saya hadapi 🙂 Sebagai alternatif solusi sederhana, saya dapat menggunakan state pattern (baca di https://en.wikipedia.org/wiki/State_pattern). Pada design pattern ini, sebuah context dapat memiliki satu atau lebih state dimana hanya satu state yang aktif pada saat bersamaan. Operasi yang sama pada sebuah context dapat mengerjakan kode program yang berbeda tergantung pada state yang aktif. Pada kasus saya, FakturJual adalah context dan setiap enumeration di StatusFakturJual mewakili state yang ada.

Langkah pertama yang saya lakukan adalah mendefinisikan seluruh state yang ada beserta operasi yang didukung olehnya, misalnya seperti pada gambar berikut ini:

Rancangan class yang mewakili state

Rancangan class yang mewakili state

Method seperti kirim() dan terima() kini dapat diletakkan pada method proses() di class masing-masing yang mewakili state bersangkutan. Sebagai contoh, method proses() pada FakturJualOlehSalesDibuat adalah apa yang sebelumnya disebut kirim(). Begitu juga method proses() pada FakturJualOlehSalesDiantar adalah apa yang sebelumnya disebut terima(). Selain itu, saya juga perlu memastikan bahwa method proses() akan mengubah nilai status dari FakturJual sehingga berisi status berikutnya (di workflow). Karena tidak semua proses() membutuhkan nilai yang sama, saya akan meletakkan argumen yang dibutuhkan oleh proses() dalam bentuk Map. Sebagai alternatif lain yang lebih terstruktur, saya bisa tetap memakai method seperti kirim() atau terima() (sehingga operasi memiliki kata kerja yang jelas ketimbang hanya sebuah proses()). Bila method tersebut dipanggil tidak pada state yang seharusnya, saya bisa melemparkan kesalahan seperti UnsupportedOperationException sebagai tanda bahwa method tidak boleh dipanggil.

Method hapus() di setiap class yang mewakili state adalah apa yang sebelumnya berupa hapusPenerimaan(), hapusPengiriman(), dan sebagainya.

Method tambahRetur() dan hapusRetur() dipakai untuk memproses retur. Karena proses retur memiliki administrasi yang berbeda tergantung pada status faktur (termasuk tidak didukung), maka masing-masing class yang mewakili state memiliki kode programnya masing-masing dalam menangani dan menghapus (membatalkan) retur.

Setelah perubahan ini, saya tidak lagi membuat sebuah kode program monolithic yang penuh dengan if agar bisa menangangi semua status. Yang saya lakukan kini adalah membuat kode program untuk masing-masing state di class-nya masing-masing. ‘Beban pikiran’-pun berkurang banyak dan membuat kode program kini terasa lebih menyenangkan.

Langkah berikutnya adalah mengubah context agar memakai state yang ada. Sebagai contoh saya menambahkan method seperti pada gambar berikut ini:

Rancangan untuk class context

Rancangan untuk class context

Setiap subclass dari FakturJual perlu men-override method getOperasiFakturJual() karena masing-masing implementasi Faktur bisa memiliki workflow yang berbeda. Hal ini tidak perlu diketahui oleh kode program presentation layer karena mereka hanya perlu memanggil proses() dari Faktur untuk berpindah ke state berikutnya. Karena kode program yang memanggil proses() tidak perlu melakukan validasi bahkan tidak perlu mengetahui apa state berikutnya, saya berharap bisa melakukan modifikasi workflow di kemudian hari secara lebih mudah dan lebih bebas terhadap kesalahan.

Implementasi pada class FakturJual bisa berupa kode program berikut ini:

abstract OperasiFakturJual getOperasiFakturJual()

void proses(Map args) {
    getOperasiFakturJual().proses(this, args)
}

void hapus() {
    getOperasiFakturJual().hapus(this)
}

void tambahRetur(ReturFaktur returFaktur) {
    getOperasiFakturJual().tambahRetur(this, returFaktur)
}

void hapusRetur(String nomor) {
    getOperasiFakturJual().hapusRetur(this, nomor)
}

Contoh implementasi untuk method getOperasiFakturJual() bisa berupa:

switch (status) {
    case null: return new FakturJualOlehSalesMulai()
    case StatusFakturJual.DIBUAT: return new FakturJualOlehSalesDibuat()
    case StatusFakturJual.DIANTAR: return new FakturJualOlehSalesDiantar()
    case StatusFakturJual.DITERIMA: return new FakturJualOlehSalesDiterima()
    case StatusFakturJual.LUNAS: return new FakturJualOlehSalesLunas()
}
...

Bila seandainya workflow untuk faktur berubah di kemudian hari, saya hanya perlu melakukan perubahan pada method ini. Sebagai contoh, klien yang tidak memiliki manajemen operasional gudang yang baik biasanya tidak ingin fasilitas seperti surat jalan. Bagi klien seperti ini, sebuah faktur yang dibuat akan dianggap sudah dikirim dan diterima oleh pelanggannya. Seandainya saya ingin mendukung 2 jenis workflow serupa, yang bisa dikonfigurasi oleh pengguna, maka saya bisa mengubah kode program getOperasiFakturJual() menjadi seperti berikut ini:

...
if (workflowSingkat) {
  switch (status) {
    case null: return new FakturJualOlehSalesSingkatMulai()
    case StatusFakturJual.DITERIMA: return new FakturJualOlehSalesSingkatDiterima()
    case StatusFakturJual.LUNAS: return new FakturJualOlehSalesSingkatLunas()
  }
} else {
  switch (status) {
    case null: return new FakturJualOlehSalesMulai()
    case StatusFakturJual.DIBUAT: return new FakturJualOlehSalesDibuat()
    case StatusFakturJual.DIANTAR: return new FakturJualOlehSalesDiantar()
    case StatusFakturJual.DITERIMA: return new FakturJualOlehSalesDiterima()
    case StatusFakturJual.LUNAS: return new FakturJualOlehSalesLunas()
  }
}
...

Untuk men-reuse kode program di setiap state, saya bisa menggunakan inheritance. Sebagai contoh, proses() (dan pembayaran) di FakturJualOlehSalesSingkatLunas() tidak berbeda jauh dengan FakturJualOlehSalesLunas. Hanya saja method hapus() di versi singkat akan langsung menghapus pemesanan sementara versi normalnya akan mengembalikan status menjadi DITERIMA. Untuk itu, saya bisa mendefinisikan FakturJualOlehSalesSingkatLunas sebagai turunan dari FakturJualOlehSalesLunas seperti:

class FakturJualOlehSalesSingkatLunas extends FakturJualOlehSalesLunas {

  @Override
  void hapus(FakturJualOlehSales fakturJual) {
     // perubahan disini
  }

}

Dengan penggunaan state pattern, satu-satunya yang perlu saya edit untuk mengubah urutan workflow faktur adalah pada bagian di atas. Kode program presentation layer yang memanggil proses() akan mengerjakan kode program di class yang mewakili state secara otomatis sesuai dengan urutan yang telah saya definisikan.

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.

Belajar Memakai Object Di OOP

Pada artikel Belajar Menerapkan Object Oriented Programming (OOP), saya berusaha menuliskan sudut pandang pada saat membuat program bila menggunakan teknik OOP. Kali ini, saya akan membahas sebuah kendala yang sering kali dijumpai banyak pemula sebelum bisa menerapkan OOP secara efektif: memahami apa itu object!

Banyak pemula yang baru belajar OOP menganggap bahwa sebuah object hanya dipakai untuk menggabungkan beberapa atribut menjadi satu. Walaupun tidak salah, object memiliki kemampuan lebih dari ini. Salah satu ciri khas object adalah masing-masing object memiliki wilayah memori dan siklus hidup tersendiri. Alokasi memori, stack dan siklus hidup sebuah object selalu berdiri sendiri dari object lain walaupun mereka dibuat dari sebuah class yang sama. Dengan demikian, sebuah method yang dikerjakan pada object A hanya akan mempengaruhi variabel di object A tanpa mempengaruhi object B walaupun keduanya dibuat dari sebuah class yang sama.

Sebagai contoh, anggap saja saya merancang tampilan yang mengimplementasikan MVC dimana saya membuat 3 class seperti FakturJualModel, FakturJualView, dan FakturJualController. Ketiga class ini mewakili sebuah tampilan dengan kemampuan melihat, masukkan, mengubah atau menghapus data faktur jual. Lalu, saya membuat 3 object baru dari class di atas dengan pseudocode seperti:

def m1 = new FakturJualModel()
def v1 = new FakturJualView()
def c1 = new FakturJualController()
def mvc = createMVC(m1, v1, c1)
tabbedPane.addTab(title, v1)

Pada saat program dijalankan, akan ada 3 object yang dibuat di memori seperti yang terlihat pada UML Object Diagram berikut ini:

UML Object Diagram yang menggambarkan object yang terbentuk

UML Object Diagram yang menggambarkan object yang terbentuk

Tampilan di program akan terlihat seperti pada gambar berikut ini:

Contoh tampilan program

Contoh tampilan program

Sekarang, saat program masih berjalan, bila pseudocode berikut ini dikerjakan:

def m2 = new FakturJualModel()
def v2 = new FakturJualView()
def c2 = new FakturJualController()
def mvc = createMVC(m2, v2, c2)
tabbedPane.addTab(title, v2)

maka, di memori akan ada 3 object baru yang berbeda, seperti yang terlihat pada UML Object Diagram berikut ini:

Object yang terbentuk di memori setelah tab kedua dibuat

Object yang terbentuk di memori setelah tab kedua dibuat

Tampilan di program akan terlihat seperti pada gambar berikut ini:

Contoh tampilan program

Contoh tampilan program

Saat ini ada 6 object yang berbeda di memori. Masing-masing menyimpan atribut-nya (baca: variabel) di wilayah memori masing-masing. Operasi pada sebuah object hanya akan mempengaruhi atribut di wilayah miliknya atau yang berhubungan dengan dirinya. Dengan demikian, perubahan yang saya lakukan pada tab pertama tidak akan menganggu tab kedua (dan sebaliknya).

Apa yang saya lakukan di atas adalah contoh kasus dimana dimana OOP bisa sangat berguna dibanding teknik prosedural. Pada bahasa yang tidak mengenal object, saya harus mensimulasikan ‘object’ secara manual, misalnya memakai array agar masing-masing tab memiliki ‘data’-nya masing-masing yang tidak saling bercampur aduk. Ini bisa menimbulkan kerumitan lainnya. Bandingkan dengan solusi awal dimana saya hanya perlu membuat object baru dengan new!

Belajar Memakai Git Rebase

Fasilitas rebase di Git dapat dipakai untuk memodifikasi riwayat commit yang sudah ada. Sebagai contoh, rebase dapat dipakai untuk menggantikan merge. Walaupun memberikan hasil yang sama, keduanya memiliki ‘efek samping’ yang berbeda. Operasi merge akan menghasilkan commit baru sementara rebase tidak! Dengan demikian, rebase menghasilkan riwayat yang lebih rapi tanpa percabangan. Walaupun demikian, rebase sebaiknya tidak dipakai bila branch sudah dipublikasikan dan dipakai oleh orang lain. Operasi rebase biasanya hanya untuk merapikan branch yang masih di komputer lokal dan belum dipublikasikan ke server Git.

Untuk menunjukkan penggunaan rebase, anggap saja saya sedang mengerjakan sebuah proyek yang sudah mencapai versi 0.8 dimana isi branch master terlihat seperti pada gambar berikut ini:

Kondisi awal riwayat commit

Kondisi awal riwayat commit

Setelah merilis versi tersebut, saya kemudian membuat sebuah branch baru yang mewakili versi 0.9. Tidak lupa saya juga melakukan merge untuk branch lama ke master. Setelah itu, saya melakukan beberapa perubahan baru untuk versi terbaru (0.9) seperti yang terlihat pada gambar berikut ini:

Membuat branch baru dari master

Membuat branch baru dari master

Pada saat sedang mengembangkan fitur yang dijadwalkan untuk 0.9, saya memperoleh permintaan untuk menyelesaikan kesalahan yang ada di versi 0.8. Karena proritasnya sangat tinggi, saya harus menyelesaikan perubahan pada versi 0.8.1 dan menunda perubahan pada versi 0.9. Saya melakukan cukup banyak perubahan sehingga alur history saya terlihat seperti berikut ini:

Melakukan perubahan pada branch lama

Melakukan perubahan pada branch lama

Sekarang, setelah satu minggu berlalu, saya ingin lanjut mengerjakan apa yang tertunda di branch untuk versi 0.9. Tapi saya sudah melakukan banyak perubahan di versi 0.8 yang sudah di-merge ke branch master. Sementara itu, branch untuk versi 0.9 saat ini masih tetap berisi kode program lama sebelum perubahan versi 0.8.1 ke atas. Untuk memperbaharui branch versi 0.9 agar merujuk pada kode program terbaru di master, saya dapat menggunakan perintah seperti berikut ini:

$ git checkout develop_0.9
Switched to branch 'develop_0.9'

$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Set versions to 0.9
Using index info to reconstruct a base tree...
M       application.properties
Falling back to patching base and 3-way merge...
Auto-merging application.properties
CONFLICT (content): Merge conflict in application.properties
Failed to merge in the changes.
Patch failed at 0001 Set versions to 0.9
The copy of the patch that failed is found in:
   c:/test/.git/rebase-apply/patch

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".

Pesan kesalahan di atas adalah jenis pesan kesalahan yang umum muncul bila terjadi konflik file yang tidak dapat diselesaikan secara otomatis. Yang perlu saya lakukan adalah mengubah file yang bermasalah dan melanjutkan proses rebase seperti yang terlihat pada perintah berikut ini:

$ vi application.properties

$ git add application.properties

$ git rebase --continue
Applying: Set versions to 0.9
Applying: Use c3p0 connection pool.

Sekarang, riwayat akan terlihat seperti pada gambar berikut ini:

Riwayat commit setelah rebase

Riwayat commit setelah rebase

Seluruh perubahan yang saya lakukan di master kini juga muncul di branch untuk versi 0.9. Riwayat terlihat kembali lurus sehingga terlihat rapi.

Sebagai perbandingan, selain memakai rebase, saya juga dapat memakai merge untuk mencapai hasil yang sama, misalnya dengan perintah seperti berikut ini:

$ git merge master
Auto-merging application.properties
CONFLICT (content): Merge conflict in application.properties
Automatic merge failed; fix conflicts and then commit the result.

$ vi application.properties

$ git commit -a -m 'Setelah perubahan 0.8.1'
[develop_0.9 e61a379] Setelah perubahan 0.8.1

Hasil dari merge adalah history seperti pada gambar berikut ini:

Riwayat commit setelah merge

Riwayat commit setelah merge

Terlihat bahwa bila saya memakai merge, commit yang sudah ada untuk branch 0.9 tidak berubah sama sekali. Yang ada adalah commit baru ditambahkan pada branch ini kemudian selisihnya (delta perubahan atau diff) diberikan sebagai sebuah commit baru. Terlihat bahwa penggunaan merge membuat history menjadi tidak rapi lagi, tapi tepat dipakai bila commit yang sudah ada tidak boleh diubah lagi (misalnya telah digunakan oleh developer lagi).

Fungsi lain dari rebase adalah merapikan riwayat commit yang ada, misalnya menghapus commit yang sudah dibuat. Karena Git menyimpan setiap commit sebagai delta dari commit sebelumnya dalam bentuk tree, maka tentu saja saya tidak bisa leluasa menghapus sebuah commit di posisi mana saja sesuka hati (karena setiap commit selalu berhubungan dengan commit sesudahnya). Sebagai contoh, saya memiliki riwayat seperti yang terlihat pada gambar berikut ini:

Kondisi awal riwayat history

Kondisi awal riwayat history

Bila saya ingin menggabungkan 9 commit terakhir menjadi sebuah commit tunggal, maka saya dapat memberikan perintah seperti berikut ini:

$ git rebase -i HEAD~10

Saya kemudian mengubah isi file yang muncul menjadi seperti yang terlihat pada gambar berikut ini:

reword f64b06f Set versions to 0.8.2
fixup 6b5e3cb Change invoice's layout.
fixup 368e0af Add sales report for every products.
fixup 494c91f Add vertical stretch when overflow for product's name.
fixup 0fd37c0 Allow filtering receivable report by invoice periods and region.
fixup 5036f32 Fixes product's quantity with zero value was displayed.
fixup eabcf75 Add warehouse name to report.
fixup 830e5f9 Minor changes to layout.
fixup b46d934 Add line separator.
fixup 68384e9 Sort report data.

Pada file tersebut, saya memilih reword untuk mengubah pesan commit dan fixup untuk menggabungkan sebuah commit dengan commit di atasnya. Selain kedua perintah tersebut, saya juga dapat memberikan perintah lain seperti pick untuk tidak melakukan perubahan, edit untuk mengubah perubahan yang dilakukan oleh commit tersebut, squash yang bekerja seperti fixup dimana saya bisa mengisi pesan commit baru, dan exec untuk mengerjakan perintah shell. Saya juga bisa menghapus sebuah commit dengan menghapus baris yang mewakili commit tersebut.

Setelah menyimpan file di atas, proses rebase akan dimulai. Karena saya mengisi reword pada commit terakhir, maka sebuah editor kembali muncul untuk menanyakan pesan commit terbaru. Saya pun mengisi nama pesan commit baru dan menyimpan file. Setelah proses rebase selesai, saya akan menemukan riwayat seperti yang terlihat seperti pada gambar berikut ini:

Kondisi riwayat commit setelah rebase interaktif yang memakai perintah reword dan fixup

Kondisi riwayat commit setelah rebase interaktif yang memakai perintah reword dan fixup

Seluruh perubahan yang saya lakukan sudah digabungkan menjadi sebuah commit tunggal.

Bagaimana bila saya membuat kesalahan pada saat melakukan rebase? Salah satu fasilitas dari Git adalah setiap kali terdapat perubahan posisi terakhir dari sebuah branch, posisi tersebut akan disimpan pada reflog. Secara default, perintah git gc hanya akan menghapus reflog yang sudah lebih dari 90 hari. Jadi, terdapat peluang sangat besar untuk memperbaiki kesalahan rebase yang baru saja dilakukan.

Untuk melihat informasi reflog, saya memberikan perintah seperti berikut ini:

$ git reflog

Setelah menentukan posisi branch yang benar (seperti HEAD@{1} dan sebagainya), saya kemudian memberikan perintah berikut ini untuk mengembalikan branch tersebut ke kondisi yang diharapkan:

$ git reset --hard HEAD@{1}

Perintah di atas akan mengembalikan posisi branch saat ini ke posisi yang telah ditentukan. Commit yang sudah dihapus dan digabungkan tetap akan dikembalikan seperti semula.

Belajar Menerapkan Object Oriented Programming (OOP)

Tidak dapat dipungkiri bahwa flowchart adalah cara paling alami untuk menggambarkan kode program karena komputer memang mengerjakan baris program secara berurut. Sebagai contoh, berikut ini adalah flowchart untuk menghitung nilai total untuk sebuah faktur:

Contoh Flowchart

Contoh Flowchart

Bila dibuat ke dalam program, akan terlihat seperti berikut ini:

LocalDate tanggal = LocalDate.now();
String nomorFaktur = "FA-001";
String[] produk = new String[]{"ProdukA", "ProdukB"};
int[] jumlah = new int[]{10, 20};
long[] harga = new long[]{1000, 2000};
long total = 0;
for (int i=0; i<produk.length; i++) {
   total += jumlah[i] * harga[i];
}

Salah satu kritik yang sering diberikan pada OOP adalah ia tidak menggambarkan alur eksekusi secara jelas. Namun, sebagai gantinya, ia memiliki sebuah keuntungan yang tidak dimiliki oleh metode prosedural seperti di atas. Bila dilihat sekilas, flowchart di atas berkaitan dengan faktur dan produk. Namun, faktur dan produk tidak tergambar secara jelas di flowchart. Bandingkan dengan UML Class Diagram berikut ini:

Contoh UML Class Diagram

Contoh UML Class Diagram

public class Faktur {
  String nomor;
  LocalDate tanggal;
  List<ItemFaktur> items = new ArrayList<>();

  public long total() {
      long hasil = 0;
      for (ItemFaktur item: items) {
          hasil += item.total();
      }
      return hasil;
  }
}

public class ItemFaktur {
  Produk produk;
  int qty;
  long harga;

  public long total() {
      return qty * harga;
  }
}

public class Produk {
  String nama;
  long hargaEceran;
}

Pada versi OOP, terlihat klasifikasi secara jelas. Letak kode program juga dipisah sesuai dengan klasifikasi yang telah ditentukan. Semakin rumit permasalahan yang dihadapi, metode OOP akan semakin berguna.

Apa beda class diagram (OOP) dengan ERD (tabel database)? Class diagram tidak menggambarkan penampungan data seperti pada ERD. Pada saat membuat ERD, fokus saya adalah menentukan struktur penyimpanan yang efisien. Pada saat membuat class diagram, fokus saya adalah melakukan klasifikasi permasalahan sehingga permasalahan menjadi lebih mudah dipahami dan diselesaikan.

Apa yang digambarkan oleh class diagram adalah kumpulan class yang saling berinteraksi. Class-class ini tentunya tidak bisa bekerja sendiri. Mereka harus direalisasikan dalam bentuk object, misalnya dengan kode program seperti berikut ini:

Faktur faktur1 = new Faktur(...);
faktur1.total();

Faktur faktur2 = new Faktur(...);
faktur2.total();

Alur eksekusi program tidak terlihat secara jelas di class diagram. Untuk itu, UML memiliki activity diagram yang mirip flowchart. Tapi diagram yang paling tepat untuk menggambarkan interaksi antar-class secara berurutan adalah sequence diagram. Sebagai contoh, objek untuk class Faktur akan diciptakan di memori pada saat tombol Simpan di-klik, seperti yang ditunjukkan pada diagram berikut ini:

Contoh UML Sequence Diagram

Contoh UML Sequence Diagram

public void simpan_click() {
   Faktur f = new Faktur();
   f.tanggal = datePicker.getDate();
   f.nomor = txtNomor.getText();
   f.items = tblItems.getItems();
   txtTotal.text = f.total();
}

Pada class diagram untuk domain model, saya tidak perlu menggambarkan class milik tampilan UI seperti form dan tombol. Setiap class yang ada di class diagram tersebut harus dapat bekerja tanpa terikat oleh UI atau database. Perhitungan total dan interaksi lainnya harus dapat bekerja, baik ada UI maupun tidak ada. Dengan demikian, objek untuk class Faktur tidak hanya bisa diciptakan oleh UI, tapi juga di unit test untuk pengujian otomatis dan sebagainya.

Seandainya saya memakai ORM seperti JPA, maka saya bisa menyimpan objek Faktur secara mudah ke database. ORM adalah framework yang akan menyimpan object ke tabel di database relasional secara otomatis. Alur eksekusi saat tombol Simpan di-klik kini akan terlihat seperti pada diagram berikut ini:

Menyimpan Ke Database

Menyimpan Ke Database

public void simpan_click() {
   Faktur f = new Faktur();
   ...
   txtTotal.text = f.total();
   dao.simpan(f);
}

Sekali lagi, saya tidak memasukkan class DAO ke dalam rancangan class diagram domain. Ingat bahwa domain class yang ada harus dapat bekerja baik ada database maupun tidak ada. Kode program pada UI (seperti saat tombol di-klik) dan kode program pada DAO (untuk menyimpan ke database) disebut sebagai berada pada layer terpisah. Mereka bukan bagian dari permasalahan utama (inventory, akuntansi, restoran, dsb). Kode program pada layer yang berbeda bisa ditulis secara terpisah setelah domain class selesai dirancang.

Pada faktur, biasanya nomor perlu dihasilkan secara otomatis. Bila kode program untuk menghasilkan nomor secara otomatis tidak berada di class diagram, kemungkinan besar ia ada di database (dalam bentuk auto increment primary key atau trigger) atau di UI. Ini adalah sesuatu yang tidak disarankan! Sebagai contoh, bila terletak di UI, ia akan terlihat seperti berikut ini:

public void simpan_click() {
   Faktur f = new Faktur();
   int nomorTerakhir = dao.findFakturCount();
   f.nomor = 'FA-' + nomorTerakhir;
   ...
}

Kode program untuk menghasilkan nomor faktur adalah bagian dari permasalahan bisnis. Misalnya suatu saat nanti pengguna bisa menginginkan format yang berbeda. Kode program seperti ini juga harus reusable. Dengan demikian, ia seharusnya tergambar dalam class diagram. Oleh sebab itu, saya bisa membuat repository seperti yang terlihat pada gambar berikut ini:

Memakai Repository

Memakai Repository

Saya meletakkan kode program yang menghasilkan nomor berurut pada method buat() di FakturRepository. Sekarang, alur interaksi class menjadi berikut ini:

Sequence Diagram Setelah Penggunaan Repository

Sequence Diagram Setelah Penggunaan Repository

Kode program yang memakai class akan terlihat seperti:

public void simpan_click() {
   Faktur f = new Faktur();
   f.tanggal = datePicker.getDate();
   f.items = tblItems.getItems();
   fakturRepository.buat(f);
   ...
}

Kesimpulan:

  1. Terlihat bahwa OOP tidak menunjukkan alur eksekusi program secara jelas. Walaupun demikian, ia sangat mendukung pemodelan yang mempermudah developer dalam mengelola kode program. Kode program prosedural lebih mudah ditelusuri tapi rentan menyebabkan kebingungan di kemudian hari terutama bila cakupan kode program semakin luas.
  2. Kode program pada domain class harus bisa dikerjakan tanpa terikat pada UI, database, dan sebagainya. Ini menimbulkan pembagian secara horizontal yang disebut layer dimana setiap layer memiliki tugas-nya masing-masing yang tidak memiliki sangkut paut dengan internal di layer lain. Contoh layer yang umum dijumpai adalah presentation layer untuk menampilkan GUI dan persistence layer untuk menyimpan object ke database.

Tips Memakai Enterprise Architect Untuk Merancang Domain Model

Merancang UML class diagram boleh dibilang adalah sesuatu yang bersifat pribadi. Hasil rancangan setiap orang bisa berbeda-beda tergantung kreatifitas dan style masing-masing. Mungkin architect yang pernah memakai brush di Photoshop akan setuju dengan pendapat saya (apa hubungan Photoshop dan UML?!). Yup! Perbedaan yang ada sah-sah saja selama hasil rancangan dapat dipahami dan dipakai oleh anggota tim lainnya. Dan itu sebabnya mahasiswa yang membuat UML class diagram dengan tool reverse engineering guna sekedar mengisi halaman skripsi tidak akan mendapat manfaat dari UML (selain pujian dari dosen yang kagum melihat tumpukan class diagram tanpa makna 🙂 ).

Pada artikel ini, saya akan menuliskan beberapa hal yang sering saya lakukan dalam merancang domain model dengan menggunakan Enterprise Architect (EA) dari Sparx Systems.

Struktur Proyek

Umumnya proyek pada Enterprise Architect terdiri atas satu atau lebih model package yang memiliki satu atau lebih view. Model package ditujukan untuk melihat (atau meng-analisa) sistem yang sama dari beberapa aspek yang berbeda seperti requirement, development, dan sebagainya. Sebagai contoh, saya bisa membuat struktur proyek di EA seperti berikut ini:

Tampilan project browser

Tampilan project browser

Sebuah view tidak terbatas pada satu diagram. Ia boleh saja memiliki banyak diagram berbeda. Sebagai contoh, pada sebuah proyek inventory, saya dapat memiliki beberapa UML class diagram berbeda seperti pada gambar berikut ini:

Beberapa diagram berbeda dalam sebuah view

Beberapa diagram berbeda dalam sebuah view

Saya lebih senang memisahkan sebuah class diagram besar ke dalam beberapa diagram terpisah. Hal ini akan memberikan fokus bagi saya selama merancang. Selain itu, saya (dan anggota tim lain) akan lebih mudah memahami diagram karena hanya perlu membaca diagram yang berkaitan dengan permasalahan yang sedang dihadapi (tanpa harus menelusuri seluruh elemen yang ada). Bukankah itu kelebihan OOP? Developer tidak perlu tahu flowchart mulai dari A sampai Z untuk bisa mulai coding; developer hanya perlu fokus pada class yang terlibat.

Lalu, apa patokan dalam memisahkan diagram ke dalam beberapa diagram yang lebih kecil? Bila memakai domain driven design seperti yang saya lakukan, maka patokan yang tepat adalah boundary context. Setiap boundary context diwakili oleh sebuah diagram. Komunikasi antar diagram yang berbeda diusahakan seminimal mungkin dan hanya melalui class penghubung atau event.

Sebagai contoh, yang menghubungkan diagram pembelian dan diagram penjualan dengan diagram inventory adalah class Produk. Diagram pembelian hanya mengakses Produk yang berada di inventory. Ia sama sekali tidak mengakses class lainnya di inventory seperti Gudang dan Satuan secara langsung. Developer yang mengembangkan pembelian hanya perlu memahami Produk tanpa perlu tahu banyak internal lainnya yang ada di diagram inventory. Ia mungkin akan memakai Gudang atau Satuan secara tidak langsung melalui method di Produk, tapi ia tidak perlu tahu tentang itu. Semakin sedikit yang harus diketahui oleh developer, maka permasalahan dalam benaknya menjadi semakin sederhana, sehingga ia bisa menghasilkan kode program secara lebih cepat dan berkualitas.

Agar lebih rapi, saya dapat mengelompokkan class pada UML package diagram sehingga saya bisa memiliki daftar seluruh class yang ada. Untuk keperluan itu, saya bisa memakai sebuah view yang terdiri atas sebuah UML package diagram dimana masing-masing package diwakili sebuah UML class diagram. Sebagai contoh, struktur proyek saya kini berubah menjadi seperti berikut ini:

Pengelompokan dengan UML package diagram

Pengelompokan dengan UML package diagram

Tampilan package diagram akan terlihat seperti pada gambar berikut ini:

Contoh tampilan UML package diagram

Contoh tampilan UML package diagram

Memperjelas Diagram Dengan Diagram Lainnya

Ada saatnya dimana saya merasa bahwa class diagram tidak cukup jelas untuk menggambarkan permasalahan yang sedang saya hadapi. Untuk itu, saya terkadang menambahkan diagram lain untuk memperjelas class diagram tersebut. Pada EA, saya dapat melakukan hal ini dengan men-klik kanan sebuah class dan memilih menu Add seperti yang terlihat pada gambar berikut ini:

Menambah diagram lain pada class diagram

Menambah diagram lain pada class diagram

Sebagai contoh, saya memperjelas class FakturBeli dengan sebuah UML state machine diagram sehingga struktur proyek terlihat seperti pada gambar berikut ini:

Tampilan di project browser

Tampilan di project browser

Saya bisa melihat state machine diagram tersebut secara terpisah dengan men-double click diagramnya:

Contoh UML state machine diagram

Contoh UML state machine diagram

Memperjelas Diagram Dengan Constraint Dan Note

Constraint dapat dipakai untuk mendeskripsikan business rule sehingga pembaca diagram menjadi semakin memahami asosiasi yang ada. Untuk menambahkan constraint, saya dapat men-double click sebuah asosiasi, lalu memilih Constraints seperti yang terlihat pada gambar berikut ini:

Menambah constraint pada asosiasi

Menambah constraint pada asosiasi

Constraint yang saya tulis akan ditampilkan oleh EA langsung pada diagram seperti yang terlihat pada gambar berikut ini:

Tampilan constraint

Tampilan constraint

Saya juga bisa memberikan keterangan untuk class atau diagram dengan menggunakan note. EA menyediakan elemen yang mewakili catatan yang dapat ditemui dalam bentuk:

Memilih note

Memilih note

Setelah elemen note dibuat, ia dapat diasosiasikan dengan sebuah class seperti yang terlihat pada gambar berikut ini:

Class diagram dengan note

Class diagram dengan note

Kreatif Dengan UML Stereotype

UML membolehkan penggunanya untuk menciptakan kosa kata baru melalui fasilitas stereotype. Hampir seluruh elemen yang ada seperti class, asosiasi, atribut, dan method dapat memiliki sterotype-nya sendiri. EA memungkinkan saya untuk memakai atau menambah stereotype yang sudah ada seperti yang terlihat pada gambar berikut ini:

Memakai stereotype di EA

Memakai stereotype di EA

Pada contoh di atas, saya memberi stereotype simple-jpa pada semua class yang harus memiliki dynamic finders simple-jpa. Dengan demikian, saya tahu bahwa class-class tersebut memiliki method tambahan walaupun saya tidak menampilkannya di diagram.

Selain itu, pada class yang memiliki banyak atribut atau method, saya dapat menggunakan stereotype untuk melakukan pemisahan seperti yang terlihat pada gambar berikut ini:

Memakai stereotype untuk mengkategorikan method

Memakai stereotype untuk mengkategorikan method

Terlalu banyak method pada sebuah class adalah sebuah smell code. Tapi hal ini tidak dapat dihindari pada aggregate root yang menerapkan composite pattern. Method seperti bayar() sebenarnya akan mendelegasikan tugasnya ke KewajibanPembayaran.bayar(). Cara terbaik yang bisa saya lakukan adalah melakukan kategorisasi melalui stereotype sehingga class tetap mudah dibaca.

Menyembunyikan Detail Yang Tidak Dibutuhkan

Saya memiliki class FakturJual yang diturunkan dari Faktur. Keduanya berada di diagram yang berbeda (faktur dan penjualan). Pada diagram penjualan, saya tidak perlu menyertakan Faktur karena saya hanya ingin berkonsentrasi pada FakturJual. Bila hanya menyertakan FakturJual tanpa Faktur, maka EA akan menampilkan struktur inheritance untuk class tersebut menjadi seperti pada gambar berikut ini:

Superclass tidak harus selalu ditampilkan

Superclass tidak harus selalu ditampilkan

Untuk menambahkan superclass yang berada di diagram lain, saya dapat men-klik kanan class dan memilih menu Advanced, Parent… seperti yang terlihat pada gambar berikut ini:

Menambah superclass

Menambah superclass

Bila saya harus memakai class dari diagram lain, saya dapat men-drag class dari diagram lain tersebut ke diagram yang sedang aktif. EA akan memberikan informasi bahwa class tersebut berada di package lain seperti yang terlihat pada gambar berikut ini:

Class dari diagram lain

Class dari diagram lain

Bila menyertakan class dari diagram lain seperti pada gambar di atas, biasanya saya tidak ingin menampilkan detail untuk class tersebut. Hal ini karena detail class tersebut termasuk asosiasinya bisa dilihat sendiri di diagram tempat ia dibuat. Untuk menyembunyikan atribute atau method dari sebuah class, saya dapat men-klik kanan class tersebut dan memilih menu Feature Visibility…. Pada dialog yang muncul, saya dapat menentukan apa saja yang perlu ditampilkan atau disembunyikan, seperti yang terlihat pada gambar berikut ini:

Menyembunyikan attribute dan method

Menyembunyikan attribute dan method

Saya juga dapat memilih Wrap Features atau Truncate Features agar class dengan nama atribut atau method yang panjang dapat diperkecil sehingga tidak memakan banyak tempat.

Merancang Satuan Dan Harga

Seorang teman beberapa kali ini menghubungi saya untuk berdiskusi mengenai masalah yang dihadapinya. Ia sedang merancang sistem retail dimana ia harus mengelola produk yang bisa memiliki harga yang berbeda di satuan yang berbeda. Apa yang harus saya lakukan? Tidak ada jawaban ‘terbaik’ karena seberapa efektif sebuah rancangan tidak hanya mengenai seberapa jelas dan mudah dipahami, tetapi juga kebutuhan bisnis, teknologi dan pengalaman programmer yang dimiliki. Seperti biasa, saya akan mencoba membuat rancangan yang diimplementasikan dengan Groovy, Griffon dan simple-jpa.

Saya akan mulai dengan merancangan satuan. Sebuah nilai dalam satuan tertentu harus bisa di-konversi ke nilai dalam satuan lainnya. Sebagai contoh, 1 m adalah 100 cm, 1.000 mm, dan 39,3701 inch. Contoh lainnya, 1 lusin terdiri atas 12 unit dan 1 gross terdiri atas 144 unit. Tidak semua satuan bisa saling dikonversi. Sebagai contoh, satuan seperti meter tidak boleh dikonversi menjadi lusin.

Rancangan awal saya yang saya buat terlihat seperti pada gambar berikut ini:

Kuantitas, Satuan dan Konversi

Kuantitas, Satuan dan Konversi

Pada rancangan di atas, saya berusaha memisahkan tanggung jawab dan peran sebisa mungkin menjadi sebuah class tersendiri. Tujuannya adalah agar suatu saat nanti setelah class semakin berkembang, tidak ada class ‘gendut’ yang sulit dimodifikasi. Yang dimaksud ‘sulit’ disini adalah penuh kekhawatiran akan dampak buruk bila di-‘senggol’ sedikit. Secara umum, rancangan yang baik menghasilkan class yang bebannya ditanggung secara bersama dan merata.

Selain itu, saya tidak lupa menambahkan perilaku (behavior /method). Sebuah rancangan yang hanya berisi class beserta attribut tanpa method adalah gejala rancangan yang tidak baik yang disebut sebagai anemic domain model. Mengapa anemic domain model adalah gejala rancangan yang buruk? Sebuah aplikasi PASTI memiliki operasi. Bila rancangan hanya berisi data dalam bentuk atribut, maka dimana letaknya operasi tersebut? Kenapa operasi tersebut tidak ada dalam diagram? Dimana letaknya kode program seperti if, for, while yang mengolah data? Mereka tentunya harus punya ‘tempat’ di class yang sedang dirancang.

Salah satu prinsip yang dapat dipakai untuk mengukur loose coupling dari rancangan adalah Law of Demeter (LoD). Pada LoD, sebuah class hanya boleh berkomunikasi dengan class yang berhubungan langsung dengannya. Sebagai contoh, class Kuantitas akan memanggil method milik class Satuan, tapi tidak boleh mengakses Konversi milik Satuan tersebut secara langsung. Oleh sebab itu, class Satuan menyediakan wrapper yang mewakili nilai milik Konversi. Dengan demikian, bila suatu saat nanti class Konversi perlu berubah drastis, maka dampak terbesarnya hanya pada class Satuan karena class lain tidak mengakses Konversi secara langsung.

Seperti apa contoh implementasinya di Groovy? Class SatuanTidakCocok adalah sebuah Exception yang mewakili kesalahan sehingga saya bisa membuatnya seperti berikut ini:

import ...

@Canonical
class SatuanTidakCocok extends RuntimeException {

    Satuan dari
    Satuan ke

    @Override
    String getMessage() {
        "Tidak dapat melakukan konversi dari $dari ke $ke"
    }
}

Class Konversi adalah sebuah value object sehingga pada JPA, ia perlu memakai annotation @Embeddable seperti yang terlihat berikut ini:

import ...

@Embeddable @Canonical
class Konversi {

    @NotNull
    BigDecimal faktorSatuanBaku

    @ManyToOne
    Satuan satuanBaku

}

Class Satuan adalah sebuan entity yang implementasinya bisa berupa:

import ...

@DomainClass @Entity @Canonical
class Satuan {

    @NotEmpty
    String nama

    @Embedded
    Konversi konversi

    BigDecimal faktor(Satuan satuanLain) {
        if (satuanLain.equals(this)) {
            return 1
        } else if (satuanLain.equals(satuanBaku())) {
            return konversi.faktorSatuanBaku
        } else if (satuanLain.satuanBaku().equals(satuanBaku())) {
            return faktor(satuanBaku()) / satuanLain.faktor(satuanBaku())
        } else {
            throw new SatuanTidakCocok(satuanLain, this)
        }
    }

    Satuan satuanBaku() {
        konversi? konversi.satuanBaku: this
    }

    boolean equals(o) {
        if (this.is(o)) return true
        if (getClass() != o.class) return false

        Satuan satuan = (Satuan) o

        if (id != satuan.id) return false
        if (nama != satuan.nama) return false

        return true
    }

    int hashCode() {
        return nama.hashCode() + id
    }

}

Karena Satuan memiliki operasi yang cukup rumit, maka saya akan membuat sebuah unit test untuk menguji class tersebut, misalnya:

import ...

class SatuanTests {

    @Test
    void testSatuanBaku() {
        Satuan satuanM = new Satuan('Meter')
        Satuan satuanCM = new Satuan('Centimeter', new Konversi(1/100, satuanM))
        Satuan satuanMM = new Satuan('Milimeter', new Konversi(1/1000, satuanM))
        Satuan satuanInch = new Satuan('Inch', new Konversi(1/39.3701, satuanM))

        Satuan satuanPcs = new Satuan('Pcs')
        Satuan satuanLusin = new Satuan('Lusin', new Konversi(12, satuanPcs))
        Satuan satuanGross = new Satuan('Gross', new Konversi(144, satuanPcs))

        assertEquals(satuanM, satuanM.satuanBaku())
        assertEquals(satuanM, satuanCM.satuanBaku())
        assertEquals(satuanM, satuanMM.satuanBaku())
        assertEquals(satuanM, satuanInch.satuanBaku())

        assertEquals(satuanPcs, satuanPcs.satuanBaku())
        assertEquals(satuanPcs, satuanLusin.satuanBaku())
        assertEquals(satuanPcs, satuanGross.satuanBaku())
    }

    @Test
    public void testFaktorUntukSatuanSama() {
        Satuan satuanM = new Satuan('Meter')
        Satuan satuanCM = new Satuan('Centimeter', new Konversi(1/100, satuanM))

        assertEquals(0, satuanM.faktor(satuanM).compareTo(1))
        assertEquals(0, satuanCM.faktor(satuanCM).compareTo(1))
    }

    @Test
    public void testFaktor() {
        Satuan satuanM = new Satuan('Meter')
        Satuan satuanCM = new Satuan('Centimeter', new Konversi(1/100, satuanM))
        Satuan satuanMM = new Satuan('Milimeter', new Konversi(1/1000, satuanM))
        Satuan satuanInch = new Satuan('Inch', new Konversi(1/39.3701, satuanM))

        assertEquals('100,000', String.format("%.3f",satuanM.faktor(satuanCM)))
        assertEquals('0,010', String.format("%.3f", satuanCM.faktor(satuanM)))
        assertEquals('2,540', String.format("%.3f", satuanInch.faktor(satuanCM)))
        assertEquals('0,039', String.format("%.3f", satuanMM.faktor(satuanInch)))

        Satuan satuanPcs = new Satuan('Pcs')
        Satuan satuanLusin = new Satuan('Lusin', new Konversi(12, satuanPcs))
        Satuan satuanGross = new Satuan('Gross', new Konversi(144, satuanPcs))

        assertEquals(12, satuanLusin.faktor(satuanPcs).intValue())
        assertEquals(144, satuanGross.faktor(satuanPcs).intValue())
    }

    @Test
    public void testSatuanTidakCocok() {
        Satuan satuanM = new Satuan('Meter')
        Satuan satuanPcs = new Satuan('Pcs')

        GroovyAssert.shouldFail(SatuanTidakCocok) {
            satuanM.faktor(satuanPcs)
        }
    }
}

Pada pengujian diatas, saya memakai salah satu fasilitas unik di Groovy yaitu GroovyAssert.shouldFail(). Method tersebut akan menggagalkan pengujian bila seandainya Exception yang diharapkan tidak terjadi.

Saya kemudian menjalankan pengujian dan memastikan bahwa semua method di atas lulus, seperti yang terlihat pada gambar berikut ini:

Hasil unit test yang sukses

Hasil unit test yang sukses

Berikutnya, saya akan membuat implementasi untuk class Kuantitas seperti berikut ini:

import ...

@Embeddable @Canonical
class Kuantitas {

    @NotNull
    BigDecimal jumlah

    @ManyToOne @NotNull
    Satuan satuan

    Kuantitas to(Satuan satuanLain) {
        new Kuantitas(jumlah * satuan.faktor(satuanLain), satuanLain)
    }

    Kuantitas plus(Kuantitas kuantitas) {
        new Kuantitas(jumlah + kuantitas.to(satuan).jumlah, satuan)
    }

    Kuantitas minus(Kuantitas kuantitas) {
        plus(new Kuantitas(-kuantitas.jumlah, kuantitas.satuan))
    }

}

Pada kode program di atas, method minus() akan memanggil method plus() dengan nilai negatif karena pada dasarnya a-b adalah a+(-b). Saya juga bisa melakukannya untuk pembagian dimana a/b adalah a*(1/b).

Berikutnya, saya membuat unit test baru untuk menguji class Kuantitas seperti berikut ini:

import ...

class KuantitasTests {

    @Test
    void testTo() {
        Satuan satuanM = new Satuan('Meter')
        Satuan satuanCM = new Satuan('Centimeter', new Konversi(1/100, satuanM))
        Satuan satuanMM = new Satuan('Milimeter', new Konversi(1/1000, satuanM))
        Satuan satuanInch = new Satuan('Inch', new Konversi(1/39.3701, satuanM))

        Kuantitas nilai1 = new Kuantitas(10, satuanM)
        assertEquals(1000, nilai1.to(satuanCM).jumlah.intValue())
        assertEquals(10000, nilai1.to(satuanMM).jumlah.intValue())
        assertEquals('393,701', String.format('%.3f', nilai1.to(satuanInch).jumlah))

        Satuan satuanPcs = new Satuan('Pcs')
        Satuan satuanLusin = new Satuan('Lusin', new Konversi(12, satuanPcs))
        Satuan satuanGross = new Satuan('Gross', new Konversi(144, satuanPcs))

        Kuantitas nilai2 = new Kuantitas(15, satuanLusin)
        assertEquals(180, nilai2.to(satuanPcs).jumlah.intValue())
        assertEquals('1,250', String.format('%.3f', nilai2.to(satuanGross).jumlah))

        Kuantitas nilai3 = new Kuantitas(2, satuanGross)
        assertEquals(288, nilai3.to(satuanPcs).jumlah.intValue())
        assertEquals(24, nilai3.to(satuanLusin).jumlah.intValue())
    }

    @Test
    void testPlus() {
        Satuan satuanM = new Satuan('Meter')
        Satuan satuanCM = new Satuan('Centimeter', new Konversi(1/100, satuanM))
        Satuan satuanInch = new Satuan('Inch', new Konversi(1/39.3701, satuanM))

        Kuantitas nilai1 = new Kuantitas(5, satuanM)
        Kuantitas nilai2 = new Kuantitas(10, satuanCM)
        Kuantitas nilai3 = new Kuantitas(2, satuanInch)
        Kuantitas hasil = nilai1 + nilai2 + nilai3

        assertEquals('5,1508', String.format('%.4f', hasil.jumlah))
        assertEquals(satuanM, hasil.satuan)

        hasil += nilai1
        assertEquals('10,1508', String.format('%.4f', hasil.jumlah))
        assertEquals(satuanM, hasil.satuan)
    }

    @Test
    void testMinus() {
        Satuan satuanPcs = new Satuan('Pcs')
        Satuan satuanLusin = new Satuan('Lusin', new Konversi(12, satuanPcs))
        Satuan satuanGross = new Satuan('Gross', new Konversi(144, satuanPcs))

        Kuantitas nilai1 = new Kuantitas(2, satuanGross)
        nilai1 -= new Kuantitas(3, satuanLusin)
        nilai1 -= new Kuantitas(10, satuanPcs)

        assertEquals(242, nilai1.to(satuanPcs).jumlah.intValue())
        assertEquals(satuanGross, nilai1.satuan)
    }

    @Test
    void testSatuanTidakSama() {
        Satuan satuanPcs = new Satuan('Pcs')
        Satuan satuanM = new Satuan('Meter')

        GroovyAssert.shouldFail(SatuanTidakCocok) {
            new Kuantitas(2,satuanPcs) + new Kuantitas(3,satuanM)
        }
    }

}

Kode program pada unit test akan mewakili seperti apa class saat dipakai (misalnya saat dipanggil di controller atau dipanggil dari class lain). Pada contoh ini, saya memanfaatkan fasilitas khusus dari Groovy untuk melakukan operator overloading. Method dengan nama plus() milik Kuantitas akan dikerjakan bila terdapat ekspresi dengan tanda tambah seperti kuantitas1 + kuantitas2. Begitu juga dengan method minus() yang akan dikerjakan bila terdapat ekspresi seperti kuantitas1 - kuantitas2. Ini salah satu contoh dimana operator overloading sangat berguna: ia membuat class Kuantitas lebih mudah dipakai dan dipahami.

Berikutnya, saya perlu merancang class yang mewakili produk dan harganya. Setiap produk memiliki jumlah dalam satuan tertentu. Mereka juga bisa dijual dalam bentuk satuan yang berbeda dengan harga yang berbeda. Saya tidak tahu seperti apa pricing model yang dipakai karena saya tidak memiliki akses ke domain expert. Oleh sebab itu saya mencari informasi di buku atau web mengenai pricing model. Agar sederhana, saya akan menganggap klien ingin memakai model all-units discounts dimana bila pembelian mencapai kuantitas tertentu (atau pada satuan tertentu), maka harga seluruh unit akan mendapatkan diskon yang sama. Sebagai contoh, harga sebuah produk Snack A bila dibeli secara eceran adalah Rp 3.000,-/pcs. Bila dibeli secara lusin, maka harga Snack A menjadi Rp. 2.800,-/pcs sehingga harga 1 lusin adalah Rp 33.600,-/lusin (dimana tanpa diskon adalah Rp 36.000,-/lusin). Bila dibeli secara gross, maka harga Snack A menjadi Rp 2.600,-/pcs sehingga harga 1 gross adalah Rp Rp 374.400,-/gross (dimana tanpa diskon adalah Rp. 432.000,-/gross). Dengan kata lain, saya dapat menyimpulkan bahwa harga Snack A mendapat diskon Rp 200,- dari harga eceran bila dibeli per lusin dan mendapat diskon Rp 400,- dari harga eceran bila dibeli per gross.

Saya kemudian mengubah rancangan sehingga menjadi seperti berikut ini:

Hasil akhir setelah penambahan Produk, Diskon dan Harga

Hasil akhir setelah penambahan Produk, Diskon dan Harga

Pada rancangan di atas, saya membuat sebuah value object baru yang diwakili class Harga. Pada dasarnya Harga dan Kuantitas hampir sama; perbedaannya adalah mereka saling terbalik dalam melakukan konversi. Untuk Kuantitas, 2 lusin adalah 2 x 12= 24 pieces. Untuk Harga, Rp 12.000,-/lusin adalah 12.000 / 12 = Rp 1.000,-/pieces. Karena kesamaannya, hubungan antara Kuantitas dan Harga dapat berupa spesialisasi/inheritance. Akan tetapi, karena saya memakai Hibernate, saya menjumpai keterbatasan teknologi dimana sebuah class @Embeddable tidak dapat diturunkan dari class lain. Keputusan seperti ini perlu diperhatikan sehingga tidak menghasilkan rancangan yang tidak dapat diimplementasikan pada teknologi yang dipakai.

Saya memakai qualifier Satuan pada hubungan antara Produk dan Diskon untuk memperlihatkan bahwa setiap Produk memiliki Diskon per Satuan. Saya meniru teknik ini setelah membacanya di buku Domain Driven Design (DDD). Implementasi untuk rancangan tersebut dilakukan dengan menggunakan Map yang memiliki asosiasi keyvalue.

Mengapa ada method addDiskonByHargaSatuan(Harga) di Produk? Teman saya menjelaskan bahwa terkadang pengguna hanya ingin memasukkan harga per satuan seperti Rp 33.600,- per lusin (dimana harga per piece adalah Rp 3.000,-). Padahal, sistem harus menyimpannya dengan nilai berupa diskon Rp 200,- per piece. Ia kemudian mengemukakan idenya bahwa sebaiknya pengguna menghitung nilai diskon secara manual melalui kalkulator. Atau alternatif lainnya, biarkan programmer menghitung sendiri nilai ini di view. Tapi, sebuah solusi yang lebih baik adalah memasukkan proses perhitungan ini pada rancangan dalam bentuk sebuah method karena perhitungan ini adalah bagian dari business logic.

Contoh implementasi yang mungkin untuk class Diskon adalah:

import ...

@Embeddable @Canonical
class Diskon {

    BigDecimal potonganLangsung

    BigDecimal potonganPersen

    BigDecimal hasilDiskon(BigDecimal nilai) {
        nilai - jumlahDiskon(nilai)
    }

    BigDecimal jumlahDiskon(BigDecimal nilai) {
        (potonganPersen?:0) / 100 * nilai + (potonganLangsung?:0)
    }
}

Untuk menguji class ini, saya membuat unit test seperti:

import ...

class DiskonTests {

    @Test
    void testHasilDiskon() {
        Diskon diskon = new Diskon(1000)
        assertEquals(49000, diskon.hasilDiskon(50000).intValue())

        diskon = new Diskon(potonganPersen: 5)
        assertEquals(47500, diskon.hasilDiskon(50000).intValue())

        diskon = new Diskon(1000, 5)
        assertEquals(46500, diskon.hasilDiskon(50000).intValue())
    }

    @Test
    void testJumlahDiskon() {
        Diskon diskon = new Diskon(1000)
        assertEquals(1000, diskon.jumlahDiskon(50000).intValue())

        diskon = new Diskon(potonganPersen: 5)
        assertEquals(2500, diskon.jumlahDiskon(50000).intValue())

        diskon = new Diskon(1000, 5)
        assertEquals(3500, diskon.jumlahDiskon(50000).intValue())
    }
}

Setelah memastikan bahwa Diskon sudah bisa bekerja baik dan benar, saya lanjut membuat implementasi untuk Harga seperti yang terlihat berikut ini:

import ...

@Embeddable @Canonical
class Harga {

    @NotNull
    BigDecimal jumlah

    @NotNull @ManyToOne
    Satuan satuan

    public Harga to(Satuan satuanLain) {
        new Harga(jumlah / satuan.faktor(satuanLain), satuanLain)
    }

    public Harga plus(Harga hargaLain) {
        if (!hargaLain.satuan.equals(satuan)) {
            throw new SatuanTidakCocok(hargaLain.satuan, satuan)
        }
        new Harga(jumlah + hargaLain.to(satuan).jumlah, satuan)
    }

    public Harga minus(Harga hargaLain) {
        plus(new Harga(-hargaLain.jumlah, hargaLain.satuan))
    }

    public Harga multiply(Number angka) {
        new Harga(angka * jumlah, satuan)
    }

    public Harga multiply(Kuantitas kuantitas) {
        if (!kuantitas.satuan.equals(satuan)) {
            throw new SatuanTidakCocok(kuantitas.satuan, satuan)
        }
        multiply(kuantitas.jumlah)
    }

    public Harga diskon(Diskon diskon) {
        new Harga(diskon.hasilDiskon(jumlah), satuan)
    }

}

Class Harga memiliki cukup banyak operasi, sehingga saya perlu lebih rajin membuat unit test untuk menguji kebenarannya:

import ...

class HargaTests {

    @Test
    void testTo() {
        Satuan satuanM = new Satuan('Meter')
        Satuan satuanCM = new Satuan('Centimeter', new Konversi(1/100, satuanM))
        Satuan satuanMM = new Satuan('Milimeter', new Konversi(1/1000, satuanM))

        Harga harga = new Harga(1000, satuanM)
        assertEquals(1000, harga.to(satuanM).jumlah.intValue())
        assertEquals(10, harga.to(satuanCM).jumlah.intValue())
        assertEquals(1, harga.to(satuanMM).jumlah.intValue())

        harga = new Harga(2000, satuanCM)
        assertEquals(200000, harga.to(satuanM).jumlah.intValue())
        assertEquals(2000, harga.to(satuanCM).jumlah.intValue())
        assertEquals(200, harga.to(satuanMM).jumlah.intValue())
    }

    @Test
    void testPlus() {
        Satuan satuanM = new Satuan('Meter')
        Satuan satuanCM = new Satuan('Centimeter', new Konversi(1/100, satuanM))

        Harga harga1 = new Harga(1000, satuanM)
        Harga harga2 = new Harga(1100, satuanM)
        Harga harga3 = new Harga(2100, satuanM)
        assertEquals(harga3, harga1+harga2)

        GroovyAssert.shouldFail(SatuanTidakCocok) {
            harga1 + new Harga(1100, satuanCM)
        }
    }

    @Test
    void testMinus() {
        Satuan satuanM = new Satuan('Meter')
        Satuan satuanCM = new Satuan('Centimeter', new Konversi(1/100, satuanM))

        Harga harga1 = new Harga(1100, satuanM)
        Harga harga2 = new Harga(1000, satuanM)
        Harga harga3 = new Harga(100, satuanM)
        assertEquals(harga3, harga1-harga2)

        GroovyAssert.shouldFail(SatuanTidakCocok) {
            harga1 - new Harga(1100, satuanCM)
        }
    }

    @Test
    void testMultiplyNumber() {
        Satuan satuanM = new Satuan('Meter')

        Harga harga1 = new Harga(1000, satuanM)
        Harga harga2 = new Harga(5000, satuanM)
        assertEquals(harga2, harga1 * 5)

        harga2 = new Harga(3300, satuanM)
        assertEquals(harga2, harga1 * 3.3)
    }

    @Test
    void testMultiplyKuantitas() {
        Satuan satuanM = new Satuan('Meter')
        Satuan satuanCM = new Satuan('Centimeter', new Konversi(1/100, satuanM))

        Harga harga = new Harga(1000, satuanM)
        Kuantitas kuantitas = new Kuantitas(5, satuanM)
        Harga hasil = new Harga(5000, satuanM)
        assertEquals(hasil, harga * kuantitas)

        GroovyAssert.shouldFail(SatuanTidakCocok) {
            kuantitas = new Kuantitas(5, satuanCM)
            harga * kuantitas
        }
    }

    @Test
    void testDiskon() {
        Satuan satuanM = new Satuan('Meter')
        Harga harga = new Harga(1000, satuanM)

        Diskon diskon = new Diskon(100)
        Harga hasil = new Harga(900, satuanM)
        assertEquals(hasil, harga.diskon(diskon))

        diskon = new Diskon(potonganPersen: 1.5)
        hasil = new Harga(985, satuanM)
        assertEquals(hasil, harga.diskon(diskon))

        diskon = new Diskon(100, 3)
        hasil = new Harga(870, satuanM)
        assertEquals(hasil, harga.diskon(diskon))
    }
}

Berikutnya, saya membuat implementasi untuk class Produk yang terlihat seperti berikut ini:

import ...

@DomainClass @Entity @Canonical(excludes='diskon')
class Produk {

    @NotBlank
    String kode

    @NotBlank
    String nama

    @NotNull @Embedded
    Kuantitas jumlah

    @NotNull @Embedded
    Harga harga

    @ElementCollection(fetch=FecthType.EAGER)
    Map<Long, Diskon> diskon = [:]

    void setDiskon(Satuan satuan, Diskon jumlahDiskon) {
        diskon[satuan.id] = jumlahDiskon
    }

    Diskon getDiskon(Satuan satuan) {
        diskon[satuan.id]
    }

    void addDiskonByHargaSatuan(Harga hargaSatuan) {
        Satuan satuanBaku = harga.satuan.satuanBaku()
        Harga selisih = harga.to(satuanBaku) - hargaSatuan.to(satuanBaku)
        diskon[hargaSatuan.satuan.id] = new Diskon(selisih.jumlah)
    }

    Harga harga(Satuan satuan) {
        Satuan satuanBaku = harga.satuan.satuanBaku()
        Harga hargaSatuan = harga.to(satuanBaku)
        if (diskon[satuan.id]) {
            hargaSatuan = hargaSatuan.diskon(diskon[satuan.id])
        }
        hargaSatuan.to(satuan)
    }

}

Salah satu keterbatasan pada @Embeddable Map di JPA Hibernate saat ini adalah bila key berupa entity maka eager loading tidak dapat dilakukan! Ini adalah sebuah bug yang masih belum diselesaikan hingga saat ini. Karena saya ingin fasilitas eager loading, saya tidak bisa mendefinisikan Map. Sebagai gantinya, saya memakai Map dimana Long adalah id dari Satuan yang hendak dijadikan sebagai key. Untuk mempermudah mengakses map tersebut, saya menyediakan setDiskon(Satuan, Diskon) dan Diskon getDiskon(Satuan).

Hal lain yang harus diperhatikan bila memakai Hibernate dalam penggunaan @Embeddable adalah kemungkinan dua atribut di @Embeddable berbeda dengan nama yang sama. Hibernate akan gagal membuat tabel bila terdapat kolom dengan nama yang sama, seperti kolom satuan di Kuantitas dan kolom satuan di Harga dimana baik Kuantitas dan Harga dipakai oleh Produk. Untuk menghindari hal ini, saya menambahkan baris berikut ini pada persistence.xml:

...
<property name="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.DefaultComponentSafeNamingStrategy" />
...

Memakai Hibernate rasanya ribet sekali, bukan? Tapi sejujurnya kompleksitas ini terlahir dari ketidaksesuaian antara OOP dan tabel relasional. Bila seandainya saya harus membuat sebuah framework ORM dari awal, saya bisa menciptakan sesuatu yang sangat sederhana untuk operasi CRUD. Namun seiring berbagai permasalahan yang muncul, pada akhirnya mungkin akan kompleks seperti Hibernate. Hal ini akibat masalah-masalah relasional yang memang tidak dapat dihindari. Jadi, untuk saat ini, lebih baik memakai sesuatu yang sudah matang seperti Hibernate.

Saya kemudian membuat unit test untuk menguji kebenaran class Produk:

import ...

class ProdukTests {

    @Test
    void testSetDiskonByHargaSatuan() {
        Satuan satuanPcs = new Satuan(id: 1, nama: 'Pcs')
        Satuan satuanLusin = new Satuan(id: 2, nama: 'Lusin', konversi: new Konversi(12, satuanPcs))
        Satuan satuanGross = new Satuan(id: 3, nama: 'Gross', konversi: new Konversi(144, satuanPcs))

        Produk produk = new Produk('SA', 'Snack A')
        produk.harga = new Harga(3000, satuanPcs)

        produk.addDiskonByHargaSatuan(new Harga(3000, satuanPcs))
        produk.addDiskonByHargaSatuan(new Harga(33600, satuanLusin))
        produk.addDiskonByHargaSatuan(new Harga(374400, satuanGross))

        assertEquals(0, produk.getDiskon(satuanPcs).potonganLangsung.intValue())
        assertEquals(200, produk.getDiskon(satuanLusin).potonganLangsung.intValue())
        assertEquals(400, produk.getDiskon(satuanGross).potonganLangsung.intValue())
    }

    @Test
    void testHarga() {
        Satuan satuanPcs = new Satuan(id: 1, nama: 'Pcs')
        Satuan satuanLusin = new Satuan(id: 2, nama: 'Lusin', konversi: new Konversi(12, satuanPcs))
        Satuan satuanGross = new Satuan(id: 3, nama: 'Gross', konversi: new Konversi(144, satuanPcs))

        Produk produk = new Produk('SA', 'Snack A')
        produk.harga = new Harga(3000, satuanPcs)
        produk.setDiskon(satuanLusin, new Diskon(200))
        produk.setDiskon(satuanGross, new Diskon(400))
        assertEquals(3000, produk.harga(satuanPcs).jumlah.intValue())
        assertEquals(satuanPcs, produk.harga(satuanPcs).satuan)
        assertEquals(33600, produk.harga(satuanLusin).jumlah.intValue())
        assertEquals(satuanLusin, produk.harga(satuanLusin).satuan)
        assertEquals(374400, produk.harga(satuanGross).jumlah.intValue())
        assertEquals(satuanGross, produk.harga(satuanGross).satuan)

        produk = new Produk('SB', 'Snack B')
        produk.harga = new Harga(150000, satuanLusin)
        produk.setDiskon(satuanGross, new Diskon(0, 0.5))
        assertEquals(12500, produk.harga(satuanPcs).jumlah.intValue())
        assertEquals(satuanPcs, produk.harga(satuanPcs).satuan)
        assertEquals(150000, produk.harga(satuanLusin).jumlah.intValue())
        assertEquals(satuanLusin, produk.harga(satuanLusin).satuan)
        assertEquals(1791000, produk.harga(satuanGross).jumlah.intValue())
        assertEquals(satuanGross, produk.harga(satuanGross).satuan)
    }
}

Salah satu konsekuensi dari perancangan class Produk seperti ini adalah ia terus mengingat diskon per satuan dasar walaupun harga dimodifikasi ke satuan lain. Sebagai contoh:

Produk produk = new Produk('SA', 'Snack A')
produk.harga = new Harga(3000, satuanPcs)
produk.addDiskonByHargaSatuan(new Harga(33600, satuanLusin))
assertEquals(33600, produk.harga(satuanLusin).jumlah.intValue())

// Perubahan harga dasar
produk.harga = new Harga(39000, satuanLusin)
assertEquals(3250, produk.harga(satuanPcs).jumlah.intValue())
assertEquals(36600, produk.harga(satuanLusin).jumlah.intValue())

Perilaku ini mungkin diharapkan dan mungkin tidak diharapkan tergantung pada kebutuhan pengguna. Bila diskon tidak perlu di-ingat, maka setiap kali harga baru di-set, nilai dari diskon perlu dikosongkan, seperti:

class Produk {

  ...

  void setHarga(Harga hargaBaru) {
     diskon.clear()
     this.harga = hargaBaru
  }

  ...

}

Lalu bagaimana dengan class ProdukRepository dan SatuanRepository? Mereka adalah repository yang bertugas menyimpan dan mencari entity Produk dan Satuan. Plugin simple-jpa telah menyediakan fasilitas tersebut sehingga saya tidak perlu membuat kode program yang mengerjakan SQL lagi. Contoh implementasinya terlihat seperti:

import ...

@Transaction
class ProdukRepository {

    public void create(Produk produk) {
        persist(produk)
    }

}
import ...

@Transaction
class SatuanRepository {

    public void create(Satuan satuan) {
        persist(satuan)
    }

}

Bagaimana cara menguji repository? Karena mereka mengambil dan menyimpan object dari dan ke database, maka pengujian sebaiknya dilakukan pada integration test. Perbedaan integration test dan unit test adalah unit test hanya melibatkan object yang terisolasi di memori (tanpa hubungan ke dunia luar seperti database, internet, dsb). Berbeda dengan unit test, pada integration test, saya harus mendefinisikan object secara lengkap karena kali ini validasi akan dilakukan pada saat menyimpan object ke database. Integration test juga lebih lambat dikerjakan bila dibandingkan dengan unit test.

Contoh integration test yang saya buat untuk menguji penyimpanan di database adalah:

import ...

class ProdukTest {

    @Test
    public void testCreate() {
        SatuanRepository satuanRepository = new SatuanRepository()
        Satuan satuanPcs = new Satuan('Pcs')
        Satuan satuanLusin = new Satuan('Lusin', new Konversi(12, satuanPcs))
        Satuan satuanGross = new Satuan('Gross', new Konversi(144, satuanPcs))
        satuanRepository.create(satuanPcs)
        satuanRepository.create(satuanLusin)
        satuanRepository.create(satuanGross)

        ProdukRepository produkRepository = new ProdukRepository()
        Produk produkA = new Produk(kode: 'SA', nama: 'Snack A', harga: new Harga(3000, satuanPcs), jumlah: new Kuantitas(10, satuanPcs))
        Produk produkB = new Produk(kode: 'SB', nama: 'Snack B', harga: new Harga(2000, satuanPcs), jumlah: new Kuantitas(10, satuanPcs))
        produkB.setDiskon(satuanLusin, new Diskon(200))
        produkRepository.create(produkA)
        produkRepository.create(produkB)

        Produk p = produkRepository.findProdukByNama('Snack A')
        assertEquals(produkA, p)
        assertEquals(36000, p.harga(satuanLusin).jumlah.intValue())

        p = produkRepository.findProdukByNama('Snack B')
        assertEquals(produkB, p)
        assertEquals(21600, p.harga(satuanLusin).jumlah.intValue())

        assertEquals(2, produkRepository.findAllProdukByNamaLike('Snack%').size())
    }
}

Bila saya menjalankan integration test di atas, Hibernate akan membuat struktur tabel yang terlihat seperti berikut ini:

Struktur tabel yang dihasilkan Hibernate

Struktur tabel yang dihasilkan Hibernate

Mereka yang terbiasa mengakses tabel secara langsung melalui SQL biasanya sering kali menganggap bahwa sebuah class adalah padanan dari sebuah tabel di database. Tapi tidak selalu demikian. Sebagai contoh, pada rancangan saya terdapat 9 class tetapi tabel yang dibutuhkan hanya 3 tabel (plus 1 tabel khusus untuk keperluan hibernate). Tidak semua dari class tersebut perlu disimpan nilainya dalam tabel. Beberapa bahkan bisa digabung, seperti class yang mengandung inheritance dan embeddable. Selain itu, perbedaan antara ERD dan class diagram terlihat secara jelas: tidak ada operasi di ERD sehingga saya tidak tahu bagaimana proses dan interaksi yang ada dalam program bila hanya melihat ERD!

Pada integration test di atas, tidak ada query SQL yang dilakukan karena repository memakai simple-jpa yang menyediakan dynamic finders. Lalu seperti ada SQL yang dihasilkan di balik layar? Berikut adalah cuplikan log yang saya peroleh:

insert into hibernate_sequences(sequence_name, sequence_next_hi_value) values('Satuan', ?)

update hibernate_sequences set sequence_next_hi_value = ? where sequence_next_hi_value = ? and sequence_name = 'Satuan'

insert into Satuan (createddate, deleted, konversi_faktorsatuanbaku, konversi_satuanbaku_id, modifieddate, nama, id) values (?, ?, ?, ?, ?, ?, ?)

insert into Satuan (createddate, deleted, konversi_faktorsatuanbaku, konversi_satuanbaku_id, modifieddate, nama, id) values (?, ?, ?, ?, ?, ?, ?)

insert into Satuan (createddate, deleted, konversi_faktorsatuanbaku, konversi_satuanbaku_id, modifieddate, nama, id) values (?, ?, ?, ?, ?, ?, ?)

select sequence_next_hi_value from hibernate_sequences where sequence_name = 'Produk' for update

insert into hibernate_sequences(sequence_name, sequence_next_hi_value) values('Produk', ?)

update hibernate_sequences set sequence_next_hi_value = ? where sequence_next_hi_value = ? and sequence_name = 'Produk'

insert into Produk (createddate, deleted, harga_jumlah, harga_satuan_id, jumlah_jumlah, jumlah_satuan_id, kode, modifieddate, nama, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

insert into Produk (createddate, deleted, harga_jumlah, harga_satuan_id, jumlah_jumlah, jumlah_satuan_id, kode, modifieddate, nama, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

insert into Produk_diskon (Produk_id, diskon_KEY, diskon_value_potonganlangsung, diskon_value_potonganpersen) values (?, ?, ?, ?)

select distinct produk0_.id as id1_4_, produk0_.createddate as createdd2_4_, produk0_.deleted as deleted3_4_, produk0_.harga_jumlah as harga_ju4_4_, produk0_.harga_satuan_id as harga_sa9_4_, produk0_.jumlah_jumlah as jumlah_j5_4_, produk0_.jumlah_satuan_id as jumlah_10_4_, produk0_.kode as kode6_4_, produk0_.modifieddate as modified7_4_, produk0_.nama as nama8_4_ from Produk produk0_ where produk0_.nama=?

select satuan0_.id as id1_6_0_, satuan0_.createddate as createdd2_6_0_, satuan0_.deleted as deleted3_6_0_, satuan0_.konversi_faktorsatuanbaku as konversi4_6_0_, satuan0_.konversi_satuanbaku_id as konversi7_6_0_, satuan0_.modifieddate as modified5_6_0_, satuan0_.nama as nama6_6_0_, satuan1_.id as id1_6_1_, satuan1_.createddate as createdd2_6_1_, satuan1_.deleted as deleted3_6_1_, satuan1_.konversi_faktorsatuanbaku as konversi4_6_1_, satuan1_.konversi_satuanbaku_id as konversi7_6_1_, satuan1_.modifieddate as modified5_6_1_, satuan1_.nama as nama6_6_1_ from Satuan satuan0_ left outer join Satuan satuan1_ on satuan0_.konversi_satuanbaku_id=satuan1_.id where satuan0_.id=?

select diskon0_.Produk_id as Produk_i1_4_0_, diskon0_.diskon_value_potonganlangsung as diskon_v2_5_0_, diskon0_.diskon_value_potonganpersen as diskon_v3_5_0_, diskon0_.diskon_KEY as diskon_K4_0_ from Produk_diskon diskon0_ where diskon0_.Produk_id=?

select distinct produk0_.id as id1_4_, produk0_.createddate as createdd2_4_, produk0_.deleted as deleted3_4_, produk0_.harga_jumlah as harga_ju4_4_, produk0_.harga_satuan_id as harga_sa9_4_, produk0_.jumlah_jumlah as jumlah_j5_4_, produk0_.jumlah_satuan_id as jumlah_10_4_, produk0_.kode as kode6_4_, produk0_.modifieddate as modified7_4_, produk0_.nama as nama8_4_ from Produk produk0_ where produk0_.nama=?

select satuan0_.id as id1_6_0_, satuan0_.createddate as createdd2_6_0_, satuan0_.deleted as deleted3_6_0_, satuan0_.konversi_faktorsatuanbaku as konversi4_6_0_, satuan0_.konversi_satuanbaku_id as konversi7_6_0_, satuan0_.modifieddate as modified5_6_0_, satuan0_.nama as nama6_6_0_, satuan1_.id as id1_6_1_, satuan1_.createddate as createdd2_6_1_, satuan1_.deleted as deleted3_6_1_, satuan1_.konversi_faktorsatuanbaku as konversi4_6_1_, satuan1_.konversi_satuanbaku_id as konversi7_6_1_, satuan1_.modifieddate as modified5_6_1_, satuan1_.nama as nama6_6_1_ from Satuan satuan0_ left outer join Satuan satuan1_ on satuan0_.konversi_satuanbaku_id=satuan1_.id where satuan0_.id=?

select diskon0_.Produk_id as Produk_i1_4_0_, diskon0_.diskon_value_potonganlangsung as diskon_v2_5_0_, diskon0_.diskon_value_potonganpersen as diskon_v3_5_0_, diskon0_.diskon_KEY as diskon_K4_0_ from Produk_diskon diskon0_ where diskon0_.Produk_id=?

select distinct produk0_.id as id1_4_, produk0_.createddate as createdd2_4_, produk0_.deleted as deleted3_4_, produk0_.harga_jumlah as harga_ju4_4_, produk0_.harga_satuan_id as harga_sa9_4_, produk0_.jumlah_jumlah as jumlah_j5_4_, produk0_.jumlah_satuan_id as jumlah_10_4_, produk0_.kode as kode6_4_, produk0_.modifieddate as modified7_4_, produk0_.nama as nama8_4_ from Produk produk0_ where produk0_.nama like ?

select satuan0_.id as id1_6_0_, satuan0_.createddate as createdd2_6_0_, satuan0_.deleted as deleted3_6_0_, satuan0_.konversi_faktorsatuanbaku as konversi4_6_0_, satuan0_.konversi_satuanbaku_id as konversi7_6_0_, satuan0_.modifieddate as modified5_6_0_, satuan0_.nama as nama6_6_0_, satuan1_.id as id1_6_1_, satuan1_.createddate as createdd2_6_1_, satuan1_.deleted as deleted3_6_1_, satuan1_.konversi_faktorsatuanbaku as konversi4_6_1_, satuan1_.konversi_satuanbaku_id as konversi7_6_1_, satuan1_.modifieddate as modified5_6_1_, satuan1_.nama as nama6_6_1_ from Satuan satuan0_ left outer join Satuan satuan1_ on satuan0_.konversi_satuanbaku_id=satuan1_.id where satuan0_.id=?

select diskon0_.Produk_id as Produk_i1_4_0_, diskon0_.diskon_value_potonganlangsung as diskon_v2_5_0_, diskon0_.diskon_value_potonganpersen as diskon_v3_5_0_, diskon0_.diskon_KEY as diskon_K4_0_ from Produk_diskon diskon0_ where diskon0_.Produk_id=?

select diskon0_.Produk_id as Produk_i1_4_0_, diskon0_.diskon_value_potonganlangsung as diskon_v2_5_0_, diskon0_.diskon_value_potonganpersen as diskon_v3_5_0_, diskon0_.diskon_KEY as diskon_K4_0_ from Produk_diskon diskon0_ where diskon0_.Produk_id=?

Terlihat bahwa terdapat permasalahan N+1 query dimana setelah mengerjakan query SELECT untuk memperoleh sebuah Produk dari tabel, Hibernate akan mengerjakan query terpisah untuk membaca Produk.Kuantitas.satuan dan masing-masing Produk.Diskon. Perilaku ini bisa dioptimalkan lagi dengan menggunakan left join atau dengan memberikan query secara manual, tapi selama tidak ada penalti yang siginifikan pada kinerja (seperti pada contoh di atas), ini adalah sesuatu yang bisa diterima.

Langkah berikutnya adalah membuat presentation untuk rancangan. Designer yang baik biasanya tidak merancang berdasarkan patokan presentation (tampilan). Oleh sebab itu, saya tidak membatasi jenis presentation yang bisa dipakai untuk rancangan ini. Selama merancang, saya hanya memikirkan bagaimana programmer nantinya memakai object yang ada. Bisa saja presentation berupa komponen Swing untuk aplikasi desktop atau tampilan HTML berbasis web. Atau bisa saja tidak ada presentation sama sekali, misalnya terdapat services layer yang mempublikasikan domain model dalam bentuk web services. Apapun jenis presentation yang dipakai, cara kerjanya tetap sama, controller di presentation akan memanggil repository untuk mengambil dan membuat root aggregate (seperti Produk dan Satuan), lalu mengerjakan business logic yang ada di domain object yang diperolehnya (seperti to(), addDiskon(), harga() dan sebagainya).

Apa itu Agile Manifesto?

Sering kali terdengar pengembangan software secara agile disebut dimana-mana. Sebenarnya apa itu agile software development? Apa setiap kali membuat program dengan cepat sudah termasuk memakai teknik agile ini? Pada tanggal 17 Februari 2001, tujuh belas developer (termasuk diantaranya adalah Ken Beck dan Martin Fowler) berjumpa untuk mendiskusikan teknik pengembangan software yang ringan dan lincah. Mereka kemudian menghasilkan apa yang disebut sebagai The Agile Manifesto yang terdiri atas 4 nilai dan 12 prinsip. Isinya dapat dilihat di http://agilemanifesto.org. Bahkan, saya juga dapat melihat hasil terjemahan dalam Bahasa Indonesia yang dapat dilihat di http://agilemanifesto.org/iso/id/ yang isinya adalah:

Manifesto Pengembangan Perangkat Lunak Agile

Kami menemukan cara yang lebih baik untuk mengembangkan
perangkat lunak dengan melakukan dan membantu sesama untuk menggunakannya.
Melalui usaha ini kami telah dapat menghargai:

Individu dan interaksi lebih dari proses dan sarana perangkat lunak
Perangkat lunak yang bekerja lebih dari dokumentasi yang menyeluruh
Kolaborasi dengan klien lebih dari negosiasi kontrak
Tanggap terhadap perubahan lebih dari mengikuti rencana

Demikian, walaupun kami menghargai hal di sisi kanan, kami lebih menghargai hal di sisi kiri.

Beberapa permasalahan yang berkaitan dengan individu dan interaksi dapat terpantau dari contoh perkataan seperti berikut ini:

  1. “Loh, modul gw ‘kan pake modulnya si B! Gw hanya tinggal pakai, titik! Yang elo bilang error sebenarnya tuh dibuat sama B!” (melempar tanggung jawab dan saling menyalahkan).
  2. “Apa sih yang gw dapat selama kerja disini? Tiap hari cuman copy-paste. Emanknya ga ada kerjaan lain ya?” (tidak termotivatasi)
  3. “Hah, udah berubah? Sejak kapan?? Percuma gw mati-matian bikinnya!!” (kurangnya kolaborasi)

Salah satu ciri tim yang agile adalah tim yang kompak dan masing-masing anggotanya dapat bekerja sendiri tanpa harus diperintah. Pada Dreyfus model, terdapat 5 jenis peran seseorang, yaitu novice (butuh panduan dan bimbingan), advanced beginner (masih berfokus pada proses), competent (dapat melakukan perencanaan), proficient (dapat melihat situasi secara keseluruhan), dan expert (dapat menemukan jalan dalam situasi baru). Sebuah tim yang sukses harus memiliki sejumlah anggota yang mencapai peran competent atau proficient. Ini adalah syarat agar salah satu prinsip Agile Manifesto dapat diterapkan:

Kembangkan proyek di sekitar individual yang termotivasi.
Berikan mereka lingkungan dan dukungan
yang mereka butuhkan, dan percayai mereka
untuk menyelesaikan pekerjaan dengan baik.

Prinsip di atas juga berarti tidak ada pemaksaan tool yang dipakai oleh developer. Sebagai contoh, developer boleh memakai IDE dan sistem operasi favorit mereka. Dengan memberikan kebebasan bagi developer, mereka diharapkan memiliki produktifitas semaksimal mungkin.

Berdasarkan prinsip agile, setiap developer tidak boleh masa bodoh terhadap pekerjaan developer lain. Mereka harus memahami hasil pekerjaan rekannya sehingga tidak saling melempar tanggung jawab. Hal ini akan membuat mereka lebih percaya diri dan menghasilkan kode program yang lebih berkualitas. Untuk mencapai kondisi ini, praktisi agile sering menerapkan apa yang disebut sebagai pair programming dimana dua developer bekerja di depan satu komputer. saat seorang developer bekerja, developer yang satunya lagi mengamati. Tujuannya adalah agar pengetahuan dapat tersebar secara merata ke seluruh developer. Tidak ada lagi ‘anak emas’ yang menjadi pusat konsentrasi pekerjaan. Tapi pada prakteknya, entah mengapa, saya sering merasa selalu ada pihak-pihak yang ingin menutup akses pengetahuan 😉

Dokumentasi seperti diagram BPMN dan UML memang penting, tapi sebaiknya bukan merupakan media utama dalam berkomunikasi dengan klien. Gunakan perangkat lunak yang bekerja sebagai media komunikasi. Salah satu cara yang dapat ditempuh adalah dengan menggunakan metode iteration. Sebagai contoh, saya memiliki kebiasaan untuk memakai versi prototype yang diawali 0 seperti 0.1, 0.2, 0.3, dan seterusnya. Selama pengembangan, fokus saya adalah menghasilkan perangkat lunak yang bekerja secepat mungkin (walaupun belum mencakup seluruh kebutuhan), kemudian meminta feedback (masukan) dari klien. Saya mencatat keluhan dari klien seperti fitur yang kurang atau tampilan yang terlalu sulit dipakai. Kemudian tim developer akan memperbaharui software, merilis versi berikutnya, dan saya kembali menunjukkan software pada klien. Proses ini dilakukan terus menerus hingga klien merasa puas (atau mungkin bosan 😉 ).

Tidak semua klien memahami metodeiterasi. Beberapa klien menganggap bahwa software adalah sebuah benda mati yang diam dan tinggal di-copy sehingga mereka hanya ingin terima ‘jadi’. Padahal kolaborasi dengan klien adalah hal yang penting. Untuk membuat klien awam bersedia bekerja sama, saya sering memakai analogi seperti: “Microsoft terus berinovasi menciptakan Windows 95, Windows 98, Windows 2000, Windows XP, Windows 7 hingga Windows 8 mengikuti kebutuhan pelanggannya. Bayangkan bila Microsoft hanya merilis Windows 95 saja, apakah dia bisa memenuhi kebutuhan pelanggannya sekarang? Demikian juga dengan kami, produk XXX 0.1 akan terus berinovasi menjadi 0.2, 0.3 dan seterusnya untuk menyesuaikan dengan kebutuhan bisnis Anda.” Terkadang saya akan memakai contoh perusahan dan produk lain yang dikenal oleh orang awam, misalnya Google dan Android. Tujuannya adalah untuk menunjukkan bahwa software adalah sesuatu yang dinamis dan bersifat iteratif, dimana pemicu setiap iterasi adalah feedback dari klien.

Iterasi yang dilengkapi kolaborasi dengan klien adalah sesuatu yang sangat efektif bila mendapat dukungan penuh dari semua pihak. Setiap iterasi menghasilkan software yang dapat ditunjukkan pada klien. Pada tahap ini, walaupun software belum dipakai di production, masing-masing pihak sudah memiliki gambar seperti apa hasil akhirnya nanti. Kenapa harus repot seperti ini? Mengapa tidak langsung membuat software yang dapat langsung dipakai di production? Berdasarkan pengalaman, software ‘pertama kali’ yang dipresentasikan kepada klien tidak selalu sesuai kebutuhan. Klien selalu meminta perubahan. Jadi mengapa harus menghabiskan waktu lama untuk menghasilkan software ‘pertama’ kali’ ini?

Sebagai contoh, saya pernah bekerja di industri software yang memakai metode konvensional. Proyek diawali dengan pertemuan antara tim analyst dan klien selama beberapa minggu. Setelah analyst kembali ke kantor, mereka mulai membuat diagram dan membagi tugas ke developer. Saat ini para developer tentu saja belum memahami apa yang mereka kerjakan; oleh sebab itu, mereka akan sering bertanya ke analyst. Terkadang analyst juga belum memahami permasalahan secara sepenuhnya, sehingga mereka berusaha menjawab ‘ala kadar’-nya, terkadang memakai insting atau memberi jawaban diplomatis seperti “kerjakan saja sebisa elo!” ;). Resiko kesalahpahaman dalam tim sangat tinggi! Hari demi hari berlalu, bulan terus berganti, dan tahun baru pun tiba. Developer sudah bekerja keras menciptakan apa yang diimpikan analyst. Hari dimana system integration testing (SIT) pun tiba. Semua anggota tim merasa bangga! Namun, saat user acceptance test (UAT) dilakukan, alangkah terkejutnya klien melihat dirinya mendapatkan sebuah software yang sangat berbeda dari yang dibutuhkannya padahal sudah menanti selama setahun!

Contoh di atas adalah contoh klasik kegagalan yang terjadi akibat lebih mementingkan negosiasi kontrak dan mengikuti rencana. Pengembangan perangkat lunak agile menyarankan untuk lebih mementingkan kolaborasi dengan klien lebih dari negosiasi kontrak. Selain itu, contoh di atas lebih mementingkan proses (misalnya, tahapan pengembangan dari analyst ke developer) dan juga adanya gap ‘pengetahuan’ yang mencolok antara analyst dan developer (karena developer tidak ikut serta dalam kolaborasi dengan klien).

Pengembangan secara iteratif menyebabkan proses pengembangan software harus tanggap terhadap perubahan. Penjadwalan tidak butuh gantt chart dimana ada milestone yang tersusun secara rapi dan harus dipatuhi. Satu-satunya jadwal (selain estimasi selesai) adalah target jangka pendek untuk merilis iterasi terbaru. Setiap rilis mungkin saja melibatkan perubahan signifikan pada codebase sehingga developer juga harus siap terhadap perubahan (misalnya dengan selalu membuat unit test dan melakukan refactoring secara teratur).

Agile manifesto mendeskripsikan roh dari metode pengembangan software agile, tapi ia tidak memberikan panduan atau kerangka kerja. Untuk kerangka kerja, saya dapat melihat beberapa ‘implementasi’-nya seperti Scrum, Extreme Programming (XP), Adaptive Software Development (ASD), Crystal, Agile Unified Process (AUP), dan sebagainya. Perlu diingat bahwa sama seperti metode pengembangan lainnya, metode agile yang disebutkan bukanlah sebuah solusi yang dapat memecahkan semua masalah dalam segala situasi. Tidak ada salahnya menyesuaikan metode agile dengan kebutuhan dan situasi, sambil terus mempertahankan nilai dan prinsip dalam agile manifestor.