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.

Perihal Solid Snake
I'm nothing...

Apa komentar Anda?

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

Logo WordPress.com

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

Gambar Twitter

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

Foto Facebook

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

Foto Google+

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

Connecting to %s

%d blogger menyukai ini: