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.

Iklan

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 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 Bedanya Merancang Sebuah Class Dan Sebuah Tabel?

Salah satu pertanyaan yang sering ditanyakan saat mulai belajar merancang aplikasi secara OOP adalah apa bedanya sebuah class dan sebuah tabel di database?   Hal ini cukup meresahkan terutama bagi mereka yang sudah memiliki pengalaman merancang tabel relasional.   Mereka mulai meragukan rancangan class diagram saat mereka mempertimbangkan kembali berbagai prinsip di tabel relasional seperti hubungan antar-tabel dan normalisasi.   Hingga pada akhirnya,  beberapa memilih menganggap class sebagai representasi data layaknya sebuah tabel.

Jadi,  apakah merancang class diagram secara OOP adalah hal yang sama seperti merancang tabel di ERD?   Sebenarnya class dan tabel adalah dua hal yang berada dalam konteks berbeda sehingga mereka tidak dapat dibandingkan,  tapi saya akan mencoba menjawab pertanyaan ini.

Berikut ini adalah cuplikan dari ERD hasil rancangan seorang developer untuk dipakai pada secara prosedural:

Contoh Rancangan Tabel Relasional

Contoh Rancangan Tabel Relasional

Rancangan di atas bukanlah rancangan relasional terbaik, masih banyak yang bisa diperbaiki dari rancangan tersebut.   Tapi satu hal yang pasti, rancangan di atas adalah rancangan yang mudah dipakai melalui bahasa pemograman prosedural dan SQL.

Saya akan mencoba menerjemahkan rancangan tersebut ke dalam class diagram.   Jangan lupa bahwa sebuah class diagram tetap pada akhirnya harus dipetakan ke dalam tabel.   Saya akan mengandaikan bahwa hasil rancangan di class diagram tidak perlu memiliki struktur tabel yang sama seperti versi prosedural di atas.

1.  Class Memiliki Struktur Inherintance

Pada rancangan ERD,  terlihat bahwa tabel faktur, faktur_retur, faktur_beli, dan faktur_mutasi memiliki sebuah kesamaan,  yaitu sama-sama merupakan sebuah faktur.   Mereka juga memiliki beberapa field yang sama seperti nomor faktur dan tanggal.   Secara OOP, pemisahan ini sangat disarankan, melalui hierarki inheritance dimana faktur_beli  adalah sebuah (is-a)  faktur, faktur_jual adalah sebuah faktur, dan seterusnya, seperti yang terlihat pada gambar berikut ini:

Inheritance Di Rancangan OOP

Inheritance Di Rancangan OOP

Pada pemikiran database relasional, inheritance mirip seperti pemisahan tabel dengan relasi one-to-one.   Tapi ada perbedaan mendasar yang membuat mereka berbeda jauh.

Pada tabel, perancang akan memisahkan tabel (normalisasi) untuk mengurangi duplikasi guna menghemat penyimpanan;  begitu juga ia akan menggabungkan tabel (denormalisasi) untuk mengurangi join sehingga bisa meningkatkan kinerja.

Pada OOP, pertimbangan perancang adalah mensimulasikan ‘permasalahan‘ dengan menciptakan class-class yang mewakili dunia nyata.   Dengan demikian,  permasalahan dapat dihadapi dengan cara yang lebih mirip seperti pemikiran manusia (bukan pemikiran flow chart gaya komputer).

Memisahkan sebuah class atau melakukan inheritance, bukan saja mengelompokkan data, tetapi juga melakukan abstraction. Perubahan kode program pada superclass secara otomatis akan diterapkan pada seluruh subclass-nya; pertimbangan ini tidak ditemui pada perancangan tabel karena sebuah tabel hanya mewakili data tanpa kode program.

2. Class Lebih Menyerupai “Dunia Nyata” Dibanding Tabel

Tujuan dari sebuah tabel adalah untuk menampung data.  Sementara itu, tujuan sebuah perancangan secara OOP adalah untuk mempermudah developer dalam memahami permasalahan.

Tabel faktur (faktur_jual) menampung seluruh data yang berkaitan dengan sebuah faktur.   Pada tabel faktur, terlihat bahwa terdapat field-field yang bertujuan untuk menyimpan data pembayaran.   Umumnya mereka akan diproses oleh dua halaman/screen yang berbeda, misalnya screen ‘Tambah Faktur‘ untuk mengisi data faktur secara umum dan screen ‘Tambah Pembayaran‘ untuk mengisi pembayaran sebuah faktur yang jatuh tempo.   Ini merupakan kandidat untuk memisahkan mereka menjadi dua class yang berbeda, seperti yang terlihat pada gambar berikut ini:

Pemisahan Class Berdasarkan Perannya

Pemisahan Class Berdasarkan Perannya

Class Pembayaran dapat dimodifikasi lebih lanjut, misalnya untuk mendukung pembayaran bertahap, seperti yang terlihat pada gambar berikut ini:

Pengembangan Class Yang Telah Dipisah

Pengembangan Class Yang Telah Dipisah

Dengan memisahkan class Pembayaran dari class FakturJual, saya memungkinkan kode program yang sama untuk pembayaran dipakai ulang di class FakturBeli seperti yang terlihat pada gambar berikut ini:

Pemisahan Class Meningkatkan Reusability

Pemisahan Class Meningkatkan Reusability

Pada contoh class di atas, data beserta dengan kode program yang berkaitan dengan Pembayaran akan di-reuse, sehingga perubahan mekanisme Pembayaran hanya perlu dilakukan pada satu class saja.   Sebagai perbandingan, ‘reusability‘ pada sebuah tabel adalah merujuk primary key untuk sebuah record (berorientasi pada data dan bagaimana menghemat media penyimpanan).

3. Class Juga Mewakili Operasi Seperti Perhitungan

Field disc di tabel faktur (master) dan field disc & discrp di tabel penjualan (detail) dipakai untuk menyimpan nilai diskon.  Mereka hanya sebuah nilai.   Tanpa melihat kode program secara langsung, akan sulit mengetahui bagaimana perhitungan yang melibatkan nilai tersebut.

Berbeda dengan tabel, sebuah class tidak hanya mewakili data, tetapi juga berisi operasi seperti perhitungan.   Field yang dipakai untuk perhitungan diskon di tabel di atas merupakan kandidat yang tepat untuk sebuah class tersendiri yaitu Diskon.   Berikut adalah contoh rancangannya:

Class Yang Mewakili Perhitungan

Class Yang Mewakili Perhitungan

Class Diskon adalah sebuah abstract class yang hanya berisi sebuah method abstract.   Setiap turunan class Diskon mewakili sebuah cara perhitungan diskon.   Saat ini, setiap baris di faktur (class ItemFaktur) dapat diberikan diskon.   Tapi, bagaimana bila total dari faktur juga bisa memiliki diskon?   Saya cukup menambahkan asosiasi class Faktur ke class Diskon, seperti pada gambar berikut ini:

Reuse Operasi Perhitungan

Reuse Operasi Perhitungan

Karena class Faktur adalah superclass dari seluruh faktur yang ada, maka turunannya seperti FakturBeli dan FakturJual secara otomatis memiliki asosiasi dengan class Diskon.   Bila suatu saat nanti ada penambahan atau perubahan pada class Diskon dan turunannya,  maka baik Faktur maupun ItemFaktur dapat langsung mendapatkan imbasnya.

Hal seperti ini tidak dijumpai pada saat merancang tabel.   Tabel hanya mewakili penyimpanan data dan tidak menunjukkan seperti apa proses yang terjadi.   Developer mungkin akan meletakkan perhitungan diskon pada sebuah function yang di-include atau di-share bersama; developer mungkin juga merasa ini adalah hal sepele sehingga hanya copy-paste rumus ke masing-masing halaman yang membutuhkan; tidak ada patokan atau batasan mengenai ‘proses’ atau perilaku (behaviour) bila hanya melihat rancangan tabel.

4. Class dan Tabel Tidak Dapat Dibandingkan!

Perancangan class secara OOP adalah merancang logika program.   Pada OOP, sebuah program dapat dianggap sebagai kumpulan objek yang saling berinteraksi.   Mereka pada akhirnya memiliki nilai yang perlu disimpan ke dalam database, dan untuk itu dibutuhkan tabel bila memakai database relasional.   Object Relational Mapper (ORM) seperti JPA pada Java atau Doctrine pada PHP berusaha menjembatani perbedaan ini sehingga developer hanya berkonsentrasi pada class-class yang ada tanpa perlu terlalu mengkhawatirkan perancangan tabel.

An Example of Analysis Patterns: Observation

In this post, I will create an application that implements Martin Fowler’s observation analysis pattern as described in his book, Analysis Patterns: Reusable Object Models.   The book is a great practical book that reflects its author experience in domain modeling.   The only problem of this book is it uses its own custom diagram rather than UML diagram (by that time, UML hasn’t been standardized yet).   It will be great if there is a second edition of the book which uses UML class diagram or object diagram to explain domain models.   I will definitely read it 😉

What is analysis pattern?   The book describes analysis patterns as “groups of concepts that represent a common construction in business modeling”.   It is something that helps me to understand business world from software engineering perspective.   Why do I care about analysis pattern?  In small team,  software developers are not only creating source code;  sometimes,  they need to be (or work closely with) domain experts.

For example, I will need to implement this simple requirement:

“The program should records motorcycle’s measurements: mileage (the distance travelled in kilometers), oil level (over-full, ok, or low), etc. Program will also store the date of measurement. Not all measurements will be performed on the same day. User should be able to add new measurement types later (for example, he may want to measure motorcycle’s emission in the future).”

The requirement above can be solved by using observation pattern.   In observation analysis pattern, a quantitative value (such as mileage) is a ‘Measurement’ and a qualitative value (such as oil level) is a ‘Category Observation’.   Both of them have an association with a ‘Phenomenon Type’.

Here is the class diagram that I’m going to implement:

Domain Classes

Domain Classes

Below is a UML object diagram that depicts an example run time state of the class diagram:

Object diagrams

Object diagrams

I will use Griffon and simple-jpa to quickly create an implementation for the design above.

This is the content of Motorcycle.groovy:

package domain

import ...

@DomainModel @Entity @Canonical
class Motorcycle {

    @Size(min=6, max=10)
    String plateNumber

    @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true)
    List<Observation> observations = []

}

Observation class has a DateTime attribute to store the date and time when the observation is performed. If the method by which the observation was performed is required, then a protocol attribute can be added to this class. The following is the content of Observation.groovy:

package domain

import ...

@DomainModel @Entity @Canonical
abstract class Observation {

    @NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
    DateTime dateTime

}

The following is the content of Measurement.groovy:


package domain

import ...

@DomainModel @Entity @Canonical
class Measurement extends Observation {

    @NotNull
    Double quantity

    @NotNull @ManyToOne
    PhenomenonType type

    @Override
    String toString() {
        "${type.name}: ${NumberFormat.getNumberInstance().format(quantity)}"
    }
}

This is the content of CategoryObservation.groovy:

package domain

import ...

@DomainModel @Entity @Canonical
class CategoryObservation extends Observation {

    @NotNull @ManyToOne
    Phenomenon phenomenon

    @Override
    String toString() {
        "${phenomenon.type.name} - ${phenomenon.name}"
    }
}

This is the content of PhenomenonType.groovy:

package domain

import ...

@DomainModel @Entity @Canonical
class PhenomenonType {

    @NotEmpty @Size(min=2, max=50)
    String name

}

The following is the content of Phenomenon.groovy:

package domain

import ...

@DomainModel @Entity @Canonical
class Phenomenon {

    @NotEmpty @Size(min=2, max=50)
    String name

    @NotNull @ManyToOne
    PhenomenonType type

}

The next step is generating MVC based on those domain classes.   simple-jpa will create individual menu per domain class, but user will not need to access all of them.   There should be only menus for Motorcycle, Phenomenon, and PhenomenonType.   I will need to remove unneeded menus from MainGroupView.groovy; their corresponding MVCGroups can also be safely deleted.

Main Menu

Main Menu

Apart from template renderer configuration, “Phenomenon Type” screen is already working.   User can add, edit, or delete a PhenomenonType from this screen.

PhenomenonType Screen

PhenomenonType Screen

“Phenomenon” screen can be considered done too. User can add, edit, or delete a Phenomenon from this screen. Whenever they create a new Phenomenon, they should select one of the PhenomenonType objects.

Phenomenon Screen

Phenomenon Screen

Now, let see “Motorcycle” screen:

Motorcycle Screen

Motorcycle Screen

This screen consists of two MVCGroups: Motorcycle and ObservationAsChild.   Motorcycle is the one shown in the picture above.   ObservationAsChild will be displayed when the user click on the ‘Observations’ button.   Observation is an abstract class whose children are Measurement and CategoryObservation, so user will intuitively expect a form to input both of kinds in ObservationAsChild.   Unfortunately, simple-jpa is not smart enough to create MVCGroup that will allow entering both Measurement and CategoryObservation.   I will need to manually change the code for ObservationAsChild MVCGroup.

The following is the new content of ObservationAsChildView.groovy after modification:

package project

import ...

application(title: 'Observation',
        preferredSize: [520, 340],
        pack: true,
        locationByPlatform: true,
        iconImage: imageIcon('/griffon-icon-48x48.png').image,
        iconImages: [imageIcon('/griffon-icon-48x48.png').image,
                imageIcon('/griffon-icon-32x32.png').image,
                imageIcon('/griffon-icon-16x16.png').image]) {

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

        panel(constraints: PAGE_START) {
            flowLayout(alignment: FlowLayout.LEADING)
            label("Type: ")
            JComboBox cboTypeFilter = comboBox(model: model.typeFilter, templateRenderer: '${(value instanceof String)?value:value.name}')
            cboTypeFilter.addItemListener(model.observationTypeMatcherEditor)
        }

        panel(constraints: CENTER) {
            borderLayout()
            panel(constraints: PAGE_START, layout: new FlowLayout(FlowLayout.LEADING)) {
                label(text: bind('searchMessage', source: model))
            }
            scrollPane(constraints: CENTER) {
                table(rowSelectionAllowed: true, id: 'table') {
                    eventTableModel(list: model.observationFilterList,
                        columnNames: ['Date Time', 'Description'],
                        columnValues: ["\${value.dateTime.toString('dd-MM-yyyy hh:mm')}",'${value}'])
                    table.selectionModel = model.observationSelection
                }
            }
        }

        panel(id: "form", layout: new MigLayout('hidemode 2', '[right][left][left,grow]',''), constraints: PAGE_END, focusCycleRoot: true) {

            group = buttonGroup()
            label('Type:')
            radioButton(text: "Measurement", buttonGroup: group, constraints: 'split 2',
                selected: bind('measurementType', target: model, mutual: true))
            radioButton(text: "Category Observation", buttonGroup: group, constraints: 'skip, wrap',
                selected: bind('categoryObservationType', target: model, mutual: true))

            label('Date Time:')
	    dateTimePicker(id: 'dateTime', dateTime: bind('dateTime', target: model, mutual: true), errorPath: 'dateTime')
	    errorLabel(path: 'dateTime', constraints: 'wrap')

            label('Quantity:', visible: bind('measurementType', source: model))
            numberTextField(id: 'quantity', columns: 20, bindTo: 'quantity', errorPath: 'quantity',
                visible: bind('measurementType', source: model))
            errorLabel(path: 'quantity', constraints: 'wrap', visible: bind('measurementType', source: model))

            label('Type:', visible: bind('measurementType', source: model))
            comboBox(model: model.type, templateRenderer: '${value.name}', errorPath: 'type',
                visible: bind('measurementType', source: model))
            errorLabel(path: 'type', constraints: 'wrap', visible: bind('measurementType', source: model))

            label('Phenomenon:', visible: bind('categoryObservationType', source: model))
            comboBox(model: model.phenomenon, templateRenderer: '${value.type.name} - ${value.name}', errorPath: 'phenomenon',
                visible: bind('categoryObservationType', source: model))
            errorLabel(path: 'phenomenon', constraints: 'wrap',
                visible: bind('categoryObservationType', source: model))

            panel(constraints: 'span, growx, wrap') {
                flowLayout(alignment: FlowLayout.LEADING)
                button(app.getMessage("simplejpa.dialog.update.button"), actionPerformed: {
                    if (!model.observationSelection.selectionEmpty) {
                        if (JOptionPane.showConfirmDialog(mainPanel, app.getMessage("simplejpa.dialog.update.message"),
                            app.getMessage("simplejpa.dialog.update.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION) {
                                return
                        }
                    }
                    controller.save()
                    form.getFocusTraversalPolicy().getFirstComponent(form).requestFocusInWindow()
                })
                button(app.getMessage("simplejpa.dialog.cancel.button"), visible: bind (source: model.observationSelection,
                    sourceEvent: 'valueChanged', sourceValue: {!model.observationSelection.selectionEmpty}),actionPerformed: model.clear)
                button(app.getMessage("simplejpa.dialog.delete.button"), visible: bind (source: model.observationSelection,
                    sourceEvent: 'valueChanged', sourceValue: {!model.observationSelection.selectionEmpty}), actionPerformed: {
                        if (JOptionPane.showConfirmDialog(mainPanel, app.getMessage("simplejpa.dialog.delete.message"),
                            app.getMessage("simplejpa.dialog.delete.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION) {
                                controller.delete()
                        }
                })
                button(app.getMessage("simplejpa.dialog.close.button"), actionPerformed: {
                    SwingUtilities.getWindowAncestor(mainPanel)?.dispose()
                })
            }
        }
    }
}

The following is the new content of ObservationAsChildController.groovy after modification:

package project

import ...

@SimpleJpaTransaction
class ObservationAsChildController {

    def model
    def view

    void mvcGroupInit(Map args) {
        args.'parentList'.each { model.observationList << it }
        listAll()
    }

    void mvcGroupDestroy() {
        destroyEntityManager()
    }

    def listAll = {
        execInsideUIAsync {
            model.typeList.clear()
            model.typeFilterList.clear()
            model.phenomenonList.clear()
        }

        List typeResult = findAllPhenomenonType()
        List phenomenonResult = findAllPhenomenon()

        execInsideUIAsync {
            model.phenomenonList.addAll(phenomenonResult)
            model.typeList.addAll(typeResult)
            model.typeFilterList.add("All")
            model.typeFilterList.addAll(typeResult)
            model.typeFilter.setSelectedItem("All")
        }
    }

    def save = {
        Observation observation
        if (model.measurementType) {
            observation = new Measurement(dateTime: model.dateTime, quantity: model.quantity, type: model.type.selectedItem)
        } else if (model.categoryObservationType) {
            observation = new CategoryObservation(dateTime: model.dateTime, phenomenon: model.phenomenon.selectedItem)
        } else {
            throw new Exception("Unknown observation!")
        }
        if (!validate(observation)) return_failed()

        if (model.observationSelection.selectionEmpty) {
            // Insert operation
            execInsideUIAsync { model.observationList << observation }
        } else {
            // Update operation
            execInsideUISync { model.observationList.set(model.observationSelection.minSelectionIndex, observation) }
        }
        execInsideUIAsync { model.clear() }
    }

    def delete = {
        Observation observation = model.observationSelection.selected[0]
        execInsideUIAsync { model.observationList.remove(observation) }
    }

} 

The following is the new content of ObservationAsChildModel.groovy after modification:

package project

import ...

class ObservationAsChildModel {

    def view

    @Bindable Long id
    @Bindable DateTime dateTime
    @Bindable boolean measurementType
    @Bindable Double quantity
    @Bindable boolean categoryObservationType

    TypeMatcher observationTypeMatcher = new TypeMatcher()
    MatcherEditor observationTypeMatcherEditor = new TypeMatcherEditor()

    BasicEventList<Observation> observationList = new BasicEventList<>()
    FilterList<Observation> observationFilterList = new FilterList<>(observationList, observationTypeMatcherEditor)
    DefaultEventSelectionModel<Observation> observationSelection =
        GlazedListsSwing.eventSelectionModelWithThreadProxyList(observationFilterList)

    BasicEventList<PhenomenonType> typeList = new BasicEventList<>()
    @Bindable DefaultEventComboBoxModel<PhenomenonType> type =
        GlazedListsSwing.eventComboBoxModelWithThreadProxyList(typeList)

    BasicEventList<PhenomenonType> typeFilterList = new BasicEventList<>()
    @Bindable DefaultEventComboBoxModel<PhenomenonType> typeFilter =
        GlazedListsSwing.eventComboBoxModelWithThreadProxyList(typeFilterList)


    BasicEventList<Phenomenon> phenomenonList = new BasicEventList<>()
    @Bindable DefaultEventComboBoxModel<Phenomenon> phenomenon =
        GlazedListsSwing.eventComboBoxModelWithThreadProxyList(phenomenonList)

    public ObservationAsChildModel() {

        observationSelection.valueChanged = { ListSelectionEvent event ->
            if (observationSelection.isSelectionEmpty()) {
                clear()
            } else {

                Observation selected = observationSelection.selected[0]
                errors.clear()
                id = selected.id
		dateTime = selected.dateTime

                if (selected instanceof Measurement) {
                    measurementType = true
                    quantity = selected.quantity
                    type.selectedItem = selected.type
                } else if (selected instanceof CategoryObservation) {
                    categoryObservationType = true
                    phenomenon.selectedItem = selected.phenomenon
                }

            }
        }
    }

    def clear = {
        id = null
        dateTime = null

        errors.clear()
        quantity = null
        type.selectedItem = null
        phenomenon.selectedItem = null
        observationSelection.clearSelection()
        measurementType = false
        categoryObservationType = false
        view.group.clearSelection()
    }

    class TypeMatcher implements Matcher {

        @Override
        boolean matches(Object o) {
            if (typeFilter.selectedItem == "All") return true
            if (!o) return false
            if (observationList.isEmpty()) return true
            if (!typeFilter.selectedItem) return true

            if (o instanceof Measurement) {
                return ((Measurement) o).type == typeFilter.selectedItem
            } else if (o instanceof CategoryObservation) {
                return ((CategoryObservation) o).phenomenon.type == typeFilter.selectedItem
            }
        }

    }

    class TypeMatcherEditor extends AbstractMatcherEditor implements ItemListener {

        @Override
        void itemStateChanged(ItemEvent e) {
            if (e.stateChange==ItemEvent.SELECTED) fireChanged(observationTypeMatcher)
        }

    }

}

ObservationAsChildView now has JRadioButton to indicate what kind of observation that will be saved.  If user clicks on “Measurement” JRadioButton, he will be able to add a quantity and select a PhenomenonType.   If user clicks on “Category Observation” JRadioButton, he will be able to select a Phenomenon (qualitative value).

Add Observations With JRadioButton

Add Observations With JRadioButton

Clicking a JRadioButton will display correspondings components

Clicking a JRadioButton will display correspondings components

In a real world application, observations will often be performed periodically, so there will be a lot of observations for a Motorcycle.   On some occasions, user may want to quickly display certain PhenomenonType for a Motorcycle.   That’s the reason why I use FilterList (from GlazedLists) for JTable in ObservationAsChildView.   By using FilterList, I can filter table’s contents based on a Matcher.

PhenomenonType filter

PhenomenonType filter

Selecting "Mileage" will only display mileage's observation

Selecting “Mileage” will only display mileage’s observation

Selecting "Oil Level" will only display oil level's observations

Selecting “Oil Level” will only display oil level’s observations

Phenomenon and PhenomenonType are knowledge level objects which are used by the others (operational level objects).   This design will allow user to easily add new measurement types later.   For example, I will add a new PhenomenonType called emission as shown in the picture below:

Add new PhenomenonType

Add new PhenomenonType

This implementation of observation analysis pattern allows me to add the new emission measurement to all motorcycles without changing any source code:

New PhenomenonType is ready to use

New PhenomenonType is ready to use

Merancang Sistem Dengan UML: Mulai Dari Mana?

Salah satu pertanyaan yang paling sering saya temukan dari para mahasiswa adalah apa saja step-step yang harus ditempuh dalam merancang sistem berbasis UML? Saya biasanya akan memperbaiki pertanyaan ini karena UML pada dasarnya hanya kumpulan diagram atau teknik visualisasi; UML tidak mendikte langkah-langkah pengembangan sistem! Lalu apa yang harus dilakukan dalam merancang sebuah sistem?

Saya yakin para mahasiswa telah memperoleh mata kuliah pengembangan sistem informasi sebelumnya, tapi kenapa mereka bertanya demikian disaat mereka harus merancang sistem yang nyata (misalnya untuk skripsi)? Salah satu penyebabnya adalah analisa & perancangan terlalu teoritis sehingga tidak mendukung implementasi/pembuatan kode program.

Dari beberapa buku analisa & perancangan yang saya baca, buku Use Case Driven Object Modeling with UML: Theory and Practice adalah salah satu buku yang spesial dan sangat membantu. Bukan hanya dilengkapi dengan humor, tetapi buku ini memberikan metode analisa & perancangan yang sangant berguna saat diterapkan dalam pembuatan kode program!   Buku yang memakai metode ICONIX Process ini ditulis oleh analyst yang memiliki latar belakang programmer.

Saya tahu bahwa sangat sulit merangkum sebuah buku 438 halaman dalam sebuah artikel blog, tapi saya ingin menunjukkan bahwa diagram UML hasil analisa & perancangan bukanlah sekedar basa basi yang dihasilkan analyst.   Dengan metode yang salah, analyst kerap terlihat tidak berguna di mata developer.   Metode yang salah juga menyebabkan mahasiswa lebih senang membuat kode program terlebih dahulu, baru melakukan reverse engineering untuk menghasilkan diagram UML.   Dengan kata lain, sistem dibuat tanpa analisas & perancangan, sementara diagram UML hanya produk sampingan yang menambah ketebalan skripsi tanpa fungsi yang sangat berarti.

Gambar berikut ini memperlihat proses analisa & perancangan sistem informasi dengan ICONIX process:

ICONIX Process

ICONIX Process

Proses analisa & perancangan sistem yang saya rangkum dari buku Use Case Driven Object Modeling With UML: Theory and Practice adalah sebagai berikut ini:

1. Membuat Functional Requirement

Gunakan Microsoft Word untuk menuliskan functional requirement (apa yang dapat dilakukan oleh sistem?).   Tahap ini melibatkan business analyst, pelanggan, end user, dan project stakeholders lainnya.  Functional requirement bersifat tidak terstruktur dan tidak dapat dipakai dalam perancangan secara langsung.

Berikut ini adalah contoh potongan dari function requirement untuk sebuah sistem bengkel motor yang sederhana:

Sistem harus dapat memproses work order untuk motor mulai dari antri, sedang dikerjakan 
oleh seorang mekanik, selesai di-servis, dan proses pembayaran. 
Sebuah work order memiliki informasi jenis pekerjaan dan sparepart yang dijual. 
Harga ongkos servis berdasarkan jenis pekerjaan dan tipe motor.

2. Membuat Domain Model (sederhana)

Salah satu fungsi domain model adalah menyamakan istilah yang akan pakai diproses selanjutnya.   Misalnya, apakah saya akan memakai istilah ‘work order’ atau ‘pekerjaan servis’?   Apa saya akan memakai sebutan ‘sparepart‘ atau ‘suku cadang‘?   Walau terlihat sepele, perbedaan istilah seringkali menimbulkan salah paham dalam komunikasi tim.

Pada tahap ini, domain model adalah class diagram yang hanya memakai relasi pewarisan (is-a/adalah sebuah) dan agregasi (has-a/memiliki sebuah).   Class diagram ini belum memiliki atribut dan operasi.   Nantinya, di proses selanjutnya, domain model akan diperbaiki dan dikembangkan menjadi lebih detail.

Gambar berikut ini memperlihatkan contoh domain model untuk functional requirement di langkah 1:

Domain Model Versi Awal

Domain Model Versi Awal

3. Membuat Use Case

Use case mendefinisikan behavioral requirements berdasarkan functional requirement (dan sumber lainnya).   Berbeda dengan anjuran dari buku analisis sisfo lain,  buku ini menyarankan untuk membuat use case dengan maksimal 2 paragraf!   Tidak perlu mengikuti template yang detail!   Sebuah use case yang panjang & detail malah akan memperlambat kita.   Tim yang membuat use case bisa jadi akhirnya hanya mengisi form yang kosong tanpa banyak berpikir panjang (misalnya sekedar copy paste) sehingga proses membuat use case hanya sekedar ritual tanpa analisa mendalam.

Kalimat yang dipakai dalam use case harus berupa kalimat aktif, misalnya “pengguna men-klik tombol Login”. Kalimat pasif seperti “Sistem menyediakan tombol Login” adalah ciri-ciri functional requirement dan bukan bagian dari use case.

Use case harus mengandung nama di domain model.   Dengan demikian, saya bisa menghubungkan class-class yang akan dirancang dengan use case.   Setiap halaman/layar yang direferensikan di dalam use case sebaiknya diberi nama yang konsisten, misalnya Halaman Tambah Sparepart atau Screen TambahSparepart.

Sebuah use case hampir mirip seperti dokumentasi sistem. Kita perlu menspesifikasikan seperti apa cara pakai sistem (termasuk respon sistem) sebelum sebuah sistem dibuat. Berikut ini adalah contoh use case diagram berdasarkan functional requirement di langkah 1 dan memakai domain model dari langkah 2:

Use Case Diagram

Use Case Diagram

Sebuah use case selain memiliki sunny-day scenario, sebaiknya juga memiliki rainy-day scenario (apa yang akan terjadi bila sesuatu salah?) atau alternatif. Sebagai contoh, berikut ini contoh use case scenario untuk use case diagram di atas:

Membuat WorkOrder baru
Basic Scenario
 Front Desk memilih Motor di Screen ListMotor dan men-klik tombol untuk membuat WorkOrder. 
 Sistem akan membuat sebuah WorkOrder baru dengan status sedang antri.
Alternate Scenario
 Motor belum terdaftar - Front Desk terlebih dahulu menambah Motor baru dengan 
    men-klik tombol untuk menambah Motor baru sebelum mengerjakan langkah 
    yang ada di Basic Scenario.
 Motor sudah memiliki WorkOrder yang sedang antri - Sistem akan menampilkan pesan kesalahan.
[Catatan: pada saat membuat use case ini, terlihat bahwa dibutuhkan use case menambah Motor. 
Agar ringkas, use case tersebut akan diabaikan.]

Mengubah Status WorkOrder menjadi sedang dikerjakan oleh Mekanik
Basic Scenario
 Front Desk memilih sebuah WorkOrder di Screen ListWorkOrder dan memilih menu untuk 
 menandakan bahwa Workorder tersebut sedang dikerjakan. 
 Sistem akan menampilkan Screen PengerjaanWorkOrder. Front Desk memilih nama Mekanik 
 yang mengerjakan WorkOrder dan men-klik tombol untuk menyimpan perubahan. 
 Sistem akan mengubah status WorkOrder menjadi sedang dikerjakan 
 serta mencatat tanggal & jam mulai dikerjakan.
Alternate Scenario
 Status WorkOrder yang dipilih bukan sedang antri - Sistem akan menampilkan pesan kesalahan.

Menambah Sparepart yang dipakai selama pengerjaan WorkOrder
Basic Scenario
 Front Desk memilih sebuah WorkOrder di Screen ListWorkOrder dan memilih menu untuk 
 menambah Sparepart di WorkOrder. Sistem akan menampilkan Screen TambahSparepart yang 
 berisi daftar Sparepart untuk WorkOrder yang dipilih. Disini Front Desk akan mengisi 
 data ItemSparepart dengan memilih Sparepart, memasukkan jumlah Sparepart, lalu men-klik 
 tombol Tambah ItemSparepart. Sistem akan memperbaharui daftar ItemSparepart di layar.
 Front Desk men-klik tombol Simpan untuk selesai. Sistem akan menyimpan perubahan Sparepart 
 pada WorkOrder terpilih.
Alternate Scenario
 Terdapat lebih dari satu jenis Sparepart yang perlu ditambahkan - Front Desk kembali 
   menambah data ItemSparepart. Setelah semua ItemSparepart selesai dimasukkan, Front Desk 
   men-klik tombol Simpan di Screen TambahSparepart. Sistem akan menyimpan perubahan.
 Status WorkOrder yang dipilih bukan sedang dikerjakan - Sistem akan menampilkan pesan 
   kesalahan.
[Catatan: pada saat membuat use case ini, terlihat bahwa ada yang kurang pada domain model, 
  yaitu ItemSparepart. Segera update domain model! Terlihat juga bahwa dibutuhkan sebuah 
  metode untuk menghapus Sparepart dan meng-edit jumlah Sparepart terpakai. 
  Agar ringkas, use case tersebut akan diabaikan.]

Mengubah status WorkOrder menjadi selesai dikerjakan
Basic Scenario
 Front Desk memilih sebuah WorkOrder di Screen ListWorkOrder, lalu memilih menu untuk mengubah 
 status WorkOrder menjadi selesai dikerjakan. Sistem akan menampilkan dialog konfirmasi. 
 Bila kasir menkonfirmasi, Sistem akan mengubah status WorkOrder tersebut menjadi selesai 
 dikerjakan dan mencatat jam selesai dikerjakan. Front Desk kemudian mengerjakan 
 use case "Mencetak rincian WorkOrder termasuk biaya".
Alternate Scenario
 Status WorkOrder bukan sedang dikerjakan - Sistem akan menampilkan pesan kesalahan.

Mencetak rincian WorkOrder termasuk biaya
Basic Scenario
 Front Desk memilih tombol untuk mencetak. Sistem kemudian mencetak detail WorkOrder 
 ke printer. 
 Detail WorkOrder yang dicetak meliputi tanggal, plat nomor motor, jam mulai dikerjakan, 
 jam selesai dikerjakan, nama mekanik yang mengerjakan, rincian seluruh Sparepart yang dipakai 
 (jumlah & harga eceran tertinggi Sparepart), ongkos servis dan total yang harus dibayar.
Alternate Scenario
 Status WorkOrder bukan selesai dikerjakan - Sistem akan menampilkan pesan kesalahan.
[Catatan: use case ini dipisahkan dari use case "Mengubah status WorkOrder menjadi 
 selesai dikerjakan" karena dianggap nanti akan ada use case lain yang dapat 
 mencetak rincian WorkOrder tetapi tidak ditampilkan disini.]

4. Requirements Review

Pada saat melakukan analisa dalam membuat use case, saya menemukan hal yang masih kurang.   Misalnya, saya perlu menambahkan class ItemSparepart pada domain model.   Selain itu, pada beberapa situasi, saya bahkan bisa menemukan ada use case yang masih kurang, misalnya use case “Tambah Motor baru“.

Pada langkah ini, saya kembali memastikan bahwa use case & domain model telah dibuat dengan baik.   Pelanggan juga perlu dilibatkan untuk memastikan bahwa use case (behavioral requirement) & functional requirement sesuai dengan yang diharapkan.   Ingatlah selalu bahwa bagian terpenting dari sebuah sistem bukanlah seberapa keren design pattern yang diterapkan di class diagram, tetapi sejauh mana sistem tersebut memberikan profit bagi penggunanya (memenuhi requirements).

5. Melakukan Robustness Analysis

Analisis adalah memikirkan “apa” (what),  sementara perancangan adalah memikirkan “bagaimana” (how). Salah satu alasan mahasiswa sering mengabaikan UML dan langsung terjun ke coding adalah celah yang cukup jauh antara analisis dan perancangan sehingga mereka memilih merancang secara eksperimental dengan langsung coding.   Umumnya mereka berakhir dalam jebakan siklus perubahan “coding” terus menerus (guna memperbaiki rancangan).   Padahal, perubahan “coding” adalah sesuatu yang sangat memakan waktu dan upaya bila dibandingkan dengan mengubah diagram UML.

Robustness analysis dipakai untuk menjembatani analisis dan perancangan.   Robustness analysis harus diterapkan pada setiap use case yang ada.  Pada Enterprise Architect, robustness analysis dapat digambarkan dengan menggunakan Analysis Diagrams (terdapat di kategori Extended).

Berikut ini adalah contoh hasil robustness analysis untuk use case yang ada:

Analysis Diagram Untuk Use Case Membuat WorkOrder Baru

Analysis Diagram Untuk Use Case Membuat WorkOrder Baru

Analysis Diagram Untuk Use Case Mengubah Status WorkOrder Menjadi Sedang DIkerjakan Oleh Mekanik

Analysis Diagram Untuk Use Case Mengubah Status WorkOrder Menjadi Sedang DIkerjakan Oleh Mekanik

Menambah Sparepart yang dipakai selama pengerjaan WorkOrder

Menambah Sparepart yang dipakai selama pengerjaan WorkOrder

Analysis Diagram Untuk Use Case Mengubah Status WorkOrder Menjadi Selesai Dikerjakan

Analysis Diagram Untuk Use Case Mengubah Status WorkOrder Menjadi Selesai Dikerjakan

Analysis Diagram Untuk Use Case Mencetak Rincian WorkOrder Termasuk Biaya

Analysis Diagram Untuk Use Case Mencetak Rincian WorkOrder Termasuk Biaya

Semakin detail robustness analysis, maka semakin banyak hal yang kurang dari use case dan domain model yang akan ditemukan.   Yup! Pada awalnya saya ragu kenapa saya harus menambah control “mengisi jumlah”, “mengisi nama”, dsb untuk setiap field yang ada.   Tapi saya cukup terkejut saat menemukan dari hal sepele tersebut, saya menemukan beberapa alternate scenario yang kurang.

Sebagai contoh, saya menemukan bahwa saya lupa menambahkan alternate scenario “jumlah Sparepart tidak mencukupi” saat melakukan analisa robustness pada use case “Menambah Sparepart yang dipakai selama pengerjaan WorkOrder”.   Semakin cepat saya menyadari ada yang kurang, semakin baik! Idealnya adalah sebelum coding dilakukan. Robustness analysis adalah salah satu senjata yang ampuh untuk itu.

Saya juga menemukan bahwa pada use case “Mengubah Status WorkOrder menjadi sedang dikerjakan oleh Mekanik”, saya lupa menuliskan bahwa Front Desk officer juga perlu memilih JenisPekerjaan.   Saya perlu segera mengubah teks use case tersebut.

Selain itu, terkadang saya juga dapat menemukan ada class yang kurang pada domain model.   Misalnya, dari hasil robutsness analysis, terlihat bahwa saya perlu menambahkan class Mekanik di domain model.

Pada tahap ini, saya juga perlu mengisi domain model dengan atribut, seperti yang terlihat pada gambar berikut ini:

Domain Model Yang Telah Memiliki Atribut

Domain Model Yang Telah Memiliki Atribut

6. Preliminary Design Review

Kembali lagi seluruh tim melakukan review dan memastikan bahwa semua yang telah dibuat sesuai dengan requirement.   Ini adalah langkah terakhir dimana pelanggan (stackholder) terlibat!   Hal ini karena langkah berikutnya melibatkan proses techincal.  Akan berbahaya bila membiarkan pelanggan yang non-technical atau semi-technical mengambil keputusan untuk hal-hal yang bersifat teknis (misalnya framework atau database yang dipakai).   Walaupun demikian, pelanggan boleh memberikan komentar mengenai tampilan.

Setelah langkah ini, tidak ada lagi perubahan requirement.  Lalu bagaimana bila pelanggan ingin menambah requirement? Buat sebuah rilis atau milestone baru dengan kembali lagi ke langkah pertama di atas.

7. Menentukan Technical Architecture

Tentukan framework apa yang akan dipakai.   Sebagai contoh, saya akan membuat sebuah aplikasi desktop dengan Griffon.  Pola arsitektur yang dipakai menyerupai Model View ViewModel (MVVM) dimana terdapat perbedaan antara domain model dan view model.   Saya juga mengasumsikan penggunaan sebuah plugin fiktif yang dirujuk sebagai simple-jpa.   Gambar berikut ini memperlihatkan contoh arsitektur yang dipakai:

Gambaran Technical Architecture

Gambaran Technical Architecture

8. Membuat Sequence Diagram

Object oriented pada dasarnya adalah menggabungkan antara data dan operasi ke dalam sebuah entitas.   Saat ini, domain model baru berisi data.  Oleh sebab itu, dibutuhkan sebuah upaya untuk menemukan operasi untuk domain model.   Caranya adalah dengan memakai sequence diagram.

Saat membuat sequence diagram, sertakan juga elemen dalam arsitektur teknis/framework.   Misalnya penggunaan MVC akan menyebabkan ada class baru seperti controller.  Yup! Penggunaan MVC akan membuat operasi tersebar ke controller.   Hal ini sering dikritik karena bukan  pendekatan OOP melainkan kembali ke zaman prosedural.  Baca buku Object Design: Roles, Responsibilities, and Collaborations untuk pendekatan yang OOP, akan tetapi jangan lupa kalau kita dibatasi oleh framework yang dipakai (ehem, seharusnya bukan framework yang membatasi kita, melainkan kita yang tegas dalam memilih framework).

Selama membuat sequence diagram, ingatlah selalu bahwa tujuannya adalah menemukan operasi (behavior) untuk setiap class yang ada, bukan menunjukkan step-by-step operasi secara detail!   Untuk menjaga agar tidak tersesat menjadi membuat flow-chart yang detail, jangan memikirkan focus of control (matikan saja fitur tersebut!).

Sequence diagram dibuat untuk setiap use case yang ada, berdasarkan hasil robustness anaylsis. Gambar berikut ini memperlihat contoh sequence diagram yang dihasilkan (agar sederhana, operasi penyimpanan data oleh simple-jpa tidak ditampilkan):

Sequence Diagram Untuk Membuat WorkOrder Baru

Sequence Diagram Untuk Membuat WorkOrder Baru

Sequence Diagram Untuk Mengubah Status WorkOrder Menjadi Sedang Dikerjakan Oleh Mekanik

Sequence Diagram Untuk Mengubah Status WorkOrder Menjadi Sedang Dikerjakan Oleh Mekanik

Sequence Diagram Untuk Menambah Sparepart Yang Dipakai Selama Pengerjaan WorkOrder

Sequence Diagram Untuk Menambah Sparepart Yang Dipakai Selama Pengerjaan WorkOrder

Sequence Diagram Untuk Mengubah Status WorkOrderMenjadi Selesai Dikerjakan

Sequence Diagram Untuk Mengubah Status WorkOrderMenjadi Selesai Dikerjakan

Sequence Diagram Untuk Mencetak Rincian WorkOrder Termasuk Biaya

Sequence Diagram Untuk Mencetak Rincian WorkOrder Termasuk Biaya

Proses analisa yang berulang kali lagi-lagi membantu saya menemukan kekurangan.   Saya selama ini ternyata lupa bahwa pada use case “Menambah Sparepart yang dipakai selaman pengerjaan WorkOrder”, sistem harus mengurangi jumlah stok Sparepart bila pengguna men-klik tombol simpan.   Oleh sebab itu, saya segera memperbaharui teks use case.

Selain itu, saya juga menemukan sebuah kesalahan yang saya buat dari awal dan tidak terdeteksi hingga sekarang, yang berhubungan dengan class OngkosServis.   Gambar berikut ini memperlihatkan perancangan awal class tersebut:

Kesalahan Rancangan Domain Model Akibat Berfokus Pada Penyimpanan Data

Kesalahan Rancangan Domain Model Akibat Berfokus Pada Penyimpanan Data

Bila membuat ERD atau design tabel, ini adalah sesuatu yang dapat diterima (ongkos disimpan dalam tabel yang mewakili hubungan one-to-many dari JenisPekerjaan ke TipeMotor). Tetapi, bila diterapkan ke dalam domain model, maka akan terjadi kejanggalan saat saya memakai domain model tersebut di sequence diagram. Untuk memperoleh ongkos servis, apa saya harus membuat instance objek OngkosServis baru? Bila diterapkan ke coding, maka ini berarti untuk memperoleh ongkos servis, saya harus selalu melakukan query JPQL  yang kira-kira terlihat seperti berikut ini:

TipeMotor tipeMotor = model.selectedTipeMotor
JenisPekerjaan jenisPekerjaan = model.selectedJenisPekerjaan
Query query = 
  em.createQuery("SELECT o.harga FROM OngkosServis o WHERE " + 
      "o.tipeMotor = :tipeMotor AND o.jenisPekerjaan = :jenisPekerjaan") 
query.setParameter("tipeMotor", tipeMotor)
query.setParameter("jenisPekerjaan", jenisPekerjaan)
Long ongkos = query.getSingleResult()

Terlihat ada yang tidak beres! Bukankah OOP harus intuitive & bisa dipakai dengan mudah? Kenapa tiba2 harus melakukan query secara eksplisit untuk memperoleh sebuah ongkos servis? Selain itu, class OngkosServis tidak pernah dipakai secara langsung di kode program, hanya dipakai di query saja!

Oleh sebab itu, saya memperbaikinya dengan membuang class OngkosServis, dan menambahkan atribut ongkosServis di class JenisPekerjaan dengan tipe data berupa Map<TipeMotor,Long>. Dengan demikian, saya bisa memakainya seperti berikut ini:

TipeMotor tipeMotor = model.selectedTipeMotor
JenisPekerjaan jenisPekerjaan = model.selectedJenisPekerjaan
Long ongkos = jenisPekerjaan.getOngkosServis(tipeMotor)

Cara di-atas jauh lebih intuitive dan mudah dipahami. Query database tetap terjadi, tetapi kali ini secara implisit (secara otomatis) oleh Hibernate tanpa campur tangan developer.

Ini adalah alasan kenapa saya selalu memberikan pesan pada mahasiswa agar tidak memikirkan proses penyimpanan data saat membuat domain model.   Jangan menyamakan domain model dan ERD.   Pada saat merancang domain model, pikirkan bagaimana class-class yang ada akan saling berinteraksi dan dipakai oleh developer.   Bahkan bila tidak memakai ORM seperti Hibernate, tetap jangan memikirkan bagaimana penyimpanan data di domain model, melainkan buat ERD terpisah untuk dipakai oleh persistence layer (DAO).

9. Critical Design Review

Kembali melakukan review untuk memastikan bahwa tidak ada yang kurang pada sequence diagram. Pastikan bahwa setiap class yang ada telah memiliki atribut dan operasi yang didefinisikan secara lengkap (memiliki nama, tipe data, parameter, dsb).

Berikut ini adalah contoh hasil domain model yang telah dilengkapi dengan operasi dan multiplicity:

Domain Model Yang Telah Lengkap

Domain Model Yang Telah Lengkap

Getter dan setter tidak perlu ditampilkan karena hanya akan membuat class diagram terlihat ‘penuh’.

Versi bidirectional-nya akan terlihat seperti pada gambar berikut ini:

Domain Model Yang Telah Lengkap (Versi Bidirectional)

Domain Model Yang Telah Lengkap (Versi Bidirectional)

Selain domain model, saya juga menemukan terdapat class-class lain yang dihasilkan yang berkaitan dengan penggunaan framework, yang terlihat seperti pada gambar berikut ini:

Class pembantu untuk framework

Class pembantu untuk framework

10. Coding

Disini developer berperan mengubah rancangan (design) menjadi kode program.   Karena semua telah direncanakan dan dipikirkan sebelumnya, maka proses coding dapat dianggap sebagai sebuah pembuktian (test) bahwa rancangan yang dibuat sudah benar. Terkadang terdapat beberapa hal yang lolos dari perancangan dan baru terungkap saat coding; pada kasus tersebut, perubahan pada rancangan harus segera dilakukan sehingga kode program dan rancangan bisa tetap sinkron.

Bila pembuatan kode program tiba-tiba menjadi tidak terkendali (pada kasus skripsi, mahasiswa tiba-tiba merasa seolah otaknya hendak meledak dan jadi malas coding), maka ada beberapa kemungkinan:

  • Hasil rancangan tidak bagus.
  • Programmer tidak mengikuti hasil rancangan yang bagus dan mengerjakannya sesuka hatinya.
  • Programmer tidak diikutsertakan dalam proses perancangan
  • Mahasiswa tidak mau pikir panjang/hanya copy paste di bab analisa & perancangan, dalam pikirannya mau segera fokus ke bab implementasi dan kode program;
  • Mahasiswa hanya memikirkan bab analisa & perancangan, tidak mau peduli dampaknya pada saat membuat kode program nanti.