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.

Belajar Memakai Inheritance Di JavaScript

Pada artikel Belajar Membuat Object Di JavaScript, saya membuat beberapa prototype dan object. Salah satunya adalah prototype untuk Faktur. Bagaimana bila seandainya saya ingin membuat sebuah turunannya dari Faktur seperti FakturJual? JavaScript tidak memiliki syntax khusus untuk inheritance, tapi hal ini dapat diakalin dengan menggunakan prototype. Sebagai contoh saya dapat membuat FakturJual yang baru dengan isi seperti berikut ini:

/** class FakturJual turunan dari Faktur **/
function FakturJual(nomor, tanggal, diskon, konsumen) {
    this.nomor = nomor;
    this.tanggal = tanggal;
    this.diskon = diskon;
    this.konsumen = konsumen;
}
FakturJual.prototype = new Faktur();

/** Contoh Penggunaan **/
var produkA = new Produk('Produk A', 120000, 10);
var produkB = new Produk('Produk B', 85000, 5);
var fakturJual = new FakturJual('FA-001', Date.now(), 10, 'Konsumen A');
console.log(fakturJual instanceof Faktur); // output: true
console.log(fakturJual instanceof FakturJual); // output: true

Karena saya memberi nilai prototype untuk FakturJual berupa sebuah instance dari Faktur, maka seluruh yang dimiliki oleh Faktur termasuk total() dapat dipanggil oleh FakturJual. Ini disebut sebagai prototype chaining. Dengan demikian, saya dapat membuat kode program seperti berikut ini:

var produkA = new Produk('Produk A', 120000, 10);
var produkB = new Produk('Produk B', 85000, 5);
var fakturJual = new FakturJual('FA-001', Date.now(), 10, 'Konsumen A');
fakturJual.tambah(produkA, 5, 130000);
fakturJual.tambah(produkB, 10, 12000);
console.log(fakturJual.total()); // output: 693000

Penggunaan prototype chaining memiliki efek samping pada nilai referensi seperti pada array. Sebagai contoh, bila saya membuat object FakturJual kedua, saya akan memperoleh kesalahan pada perhitungan total, seperti yang terlihat pada kode program berikut ini:

/** Contoh Penggunaan **/
var produkA = new Produk('Produk A', 120000, 10);
var produkB = new Produk('Produk B', 85000, 5);

var fakturJual1 = new FakturJual('FA-001', Date.now(), 10, 'Konsumen A');
fakturJual1.tambah(produkA, 5, 130000);
fakturJual1.tambah(produkB, 10, 12000);
console.log(fakturJual1.total()); // output: 693000

var fakturJual2 = new FakturJual('FA-002', Date.now(), 0, 'Konsumen B');
fakturJual2.tambah(produkA, 2, 125000);
console.log(fakturJual2.total()); // output: 1020000

Terlihat bahwa object fakturJual2 memiliki nilai items yang sama seperti yang dimiliki oleh fakturJual1. Bila saya memanggil fakturJual2.tambah(), maka item baru tersebut akan ditambahkan pada array yang di-share bersama tersebut. Hal ini bukan sesuatu yang saya harapkan karena saya ingin fakturJual2 memiliki items-nya sendiri.

Untuk mengatasi hal tersebut, saya dapat menggunakan constructor stealing dengan mengubah kode program seperti berikut ini:

function FakturJual(nomor, tanggal, diskon, konsumen) {
    Faktur.call(this, nomor, tanggal, diskon);    
    this.konsumen = konsumen;
}
FakturJual.prototype = new Faktur();

Setiap function di JavaScript adalah sebuah object dan memiliki method seperti call() untuk mengerjakannya dengan melewatkan this yang berbeda. Pada kode program di atas, saya melewatkan this yang mewakili object FakturJual baru ke function Faktur(). Dengan demikian seluruh properties milik Faktur akan dimiliki oleh Faktur. Saya juga tetap memakai prototype dari Faktur untuk FakturJual sehingga saya tetap hanya perlu men-override method sekali saja bila diperlukan. Dengan demikian, masing-masing instance dari FakturJual memiliki property-nya masing-masing, tetapi mereka tetap memakai method yang sama.

Kesimpulan: Karena JavaScript tidak mendukung inheritance melalui syntax-nya, maka penerapan inheritance dilakukan melalui pola kode program (pattern) seperti prototype chaining atau constructor stealing. Ini membuatnya lebih rentan terhadap kesalahan terutama bila sang programmer tidak waspada dalam menerapkan pola yang ada.

Belajar Membuat Object Di JavaScript

Apakah JavaScript mendukung OOP? Iya dan tidak! JavaScript tidak memiliki class seperti di Java dan C# sehingga pemograman OOP di JavaScript lebih condong ke arah prototype-based programming. Saya tidak akan berharap banyak membuat puluhan UML class diagram yang mewakili domain inventory, pembelian, penjualan, dan laba rugi, lalu meng-implementasi-kannya pada JavaScript. OOP pada JavaScript tidak untuk keperluan seperti itu sehingga banyak yang menyebutkan bahwa JavaScript tidak mendukung OOP. Bagi JavaScript, sebuah object adalah kumpulan nilai layaknya sebuah array (jangan lupa bahwa function di JavaScript adalah sebuah nilai).

Berikut ini adalah contoh definisi object di JavaScript:

var produkA = {
    nama: 'Produk A',
    harga: 120000,
    jumlah: 10,
    tersediaUntuk: function(dibutuhkan) {
        return this.jumlah >= dibutuhkan;
    }
}

var produkB = {
    nama: 'Produk B',
    harga: 85000,
    jumlah: 5,
    tersediaUntuk: function(dibutuhkan) {
        return this.jumlah >= dibutuhkan;
    }
}

console.log(produkA.tersediaUntuk(10));
console.log(produkA.tersediaUntuk(15));
console.log(produkB.tersediaUntuk(3));
console.log(produkB.tersediaUntuk(10));

Pada kode program di atas, saya memakai console.log() untuk mencetak output ke console milik browser Firefox atau Chrome. Untuk membuka console di Firefox, saya perlu memilih menu Developer, Web Console. Karena tidak ada konsep class, saya terpaksa menduplikasikan function tersediaUntuk() pada produkA dan produkB. Selain itu, tanpa class, produkA dan produkB boleh memiliki property yang berbeda walaupun mereka sama-sama mewakili produk.

JavaScript juga mendukung getter dan setter seperti pada Java untuk masing-masing property yang ada. Tapi cara mendefinisikannya agak merepotkan karena saya harus menggunakan Object.defineProperty() seperti pada contoh berikut ini:

var produkA = {
    nama: 'Produk A',
    harga: 120000,
    _jumlah: 10,
    tersediaUntuk: function(dibutuhkan) {
        return this.jumlah >= dibutuhkan;
    }
}
Object.defineProperty(produkA, 'jumlah', {
    get: function() {
        return this._jumlah;
    },
    set: function(nilai) {
        this._jumlah = (nilai < 0)? 0: nilai;
    }
});
produkA.jumlah = -10;
console.log(produkA.jumlah);  // output: 0

Tanpa class, saya harus mengulangi kode program di atas setiap kali saya ingin membuat object produkB, produkC, produkD, dan seterusnya. Ini adalah sesuatu yang melelahkan dan membuat kode program menjadi panjang. Untuk mengatasi masalah tersebut, developer JavaScript biasanya memakai constructor function. Sebagai contoh, kode program berikut ini adalah versi kode program yang memakai constructor function:

function Produk(nama, harga, jumlah) {
    this.nama = nama;
    this.harga = harga;
    Object.defineProperty(this, 'jumlah', {
        get: function() {
            return this._jumlah;
        },
        set: function(nilai) {
            this._jumlah = (nilai < 0)? 0: nilai;
        }
    });
    this.jumlah = jumlah;
    this.tersediaUntuk = function(dibutuhkan) {
        return this.jumlah >= dibutuhkan;
    }
}
var produkA = new Produk('Produk A', 120000, 10);
var produkB = new Produk('Produk B', 85000, 5);

console.log(produkA.nama); // output: Produk A
console.log(produkA.tersediaUntuk(5)); // output: true
produkA.jumlah = -10;
console.log(produkA.jumlah); // output: 0
console.log(produkB.nama); // output: Produk B
console.log(produkB.tersediaUntuk(10)); // output: false

Cara di atas lebih singkat karena saya seolah-olah memiliki class sehingga tidak ada duplikasi setiap kali membuat object baru. Bagi saya yang berasal dari Java, kode program di atas terlihat aneh, kenapa operator new dipadukan dengan sebuah Function? Di JavaScript, new harus selalu dipakai pada Function (sebagai pengganti class)! Operator new akan menciptakan sebuah object baru dan mengasosiasikan object baru tersebut dengan this di dalam function. Setelah function selesai dikerjakan, nilai dari this (object baru) akan dikembalikan.

Untuk mendapatkan informasi Function yang membuat object (di JavaScript, function seperti ini disebut constructor), saya dapat menggunakan kode program seperti:

console.log(produkA.constructor == Produk); // output: true
console.log(produkA instanceof Produk); // output: true
console.log(produkB.constructor == Produk); // output: true
console.log(produkB instanceof Produk); // output: true

Walaupun semuanya terlihat baik-baik saja sampai disini, ada satu permasalahan mendasar yang berhubungan dengan function. Pada JavaScript, sebuah nama function pada dasarnya adalah pointer ke function sehingga function dapat dianggap sebagai nilai (sama seperti pada property). Dengan menggunakan new di atas, setiap kali saya membuat object dengan Produk(), saya sebenarnya membuat nilai tersediaUntuk yang baru dan berbeda tetapi mengerjakan hal yang sama. Hal ini bisa dibuktikan dengan:

console.log(produkA.tersediaUntuk == produkB.tersediaUntuk); // output: false

Saya dapat mengatasi hal tersebut dengan menggunakan prototype (ini adalah fitur bawaan dari JavaScript dan tidak ada hubungannya dengan Prototype Framework!). Setiap kali sebuah function dibuat, JavaScript engine akan membuat property bernama prototype. Nilai prototype yang sama akan di-share ke seluruh object yang dibuat dengan function tersebut. Pada saat sebuah function dipanggil, JavaScript engine akan memeriksa apakah sebuah object memiliki function dengan nama yang akan dipanggil. Bila tidak ditemukan, JavaScript engine akan memeriksa apakah prototype untuk object tersebut mendefinisikan function yang akan dipanggil. Bila ada, maka function di prototype akan dikerjakan.

Sebagai contoh, berikut ini adalah versi yang memakai prototype:

function Produk(nama, harga, jumlah) {
    this.nama = nama;
    this.harga = harga;
    this.jumlah = jumlah;
}
Object.defineProperty(Produk.prototype, 'jumlah', {
    get: function () {
        return this._jumlah;
    },
    set: function (nilai) {
        this._jumlah = (nilai < 0) ? 0 : nilai;
    }
});
Produk.prototype.tersediaUntuk = function (dibutuhkan) {
    return this.jumlah >= dibutuhkan;
}
var produkA = new Produk('Produk A', 120000, 10);
var produkB = new Produk('Produk B', 85000, 5);
console.log(produkA.tersediaUntuk == produkB.tersediaUntuk); // output: true

Untuk melihat apa saja yang ada di prototype, saya dapat menggunakan Object.keys() seperti pada contoh kode program berikut ini:

var prototypeProdukA = Object.getPrototypeOf(produkA);
var prototypeProdukB = Object.getPrototypeOf(produkB);
console.log(prototypeProdukA == prototypeProdukB); // output: true
console.log(Object.keys(prototypeProdukA)); // output: ["tersediaUntuk"]
console.log(Object.keys(prototypeProdukB)); // output: ["tersediaUntuk"]

Sekarang, saya sudah mengetahui bagaimana pola yang umum dipakai untuk mendeklarasikan dan membuat object. Sebagai latihan, saya akan mencoba menambahkan beberapa class yang berbeda, misalnya Faktur dan ItemFaktur, seperti pada contoh berikut ini:

/** class Produk **/
function Produk(nama, harga, jumlah) {
    this.nama = nama;
    this.harga = harga;
    this.jumlah = jumlah;
};
Produk.prototype.tersediaUntuk = function (dibutuhkan) {
    return this.jumlah >= dibutuhkan;
};

/** class ItemFaktur **/
function ItemFaktur(produk, jumlah, harga) {
    this.produk = produk;
    this.jumlah = jumlah;
    this.harga = harga;
};
ItemFaktur.prototype.total = function () {
    return this.jumlah * this.harga;
};


/** class Faktur **/
function Faktur(nomor, tanggal, diskon) {
    this.nomor = nomor;
    this.tanggal = tanggal;
    this.items = [];
    this.diskon = diskon;
};
Faktur.prototype = {
    total: function () {        
        var totalItem = 0;
        this.items.forEach(function(item) {
            totalItem += item.total();
        });                    
        var totalDiskon = (this.diskon/100) * totalItem;
        return totalItem - totalDiskon;

    },
    tambah: function (produk, jumlah, harga) {
        this.items.push(new ItemFaktur(produk, jumlah, harga));
    }
};

/** Contoh Penggunaan **/
var produkA = new Produk('Produk A', 120000, 10);
var produkB = new Produk('Produk B', 85000, 5);
var faktur = new Faktur('FA-001', Date.now(), 10);
faktur.tambah(produkA, 5, 130000);
faktur.tambah(produkB, 10, 12000);
console.log(faktur); // output: {nomor: "FA-001", tanggal: 1401017015872, items: Array[2], diskon: 10}
console.log(faktur.total()); // output: 693000

Memakai Traits Di PHP 5.4

Salah satu fitur baru di PHP 5.4 adalah traits. Pada bahasa Groovy, traits masih sedang dalam proses pengembangan dan sedang didiskusikan. Apa itu traits? Anggap saja traits adalah copy-paste kode program yang dilakukan melalui bahasa pemograman. Kode program di dalam sebuah traits akan di-paste ke class lain yang memakainya.

Bukankah salah satu cara reuse kode program di OPP adalah dengan menggunakan inheritance? Saya sudah menunjukkannya di artikel Memakai Reflection API Di PHP. Pada artikel tersebut, method pada class DomainObject dapat dipakai di seluruh class turunannya.

Lalu, untuk apa ada traits? Kapan traits dipakai? Jawaban singkatnya adalah pada saat sebuah class perlu men-implementasi-kan lebih dari satu superclass. PHP, seperti Java, tidak mendukung multiple inheritance. Traits adalah alternatif untuk menerapkan multiple inheritance. Btw, saya sangat jarang sekali menemukan harus memakai multiple inheritance. Biasanya kebutuhan untuk memakai traits timbul pada saat saya membuat library/API dimana saya harus mengikuti API lain yang wajib men-extends class mereka, tetapi saya juga ingin menambahkan fitur baru yang reusable. Karena saya hanya boleh men-extends sebuah class, maka fitur reusable saya terpaksa harus di-implementasi-kan dalam bentuk traits.

Saat saya merancang domain class, sangat jarang sekali timbul kebutuhan untuk memakai traits. Oleh sebab itu, pada artikel ini, saya membuat kasus fiktif dimana saya mencoba menyelesaikannya dengan menggunakan traits. Kasus fiktif tersebut adalah sebuah kasus pada aplikasi penjualan, dimana order dapat terdiri atas produk atau paket atau keduanya. Sebuah paket adalah kumpulan dari beberapa produk yang dijual secara bersamaan. Selain itu, juga ada promosi. Sebuah promosi bisa berlaku untuk produk tunggal, beberapa produk, sebuah paket, beberapa paket, atau kombinasi produk dan paket. UML class diagram berikut memperlihatkan struktur rancangan awal class secara sederhana:

Sebuah rancangan awal

Sebuah rancangan awal

Terlihat bahwa seluruh class yang ada diturunkan dari sebuah abstract class bernama DomainObject. Bila diperhatikan lebih lanjut, pada class Promosi dan class ProdukPaket terdapat beberapa method dan operasi yang sama dengan fungsi yang sama. Duplikasi adalah code smell yang dapat menimbulkan masalah di kemudian hari. Oleh sebab itu, saya bisa me-refactor design tersebut menjadi seperti berikut ini:

Rancangan mutliple inheritance setelah refactor

Rancangan mutliple inheritance setelah refactor

Pada gambar di atas, saya membuat sebuah class baru, KomposisiProduk untuk mewakili sesuatu yang merupakan gabungan dari beberapa produk, misalnya ProdukPaket dan Promosi. Karena KomposisiProduk bukanlah sebuah entitas yang perlu disimpan ke database, maka saya tidak menurunkannya dari DomainObject.

Sampai disini, saya mengalami masalah yang berkaitan dengan bahasa PHP (dan juga Java). Sebuah class hanya boleh diturunkan (baca:inheritance) dari satu class lain. Pada rancangan di atas, class ProdukPaket diturunkan dari class Produk dan class KomposisiProduk. Ini adalah multiple inheritance dan tidak didukung oleh PHP (serta Java)! Saya perlu menurunkan ProdukPaket dari Produk karena class tersebut harus dianggap sebagai Produk yang dapat diperjualbelikan (misalnya oleh Order atau Promosi). Tapi tentu saja ProdukPaket adalah KomposisiProduk karena ia bisa terdiri atas lebih dari satu Produk. Jadi, ProdukPaket memiliki dua sifat tersebut. Apa yang harus saya lakukan untuk mengimplementasikan class ProdukPaket di PHP?

Salah satu solusi yang dapat saya tempuh adalah dengan mendefinisikan KomposisiProduk sebagai sebuah trait. Sebagai contoh, saya mendefinisikannya seperti berikut ini:

<?php

namespace domain;

trait KomposisiProduk {

    /**
     * Sebuah `SplObjectStorage` untuk menampung daftar `Produk`.
     *  
     * @var \SplObjectStorage
     */
    public $listProduk;

    /**
     * Menambah `Produk` baru untuk komposisi ini.
     * 
     * @param Produk $produk
     */
    public function tambahProduk($produk) {
        if ($this->listProduk === NULL) {
            $this->listProduk = new \SplObjectStorage();
        }               
        $this->listProduk->attach($produk);
    }

    /**
     * Menghapus `Produk` dari komposisi ini.
     * 
     * @param Produk $produk
     */
    public function hapusProduk($produk) {
        $this->listProduk->detach($produk);       
    }

}

?>

Pada kode program di atas, saya memakai SplObjectStorage untuk menampung object. Proses menambah dan menghapus object menjadi lebih mudah bila memakai SplObjectStorage ketimbang memakai array biasa. Sebuah trait tidak dapat dipakai secara langsung (dibuat instance-nya). Ia dipakai dengan disertakan pada class lain. Sebagai contoh, class ProdukPaket akan memakai trait di atas seperti yang terlihat pada kode program berikut ini:

<?php

namespace domain;

class ProdukPaket extends Produk {

    use KomposisiProduk;

    public function __construct($kode = NULL, $nama = NULL) {
        if ($kode) $this->kode= $kode;
        if ($nama) $this->nama = $nama;      
    }

    public function getHarga() {
        $harga = 0;
        foreach ($this->listProduk as $produk) {
            $harga += $produk->getHarga();           
        }
        return $harga;      
    }
}

?>

Pada kode program di atas, saya memakai trait dengan menggunakan keyword use. Jangan samakan ini dengan memakai namespace! Walaupun sama-sama memakai keyword use, mereka memiliki fungsi yang sangat berbeda! use pada trait memiliki arti bahwa seluruh kode program yang ada di KomposisiProduk akan di-copy paste ke class yang memakainya sementara use pada namespace berfungsi untuk men-import class yang dibutuhkan.

Sebuah trait dapat dipakai di beberapa class yang berbeda. Demikian juga, sebuah class dapat memakai lebih dari satu trait. Sebagai contoh, class Promosi juga akan memakai trait KomposisiProduk, seperti yang terlihat pada kode program berikut ini:

<?php

namespace domain;

class Promosi extends DomainObject {

    use KomposisiProduk;

    public $kode;

    public $nama;

    function __construct($kode = NULL, $nama = NULL) {
        if ($kode) $this->kode = $kode;
        if ($nama) $this->nama = $nama;
    }

}

?>

Untuk menguji domain class yang telah dibuat, saya akan membuat sebuah halaman PHP baru yang isinya seperti berikut ini:

<html>
<body>
<pre>
<?php

    spl_autoload_extensions('.php');
    spl_autoload_register();

    use domain\Produk;
    use domain\Promosi;
    use domain\ProdukPaket; 

    $produk1 = new Produk("SKU01112355", "ASRock Motherboard Socket LGA1155", 1534500); 
    $produk2 = new Produk("SKU01212438", "Intel Core i3-3220", 1555400);
    $produk3 = new Produk("SKU00310470", "Corsair Memory PC 4GB DDR3 PC-12800", 689700);

    $produk4 = new Produk("SKU01212254", "ASUS Motherboard Socket AM3/AM3+", 1190200);
    $produk5 = new Produk("SKU00306298", "AMD Phenom II X2 555 Black", 1068100);

    $produk6 = new Produk("SKU01112671", "Logitech Wireless Touch Keyboard K400R - Black", 541200);

    $paketIntel = new ProdukPaket("PI01", "Paket Intel 01");
    $paketIntel->tambahProduk($produk1);
    $paketIntel->tambahProduk($produk2);
    $paketIntel->tambahProduk($produk3);

    print "Paket Intel terdiri atas:\n\n";  
    foreach($paketIntel->listProduk as $p) {     
        printf("%35s %15d\n", $p->nama, $p->harga);
    }
    print "\nHarga Total Adalah: {$paketIntel->getHarga()}\n\n";

    $paketAmd = new ProdukPaket("PA01", "Paket AMD 01");
    $paketAmd->tambahProduk($produk4);
    $paketAmd->tambahProduk($produk5);
    $paketAmd->tambahProduk($produk3);

    print "Paket AMD terdiri atas:\n\n";
    foreach($paketAmd->listProduk as $p) {
        printf("%35s %15d\n", $p->nama, $p->harga);
    }
    print "\nHarga Total Adalah: {$paketAmd->getHarga()}\n\n";

    $promosiNatal = new Promosi("PRM011213", "Promosi Natal 2013");
    $promosiNatal->tambahProduk($paketAmd);
    $promosiNatal->tambahProduk($produk6);

    print "\n\nPromosi {$promosiNatal->nama} terdiri atas:\n\n";
    foreach ($promosiNatal->listProduk as $p) {
        printf("%50s %15d\n", $p->nama, $p->getHarga());
    }


?>
</pre>
</body>
</html>

Perhatikan bahwa object paketIntel, paketAmd dan promosiNatal akan memiliki method tambahProduk dan property listProduk secara otomatis berkat traits. Selain itu, paketIntel dan paketAmd tetap merupakan sebuah Produk sehingga mereka bisa ditambahkan ke Promosi atau Order sama seperti Produk lainnya. Bila dijalankan, halaman PHP di atas akan terlihat seperti berikut ini:

Paket Intel terdiri atas:

  ASRock Motherboard Socket LGA1155         1534500
                 Intel Core i3-3220         1555400
Corsair Memory PC 4GB DDR3 PC-12800          689700

Harga Total Adalah: 3779600

Paket AMD terdiri atas:

   ASUS Motherboard Socket AM3/AM3+         1190200
         AMD Phenom II X2 555 Black         1068100
Corsair Memory PC 4GB DDR3 PC-12800          689700

Harga Total Adalah: 2948000



Promosi Promosi Natal 2013 terdiri atas:

                                      Paket AMD 01         2948000
    Logitech Wireless Touch Keyboard K400R - Black          541200

Btw, penggunaan traits yang berlebihan dapat menimbulkan masalah di kemudian hari. Terkadang pandangan ini dapat diperdebatkan karena pada dasarnya seorang programmer yang memakai bahasa dinamis harusnya sudah siap menanggung resiko tinggi dari awal. Walaupun demikian, ada baiknya untuk mencoba mencari solusi memakai inheritance atau dependency injection terlebih dahulu sebelum akhirnya harus memakai traits.

Memakai Reflection API Di PHP

Reflection adalah kode program yang membaca struktur sebuah class dan memanipulasinya bila perlu. Pembuat framework atau library biasanya berusaha membuat kode program yang dapat dipakai se-fleksibel mungkin. Oleh sebab itu, mereka sering kali perlu menjawab pertanyaan seperti: apa nama class ini? ada property atau method yang ada di class ini? Reflection API dirancang untuk menjawab pertanyaan tersebut. Pada Java, saya dapat memakai reflection dengan menggunakan sebuah instance class bernama Class yang diperoleh dengan method getClass(). PHP juga menyediakan reflection API. Informasi mengenai reflection di PHP dapat dibaca di http://www.php.net/manual/en/intro.reflection.php.

Sebagai latihan, kali ini saya akan menerapkan reflection API untuk membuat kode program di artikel Memahami Overloading ala PHP. Saat ini, kode program di artikel tersebut hanya dapat diterapkan di domain class Pelanggan saja. Saya akan membuatnya menjadi sesuatu yang lebih fleksibel yang dapat diterapkan pada seluruh domain class yang ada.

Saya akan mulai dari method save(). Agar method save() menjadi fleksibel dan dapat dipakai di domain class lain, saya mengubahnya menjadi seperti berikut ini:

...
public function save() {
   $class = new \ReflectionClass(get_class());
   $namaTabel = $class->getShortName();
   $props = $class->getProperties(\ReflectionProperty::IS_PUBLIC);

   $daftarKolom = implode(',', array_map(function($prop) { 
      return $prop->name; 
   }, $props));

   $daftarParameter = implode(',', array_fill(0, sizeof($props), '?'));

   $daftarValue = array_map(function($prop) { 
      return $prop->getValue($this); 
   }, $props); 

   $db = new \PDO('mysql:host=localhost;dbname=exercises', 'snake', 'password');
   $st = $db->prepare("INSERT INTO $namaTabel($daftarKolom) VALUES ($daftarParameter)");     
   $st->execute($daftarValue);                           
}
...

Pada kode program di atas, saya membuat sebuah instance ReflectionClass yang merupakan pusat operasi reflection API. Sebagai contoh, saya bisa memperoleh nama class (tanpa namespace) dengan memanggil method getShortName() miliknya. Saya juga bisa memperoleh seluruh property untuk class dengan memanggil method getProperties() yang mengembalikan array yang berisi instance dari class ReflectionProperty. Bukan hanya itu, saya dapat memperoleh nilai sebuah property dengan memanggil method getValue() dari ReflectionProperty.

Berikutnya, saya juga mengubah method __callStatic() agar memakai reflection API, seperti yang terlihat pada kode program berikut ini:

...
public static function __callStatic($name, $args) {
   if (preg_match('/findBy([A-Z]\w*)/', $name, $matches)==1) {

      $class = new \ReflectionClass(get_class());
      $namaTabel = $class->getShortName();

      // Melakukan parsing nama method
      $expr = $matches[1];
      $fields = preg_split("/(And|Or)/", $expr, NULL, PREG_SPLIT_DELIM_CAPTURE);
      $sql = "SELECT * FROM $namaTabel WHERE ";
      for ($i=0; $i<sizeof($fields); $i++) {                
         $sql .= ' ' . $fields[$i] . ' = ? ';
         if (++$i < sizeof($fields) - 1) {
            $sql .= ' ' . $fields[$i];
         }  
      }

      // Melakukan query ke database
      $db = new \PDO('mysql:host=localhost;dbname=exercises', 'snake', 'password');
      $st = $db->prepare($sql);
      $st->execute($args);
      return $st->fetchAll(\PDO::FETCH_CLASS, $class->getName());                    
   }

   trigger_error("Method $name tidak ditemukan!", E_USER_ERROR);  
}
...

Agar lebih rapi, saya akan memindahkan kode program yang berkaitan dengan database ke sebuah class tersendiri. Untuk itu, saya membuat sebuah file baru dengan nama Database.php di folder util yang isinya seperti berikut ini:

<?php

namespace util;

class Database {

   private static $instance;

   public static function getInstance() {
      if (is_null(self::$instance)) {
         self::$instance = new Database();
         self::$instance->pdo = new \PDO('mysql:host=localhost;dbname=exercises', 'snake', 'password');
      }
      return self::$instance;
   }

   /**
    * Instance dari PDO untuk melakukan operasi database.
    * 
    * @var \PDO
    */
   public $pdo;

   private function __construct() {}

   public function execute($sql, $args = NULL) {
      $st = $this->pdo->prepare($sql);    
      $st->execute($args);    
   }

   public function executeFetchAll($sql, $class, $args = NULL) {
      $st = $this->pdo->prepare($sql);
      $st->execute($args);                   
      return $st->fetchAll(\PDO::FETCH_CLASS, $class);

   }

}

?>

Dengan demikian, saya bisa menyederhanakan kode program di Pelanggan.php menjadi seperti berikut ini:

<?php

namespace domain;

use util\Database;

class Pelanggan {

   ...

   public function save() {
      $class = new \ReflectionClass(get_class());
      $namaTabel = $class->getShortName();
      $props = $class->getProperties(\ReflectionProperty::IS_PUBLIC);

      $daftarKolom = implode(',', array_map(function($prop) { 
         return $prop->name; 
      }, $props));

      $daftarParameter = implode(',', array_fill(0, sizeof($props), '?'));

      $daftarValue = array_map(function($prop) { 
         return $prop->getValue($this); 
      }, $props); 

      Database::getInstance()->execute(
         "INSERT INTO $namaTabel($daftarKolom) VALUES ($daftarParameter)", 
         $daftarValue);    
   }

   public static function __callStatic($name, $args) {
      if (preg_match('/findBy([A-Z]\w*)/', $name, $matches)==1) {

         $class = new \ReflectionClass(get_class());
         $namaTabel = $class->getShortName();

         // Melakukan parsing nama method
         $expr = $matches[1];
         $fields = preg_split("/(And|Or)/", $expr, NULL, PREG_SPLIT_DELIM_CAPTURE);
         $sql = "SELECT * FROM $namaTabel WHERE ";
         for ($i=0; $i<sizeof($fields); $i++) {                
            $sql .= ' ' . $fields[$i] . ' = ? ';
            if (++$i < sizeof($fields) - 1) {
               $sql .= ' ' . $fields[$i];
            }  
         }

         // Melakukan query ke database
         return Database::getInstance()->executeFetchAll($sql, 
            $class->getName(), $args);
      }

      trigger_error("Method $name tidak ditemukan!", E_USER_ERROR);  
   }  

}

?>

Pertanyaan berikutnya: kode program sudah memakai reflection sehingga bersifat fleksibel, tapi bukankah setiap kali menambah domain class baru tetap harus men-copy paste kode program save() dan __callStatic() ke class baru tersebut? Untuk mengatasi masalah ini, saya dapat menggunakan salah satu fitur OOP yaitu inheritance. Sebuah class yang diturunkan dari class lain secara otomatis akan memiliki seluruh fitur dari class parent-nya. Dengan demikian, saya hanya perlu memastikan seluruh domain class diturunkan dari class DomainObject (pada file DomainObject.php) yang isinya seperti berikut ini:

<?php

namespace domain;

use util\Database;

abstract class DomainObject {

   public function save() {
      $class = new \ReflectionClass(get_class($this));
      $namaTabel = $class->getShortName();
      $props = $class->getProperties(\ReflectionProperty::IS_PUBLIC);

      $daftarKolom = implode(',', array_map(function($prop) {
         return $prop->name;
      }, $props));

      $daftarParameter = implode(',', array_fill(0, sizeof($props), '?'));

      $daftarValue = array_map(function($prop) {
         return $prop->getValue($this);
      }, $props);

      Database::getInstance()->execute(
         "INSERT INTO $namaTabel($daftarKolom) VALUES ($daftarParameter)",
         $daftarValue);
   }

   public static function __callStatic($name, $args) {
      if (preg_match('/findBy([A-Z]\w*)/', $name, $matches)==1) {

         $class = new \ReflectionClass(get_called_class());
         $namaTabel = $class->getShortName();

         // Melakukan parsing nama method
         $expr = $matches[1];
         $fields = preg_split("/(And|Or)/", $expr, NULL, PREG_SPLIT_DELIM_CAPTURE);
         $sql = "SELECT * FROM $namaTabel WHERE ";
         for ($i=0; $i<sizeof($fields); $i++) {
            $sql .= ' ' . $fields[$i] . ' = ? ';
            if (++$i < sizeof($fields) - 1) {
               $sql .= ' ' . $fields[$i];
            }
         }

         // Melakukan query ke database
         return Database::getInstance()->executeFetchAll($sql,
               $class->getName(), $args);
      }

      trigger_error("Method $name tidak ditemukan!", E_USER_ERROR);
   }

}

?>

Perhatikan bahwa saya mengubah get_class() menjadi get_class($this) agar bisa memperoleh reflection dari subclass (class turunan dari DomainObject). Karena method __callStatic() bersifat static, maka saya tidak dapat menggunakan get_class(). Sebagai alternatifnya, saya menggunakan get_called_class() yang diperkenalkan sejak PHP 5.3.

Selain itu, saya juga memberikan keyword abstract pada class DomainObject karena class ini tidak boleh diciptakan (dibuat instance-nya). Ia wajib harus dipakai melalui turunannya, seperti Pelanggan, Produk, Order, dan sebagainya.

Sekarang, saya dapat mengubah class Pelanggan agar diturunkan dari DomainObject dengan memakai keyword extends seperti pada kode program berikut ini:

<?php

namespace domain;

class Pelanggan extends DomainObject {

   public $nama;

   public $alamat;

   public $usia;     

   function __construct($nama = NULL, $alamat = NULL, $usia = NULL) {
      if ($nama) $this->nama = $nama;
      if ($alamat) $this->alamat = $alamat;
      if ($usia) $this->usia = $usia;
   }

}

?>

Bila saya menjalankan aplikasi PHP ini, method save() dan finders tetap bekerja dengan baik. Sekarang, saya juga dapat menambah domain model baru dengan mudah. Sebagai contoh, saya akan membuat sebuah class baru bernama Produk di file \domain\Produk.php yang isinya seperti berikut ini:

<?php

namespace domain;

class Produk extends DomainObject {

   public $kode;

   public $nama;

   public $harga;

   function __construct($kode = NULL, $nama = NULL, $harga = NULL) {
      if ($kode) $this->kode= $kode;
      if ($nama) $this->nama = $nama;
      if ($harga) $this->harga = $harga;
   }

}

?>

Setelah itu, saya membuat sebuah tabel dengan SQL seperti berikut ini:

CREATE TABLE produk (
   kode CHAR(7) PRIMARY KEY,
   nama VARCHAR(100) NOT NULL,
   harga INT NOT NULL
);

Berkat inheritance, class Produk secara otomatis memiliki method save() dan finders. Saya dapat membuktikannya dengan kode program seperti berikut ini:

<?php

   spl_autoload_extensions('.php');
   spl_autoload_register();

   use domain\Pelanggan;
   use domain\Produk;

   $snake = new Pelanggan("Solid Snake", "Alaska", 35);
   $snake->save();
   $liquid = new Pelanggan("Liquid Snake", "New York", 35);
   $liquid->save();
   $boss = new Pelanggan("The Boss", "Alaska", 40);
   $boss->save();

   $produk1 = new Produk("PR-0001", "Produk #1", 100000);
   $produk1->save();
   $produk2 = new Produk("PR-0002", "Produk #2", 150000);
   $produk2->save();

?>

<pre>
<?php

   print "Daftar pelanggan dengan nama Solid Snake di Alaska:\n";
   print_r(Pelanggan::findByNamaAndAlamat("Solid Snake", "Alaska"));

   print "\nDaftar pelanggan dengan usia 35 tahun:\n";
   print_r(Pelanggan::findByUsia(35));

   print "\nDaftar pelanggan yang tinggal di Alaska:\n";
   print_r(Pelanggan::findByAlamat("Alaska"));

   print "\nDaftar Produk Dengan Kode PR-0001:\n";
   print_r(Produk::findByKode("PR-0001"));

   print "\nDaftar Produk Dengan Nama 'Produk #2':\n";
   print_r(Produk::findByNama("Produk #2"));

?> 
</pre>   

Kode program di atas akan menyisipkan beberapa record ke tabel Pelanggan dan Produk, serta melakukan query ke tabel tersebut. Reflection API berperan penting membantu saya agar tidak perlu men-hard code operasi SQL ke dalam masing-masing class. Reflection API memungkinkan saya untuk menghasilkan kode program yang fleksibel dan reusable.

Memahami Overloading ala PHP

PHP tidak mendukung overloading!!! Yup! Dalam benak programmer Java, C++, C# dan sejenisnya, overloading adalah kemampuan untuk mendeklarasikan beberapa method dengan nama yang sama tetapi masing-masing memiliki jumlah dan tipe argumen yang berbeda. PHP tidak membolehkan hal tersebut!! Lalu mengapa di dokumentasi PHP terdapat sebuah halaman yang menjelaskan tentang overloading di PHP: http://php.net/__callstatic?

Apa yang disebut overloading oleh PHP adalah sesuatu yang mirip seperti methodMissing() di Groovy yang sering dipakai untuk keperluan meta programming. PHP menyediakan magic method seperti __set(), __get(), __isset(), __unset(), __call() dan __callStatic(). Mereka akan dipanggil bila programmer mengakses property atau method yang belum pernah dideklarasikan sebelumnya. Tentu saja overloading dengan cara seperti ini tentunya jauh lebih repot dibandingkan dengan Java dan C++, terutama bila yang perlu di-overload hanya beberapa method. Sebagai contoh, saya dapat melakukan ini di Java:

public class Pelanggan {

   public void pesan(Item item) {}

   public void pesan(List<Item> items) {}

   public void pesan(Item item, Integer jumlah, BigDecimal harga) {}

}

Untuk mencapai hal serupa di PHP, saya dapat menggunakan kode program seperti berikut ini:

<?php

class Pelanggan {

  public function __call($name, $arguments) {
    if ($name=='pesan') {
      switch (sizeof($arguments)) {
        case 1:
          if ($arguments[0] instanceof Item) {
            // kode program untuk pesan(Item item)
            print "pesan({$arguments[0]})<br>";
          } else if (is_array($arguments[0])) {
            // kode program untuk pesan(List items)
            print "pesan({$arguments[0]})<br>";
          }
          break;
        case 3:
          // kode program untuk pesan(Item item, Integer jumlah, BigDecimal harga)
          print "pesan({$arguments[0]},{$arguments[1]},{$arguments[2]})<br>";
          break;
      }
    }
  }

}

$pelanggan = new Pelanggan();
$pelanggan->pesan("ITEM1");
$pelanggan->pesan(["ITEM1", "ITEM2", "ITEM3"]);
$pelanggan->pesan("ITEM1", 10, 20000); 

?>

Beruntungnya, PHP adalah bahasa pemograman dinamis sehingga overloading bukanlah sesuatu yang sering dibutuhkan.

Karena apa yang disebut overloading di PHP lebih tepat disebut sebagai meta programming, maka saya bisa membuat dynamic finders (seperti di simple-jpa) di bahasa pemograman PHP. Perlu diperhatikan bahwa hal ini akan memberikan dampak buruk di sisi kinerja karena penggunaan magic method lebih lambat bila dibandingkan dengan mendeklarasikan method atau property secara langsung.

Sebagai contoh, saya membuat sebuah domain class sederhana di folder domain dengan nama Pelanggan.php yang isinya seperti berikut ini:

<?php
namespace domain;

class Pelanggan {

   public $nama;

   public $alamat;

   public $usia;     

   function __construct($nama = NULL, $alamat = NULL, $usia = NULL) {
      if ($nama) $this->nama = $nama;
      if ($alamat) $this->alamat = $alamat;
      if ($usia) $this->usia = $usia;
   }

   public function save() {      
      $db = new \PDO('mysql:host=localhost;dbname=exercises', 'snake', 'password');
      $st = $db->prepare("INSERT INTO Pelanggan(nama, alamat, usia) VALUES (?, ?, ?)");      
      $st->execute([$this->nama, $this->alamat, $this->usia]);                           
   }

}
?>

Pada kode program diatas, saya mendefinisikan sebuah class dengan nama Pelanggan. Class ini memiliki sebuah constructor untuk melakukan inisialisasi property-nya. Pada PHP, constructor juga adalah magic method. Saya melakukan penjagaan terhadap NULL di constructor karena saat saya melakukan fetching di PDO dengan PDO::FETCH_CLASS, kode program di constructor akan dikerjakan dengan argumen serba NULL setelah property diberi nilai. Ini adalah perilaku constructor yang tidak lazim dan saya perlu mewaspadainya.

Saya juga membuat sebuah method save() yang akan menyimpan data ke database dengan menggunakan PDO (PHP Data Object). PDO memungkinkan saya untuk mengakses database secara OOP. Selain itu, PDO adalah abstraction layer. Tanpa PDO, saya perlu mengakses database secara langsung dengan fungsi mysqli_xxx() yang hanya berlaku untuk database MySQL. Dengan PDO, jika suatu saat ini saya harus beralih ke database lain (misalnya Oracle), saya cukup perlu mengubah DSN (sebuah String yang isinya seperti 'mysql:host=localhost;dbname=exercises'). Mirip seperti JDBC di Java ‘bukan? Daftar database yang didukung (PDO driver) dapat dilihat di http://php.net/manual/en/pdo.drivers.php.

Pada method save(), saya juga memakai syntax baru di PHP 5.4 untuk mendeklarasikan array. Sebelum versi 5.4, array di PHP harus didefinsikan dengan keyword array seperti array(10, 20, 30, 40). PHP 5.4 membolehkan definisi array dengan cara seperti [10, 20, 30, 40]. Selain lebih sederhana, syntax baru ini juga adalah syntax deklarasi array di banyak bahasa dinamis lain seperti Groovy, Ruby, dan Python.

Berikutnya, saya akan menambahkan dynamic finders pada class Pelanggan. Karena finders tersebut dapat dipanggil kapan saja tanpa harus ada instance dari class, maka mereka harus berupa method static. PHP 5.3 memiliki __callStatic() yang membolehkan overloading (ingat bahwa ini istilah PHP sendiri!) method static. Berikut adalah isi method tersebut:

class Pelanggan {

   ...

   public static function __callStatic($name, $args) {
      if (preg_match('/findBy([A-Z]\w*)/', $name, $matches)==1) {

         // Melakukan parsing nama method
         $expr = $matches[1];
         $fields = preg_split("/(And|Or)/", $expr, NULL, PREG_SPLIT_DELIM_CAPTURE);
         $sql = 'SELECT * FROM Pelanggan WHERE ';
         for ($i=0; $i<sizeof($fields); $i++) {                
            $sql .= ' ' . $fields[$i] . ' = ? ';
            if (++$i < sizeof($fields) - 1) {
               $sql .= ' ' . $fields[$i];
            }  
         }

         // Melakukan query ke database
         $db = new \PDO('mysql:host=localhost;dbname=exercises', 'snake', 'password');
         $st = $db->prepare($sql);
         $st->execute($args);
         return $st->fetchAll(\PDO::FETCH_CLASS, '\domain\Pelanggan');                    
      }

      trigger_error("Method $name tidak ditemukan!", E_USER_ERROR);  
   }  

   ...

}
?>

Pada method di atas, saya menggunakan fetchAll() dari PDO dengan fecth style berupa PDO::FETCH_CLASS. Hal ini akan menyebabkan method fetchAll() mengembalikan sebuah array yang berisi instance dari class yang saya tentukan, yaitu Pelanggan. PDO akan mengisi property berdasarkan nama kolom di tabel.

Berikutnya, saya akan membuat sebuah tabel di database dengan perintah SQL berikut ini:

CREATE TABLE Pelanggan (
   nama VARCHAR(50) PRIMARY KEY,
   alamat VARCHAR(100),
   usia INT NOT NULL
);

Lalu, saya dapat memakai domain class saya, misalnya seperti berikut ini:

<?php

  spl_autoload_extensions('.php');
  spl_autoload_register();

  use domain\Pelanggan;

  $snake = new Pelanggan("Solid Snake", "Alaska", 35);
  $snake->save();
  $liquid = new Pelanggan("Liquid Snake", "New York", 35);
  $liquid->save();
  $boss = new Pelanggan("The Boss", "Alaska", 40);
  $boss->save();

?>

<pre>
<?php

  print "Daftar pelanggan dengan nama Solid Snake di Alaska:\n";
  print_r(Pelanggan::findByNamaAndAlamat("Solid Snake", "Alaska"));

  print "\nDaftar pelanggan dengan usia 35 tahun:\n";
  print_r(Pelanggan::findByUsia(35));

  print "\nDaftar pelanggan yang tinggal di Alaska:\n";
  print_r(Pelanggan::findByAlamat("Alaska"));

?>   
</pre>

Pada kode program di atas, saya menggunakan spl_autoload_extensions() dan spl_autoload_register() agar PHP secara otomatis men-include file class saat saya memakainya. Ingat bahwa saya mendefinisikan class pada file PHP yang terpisah. Tanpa kedua fungsi di atas, saya harus menyertakan file domain class saya dengan menggunakan keyword include atau require. Dengan fungsi spl_autoload_xxx() yang sudah ada sejak PHP 5.3, pada saat sebuah class hendak dipakai tetapi deklarasinya tidak ditemukan, maka autoloader akan bekerja mencari & membaca file yang tepat. Pada kode program di atas, autoloader akan mencari file di domain\Pelanggan.php bila menemukan penggunaan class \domain\Pelanggan. Konsekuensinya adalah sebuah class harus didefinisikan pada sebuah file PHP dengan nama yang sama dengan nama class. Setiap lokasi namespace juga harus diwakili dengan sebuah direktori/folder. Mirip seperti package di Java, bukan?

Kode program di atas akan menciptakan tiga record baru di tabel, lalu menggunakan finders untuk melakukan query, dimana hasilnya akan terlihat seperti berikut ini:

Daftar pelanggan dengan nama Solid Snake di Alaska:
Array
(
    [0] => domain\Pelanggan Object
        (
            [nama] => Solid Snake
            [alamat] => Alaska
            [usia] => 35
        )

)

Daftar pelanggan dengan usia 35 tahun:
Array
(
    [0] => domain\Pelanggan Object
        (
            [nama] => Liquid Snake
            [alamat] => New York
            [usia] => 35
        )

    [1] => domain\Pelanggan Object
        (
            [nama] => Solid Snake
            [alamat] => Alaska
            [usia] => 35
        )

)

Daftar pelanggan yang tinggal di Alaska:
Array
(
    [0] => domain\Pelanggan Object
        (
            [nama] => Solid Snake
            [alamat] => Alaska
            [usia] => 35
        )

    [1] => domain\Pelanggan Object
        (
            [nama] => The Boss
            [alamat] => Alaska
            [usia] => 40
        )

)

Mencetak ‘Object’ Dengan JasperReports: Merancang Laporan

Sesuai dengan paradigma pemograman berorientasi object (OOP),  saya meletakkan business logic ke dalam class.   Sebagai contoh, class Faktur dan ItemFaktur memiliki Diskon yang didalamnya mengandung rumus perhitungan diskon (baik untuk diskon berupa persen maupun potongan langsung).  UML Class Diagram berikut ini menunjukkan contoh rancangannya:

Contoh Rancangan OOP

Contoh Rancangan OOP

Selain business logic, sebuah sistem juga memiliki application logic seperti mencetak laporan, mengirim email, scheduling, dsb.  Saat ini saya perlu mencetak sebuah object Faktur ke printer, misalnya sebagai bukti pembayaran.   Apakah saya bisa langsung mencetak sebuah object?  Biasanya sebuah laporan selalu identik dengan hasil query dari database.   Tapi bila memakai SQL, maka saya tidak bisa memakai perhitungan yang sudah ada di class.   Seandainya saya bisa langsung mencetak object, maka akan lebih baik lagi karena business logic yang berkaitan dengan object tersebut akan ikut terbawa.  Apakah bisa?

Yup, bisa!  JasperReports dapat dipakai untuk keperluan ini.  Apa itu JasperReports?  JasperReports adalah sebuah library Java yang memungkinkan untuk menghasilkan dokumen pixel-perfect yang nantinya dapat ditampilkan, dicetak atau di-ekspor ke dalam format lain (seperti PDf atau Excel).   Pixel-perfect berarti dokumen yang dihasilkan memiliki elemen dengan posisi dan ukuran sesuai yang ditentukan; pixel-perfect merupakan kebalikan dari grid-based layout HTML <table> dimana posisi dan ukuran bersifat relatif.

Gambar berikut ini menunjukkan cara kerja JasperReports (diambil dari dokumentasi resminya):

Arsitektur JasperReports (Dari JasperReports Ultimate Guide)

Arsitektur JasperReports (Dari JasperReports Ultimate Guide)

Sebuah rancangan laporan ditulis dalam format XML yang disebut JRXML.   File XML ini akan menghasilkan sebuah JasperDesign.   Untuk laporan yang layoutnya sangat dinamis, JasperDesign dapat dibuat langsung dari kode program tanpa harus berdasarkan file XML.   JasperDesign perlu di-compile menjadi sebuah JasperReport.   Tujuannya untuk melakukan validasi dan memastikan tidak ada kesalahan pada JasperDesign.   Berikutnya, untuk menampilkan sebuah JasperReport, developer perlu menentukan data yang akan dipakai oleh JasperReport tersebut.   Setelah melakukan proses pengisian data dengan JasperFillManager,  hasilnya adalah sebuah dokumen pixel-perfect yang dapat ditampilkan, dicetak, atau di-export ke berbagai format lain.

Yang menarik disini adalah proses ‘pengisian data’ bukan bagian dari rancangan struktur laporan.   Sebuah JasperReport yang sama dapat di-isi dengan data yang berbeda berkali-kali untuk menghasilkan output laporan yang berbeda.   Menariknya, tidak ada yang mensyaratkan bahwa sebuah JasperReport harus selalu di-isi dengan data dari database.   JasperReport dapat di-isi melalui apa saja yang mengimplementasikan interface JRDataSource yang mewajibkan dua method, yakni  next() dan getFieldValue().

Contoh implementasi JRDataSource bawaan adalah:

  • JRResultSetDataSource: mengisi berdasarkan hasil query SQL melalui JDBC. Ini yang sering dijumpai.
  • JRBeanArrayDataSource: mengisi berdasarkan sebuah array yang berisi object-object JavaBean.
  • JRBeanCollectionDataSource: mengisi berdasarkan sebuah Collection yang berisi object-object JavaBean.
  • JRMapArrayDataSource: mengisi berdasarkan sebuah array yang berisi Map yang mewakili sebuah ‘baris’.
  • JRTableModelDataSource: mengisi berdasarkan TableModel dari Swing, dengan kata lain, berdasarkan isi dari sebuah JTable.
  • JRXmlDataSource: mengisi berdasarkan dokumen XML.
  • JRCsvDataSource: mengisi berdasarkan file teks dalam format CSV (comma-separated value).
  • JRXlsDataSource: mengisi berdasarkan isi file Microsoft Excel (*.xls).
  • JREmptyDataSource: mengisi dengan baris yang seluruhnya datanya bernilai null.

Berdasarkan daftar di atas, untuk mencetak sebuah ‘object‘, saya memiliki banyak pilihan.   Pada tulisan ini, saya akan memakai JRBeanCollectionDataSource untuk mengisi laporan.   Data source tersebut membutuhkan sebuah Collection seperti List yang didalamnya berisi satu atau lebih object JavaBean.  Apa yang dimaksud sebagai JavaBean?   JavaBean adalah sebuah class di Java yang mengikuti beberapa syarat:

  • Harus memiliki default constructor yaitu sebuah constructor tanpa parameter.
  • Property yang bisa diakses oleh pihak luar harus dipublikasikan melalui setter seperti setXXX() dan getter seperti getXXX() atau isXXX().   Sebagai contoh, sebuah property String namaLengkap harus memiliki setNamaLengkap() dan getNamaLengkap().   Beberapa pelajar yang tidak mengetahui tentang JavaBean mungkin merasa heran melihat saya suka menggabungkan bahasa Inggris dan bahasa Indonesia.   Beberapa bahkan melakukan improvisasi dengan mengubah method menjadi ambilNamaLengkap() dan tulisNamaLengkap(), tapi ini bukan lagi sebuah JavaBean.   Groovy menyederhanakan proses yang ada dimana bila saya mendeklarasikan sebuah String namaLengkap, maka getter dan setter secara otomatis akan dibuat, dan variabel namaLengkap tersebut akan selalu diakses berdasarkan getter/setter-nya.
  • JPA Entity yang dihasilkan oleh simple-jpa mengikuti aturan JavaBean, sehingga saya dapat memakai JRBeanCollectionDataSource.

Untuk merancang laporan, saya tidak akan mengetik JRXML secara manual.  Sebagai gantinya, saya memakai editor berbasis GUI yang disebut iReport.   Dengan iReport, saya dapat membuat isi JRXML, men-compile laporan, mengisi data dan men-preview laporan secara mudah (visual).

Langkah pertama yang saya lakukan adalah membuat laporan baru dengan memilih File, New.   Setelah itu, saya memiliki template Blank A4.   Pada dialog berikutnya, saya menentukan lokasi penyimpanan file JRXML.   Setelah itu saya men-klik tombol Next dan memilih Finish.

Langkah kedua adalah menambahkan classpath sehingga iReport mengetahui isi file class JavaBean yang akan dipakai.  Dalam hal ini, class yang dibutuhkan adalah Faktur.class, ItemFaktur.class, Diskon.class dan sebagainya.   Karena saya memakai Griffon, maka saya memberikan perintah:

griffon package jar

untuk menghasilkan file Jar dari aplikasi saya.  Di dalam file Jar tersebut sudah berisi seluruh domain class.

Kemudian kembeli ke iReport, saya memilih menu Tools, Options.   Pada tab Classpath, saya menklik tombol Add JAR.   Kemudian saya men-browse ke lokasi file JAR yang dihasilkan Griffon, yaitu di folder dist/jar di lokasi proyek saya.

Langkah ketiga adalah men-klik icon Report Query dan memilih tab JavaBean Datasource.   Pada Class name, saya mengisi dengan domain.Faktur,  kemudian men-klik tombol Read attributes.  Setelah itu, saya memilih field yang perlu dicetak dan menambahkannya dengan men-klik tombol Add selected field(s), seperti yang terlihat pada gambar berikut ini:

Membaca field dari JavaBean

Membaca field dari JavaBean

Setelah ini, saya dapat mulai merancang faktur yang akan dicetak.   Saya akan membuang beberapa band yang tidak dibutuhkan seperti Title, Column Header, Column Footer, dan Page Footer.   Cara untuk membuang sebuah band adalah dengan men-klik kanan pada wilayah band tersebut dan memilih Delete Band.   Saya menyisakan band Summary untuk menampilkan total di akhir laporan.

Pada band Page Header akan terdapat judul dan informasi nomor halaman.  Lalu, pada band Detail akan terdapat isi laporan yang terlihat seperti pada gambar berikut ini:

Rancangan 'master' laporan

Rancangan ‘master’ laporan

Sebuah faktur pada dasarnya adalah sebuah formulir master-detail.   Saya telah menambahkan informasi master, lalu bagaimana dengan detail-nya? Detail untuk sebuah faktur adalah setiap line item yang berisi barang, jumlah, dan harga.  Bagaimana cara menambahkan detail tersebut?

Pada JasperReports versi lama, saya harus membuat report baru dan menyisipkannya sebagai subreport.   Beruntungnya, sejak JasperReports memperkenalkan konsep component di laporan, saya dapat menggunakan table component.   Sebuah table component hampir mirip seperti subreport tetapi ia merupakan bagian dari laporan yang sama.   Keuntungannya?  Table component lebih mudah dipakai karena tidak perlu repot membuat report baru dan menggabungkannya.

Saya segera men-drag icon table component ke band Detail:

Menambahkan table component ke detail

Menambahkan table component ke detail

Akan muncul sebuah Table wizard.   Saya men-klik tombol New dataset.   Pada Dataset name, saya mengisi dengan itemFakturDataset. Kemudian saya memilih Create an empty dataset dan men-klik tombol Finish.   Setelah itu, saya men-klik tombol Next untuk memasuki halaman Table Style.   Saya menghilangkan tanda centang pada Create a new set of styles for this table, Add Table Header dan Add Table Footer.   Setelah itu, saya men-klik tombol Finish.

Sekarang, saya bisa berpindah antara Main report untuk mengubah laporan secara keseluruhan, atau Table 1 khusus untuk mengubah detail faktur, seperti yang terlihat pada gambar berikut ini:

Table component merupakan bagian dari report yang sama.

Table component merupakan bagian dari report yang sama.

Saya akan melakukan beberapa pengaturan terlebih dahulu pada itemFakturDataset dengan men-klik dataset tersebut dan memilih Edit Query seperti yang terlihat pada gambar berikut ini:

Mengubah Dataset

Mengubah Dataset

Pada dialog Report query yang muncul, saya memilih tab JavaBean Datasource.   Setiap line item yang perlu ditampilkan diwakili oleh sebuah object ItemFaktur, sehingga saya mengisi domain.ItemFaktur pada Class name.   Setelah itu, saya men-klik tombol Read attributes, memilih property yang perlu ditampilkan, men-klik tombol Add selected field(s), lalu menekan tombol Ok untuk selesai.

Berikutnya saya perlu memberi tahu iReport bahwa data source untuk table component ini diambil dari property itemList milik Faktur.   Untuk itu, saya men-klik kanan di bagian yang kosong dan memilih menu Edit table datasource.   Pada Connection/Datasource exp, saya mengisi data source expression seperti pada gambar berikut ini:

Mengisi datasource expression

Mengisi datasource expression

Ekspresi di atas akan membuat sebuah JRBeanCollectionDataSource baru yang datanya diambil dari field itemList.  Bila dilihat dari class diagram, itemList adalah sebuah List yang berisi satu atau lebih ItemFaktur (relasi one-to-many).  Nantinya, table component akan menampilkan masing-masing ItemFaktur yang dimiliki oleh Faktur yang sedang diproses.

Langkah terakhir sebelum mulai merancang adalah menentukan field apa saja yang perlu dipakai.  Berikut ini adalah definisi field untuk table component ini:

Definisi fields untuk itemFakturDataset

Definisi fields untuk itemFakturDataset

Class ItemFaktur memiliki relasi many-to-one dengan class Barang.   Untuk mengakses property milik class Barang dari ItemFaktur, saya dapat menggunakan dengan operator titik.   Misalnya, barang.nama akan memanggil method getNama() dari atribut barang yang dimiliki oleh ItemFaktur yang sedang diproses.   Sebuah Barang juga memiliki hubungan many-to-one dengan class Jenis.   Property milik class Jenis juga dapat diakses dengan tanda titik, misalnya seperti barang.jenis.nama.

Berikutnya saya akan merancang isi detail untuk table component dimana hasil akhirnya akan terlihat seperti pada gambar berikut ini:

Rancangan untuk table component

Rancangan untuk table component

Untuk menambah kolom baru, saya men-klik bagian kosong dan memilih Add Column To The End.

Saya memakai variabel $V{REPORT_COUNT} untuk mendapatkan nomor line item yang berurut.  Tapi bagaimana dengan perhitungan subtotal?  Bukankah class ItemFaktur sudah memiliki method total() yang akan menghitung total setelah dikurangi diskon?  Bagaimana cara memanggilnya?

Method total() tidak mengikuti standar penamaan getter atau setter JavaBean sehingga ia bukanlah sebuah property yang dapat diakses sebagai field.   Tapi ada cara lain untuk mengaksesnya.   JasperReports memungkinkan pengguna untuk mendefinisikan sebuah field dengan nama _THIS.   Nilai dari field _THIS selalu merupakan instance dari objek yang sedang diproses (dalam hal ini adalah sebuah objek ItemFaktur).   Dengan demikian, ekspresi $F{_THIS}.total() akan memanggil method total() dari objek ItemFaktur yang sedang diproses.

Mendefinisikan field _THIS

Mendefinisikan field _THIS

Memanggil method total()

Memanggil method total()

Setelah ini, saya kembali ke Main Report.   Saya juga mendefinisikan field _THIS serta menambahkan informasi summary (total Faktur secara keseluruhan), seperti yang terlihat pada gambar berikut ini:

Contoh rancangan akhir report

Contoh rancangan akhir report

Pada total gross, saya memakai ekspresi Groovy berikut ini untuk menghitung total seluruh item faktur:

$F{_THIS}.itemList*.total().sum()

Hasil perhitungan total net sebenarnya sudah dikalkulasi oleh method total() milik class Faktur.   Saya dapat memanggil method tersebut dengan menggunakan ekspresi seperti:

$F{_THIS}.total()

Dengan demikian, bila saya melakukan perubahan rumus kalkulasi pada method total() di domain class, maka selain diterapkan di aplikasi (tampilan form), perubahan juga akan langsung mempengaruhi laporan (atau percetakan).

Sampai disini, saya dapat langsung memakai report di aplikasi.   Tapi akan lebih baik bila saya melihat seperti apa tampilan report di iReport terlebih dahulu.   Caranya adalah dengan membuat sebuah class dengan method static yang menghasilkan data dummy sebagai informasi yang akan ditampilkan.  Class ini terletak di JAR yang sama dengan yang diberikan di classpath.   Sebagai contoh, saya membuat class dengan isi seperti ini:

package util

import ...

class ReportTest {

    static List getDataFaktur() {
        List hasil = []

        Faktur faktur = new Faktur(nomor: '12345', tanggal: LocalDate.now())
        Jenis jenis1 = new Jenis("JENIS1", "JENIS1")
        (1..20).each {
            Barang barang = new Barang("BRG1$it", "Barang1$it", jenis1, 12345)
            faktur.tambahItem(barang, new BigDecimal(Math.random() * 10000000), (Math.random() * 1000).intValue(),
                    new Diskon(Math.random()*50, Math.random() * 1000))
        }
        Jenis jenis2 = new Jenis("JENIS2", "JENIS2")
        (1..20).each {
            Barang barang = new Barang("BRG2$it", "Barang2$it", jenis2, 54321)
            faktur.tambahItem(barang, new BigDecimal(Math.random() * 10000000), (Math.random() * 1000).intValue(),
                    new Diskon(Math.random()*50, Math.random() * 1000))
        }
        faktur.potonganRetur = 300000
        faktur.proses(false, LocalDate.now().plusDays(30), 3, null)

        hasil << faktur
        hasil
    }

}

Pada kode program di atas, agar cepat, saya membuat instance objek secara langsung tanpa mengambil dari database.   Tujuan utamanya hanya menciptakan objek yang akan ditampilkan di laporan nantinya.   Proses pengambilan object dari database pada aplikasi nanti tetap dilakukan melalui JPA atau simple-jpa.

Berikutnya, saya menambahkan sumber data tersebut dengan men-klik icon Report Datasources, dan men-klik New pada dialog yang muncul.

Menambahkan datasource baru

Menambahkan datasource baru

Pada kotak dialog Datasource yang muncul, saya memilih JavaBeans set datasource dan men-klik tombol Next.   Saya mengisi dialog sesuai dengan informasi class saya, kemudian men-klik tombol Test untuk mengujinya.   Setelah semua berjalan dengan benar, saya men-klik tombol Save untuk menyimpan perubahan.

Mendefinisikan source JavaBean yang dipakai untuk preview laporan di iReport

Mendefinisikan source JavaBean yang dipakai untuk preview laporan di iReport

Untuk melihat seperti apa tampilan report, saya men-klik tombol Preview, seperti yang terlihat pada gambar berikut ini:

Contoh hasil preview laporan

Contoh hasil preview laporan

 

Contoh hasil preview laporan dengan summary

Contoh hasil preview laporan dengan summary

Menerapkan Inheritance (Bagi Pemula)

Salah satu sifat OOP yang kerap mudah dicerna tetapi sulit diterapkan oleh pemula adalah inheritance.   Pada ruangan kelas, biasanya saya akan memakai Greenfoot yang dilengkapi animasi menarik untuk menunjukkan konsep dasar inheritance.   Selain itu, di buku-buku banyak dijumpai contoh klasik seperti Mobil, Motor dan Truk yang biasanya mudah dipahami.   Bila tidak cukup, ada contoh Binatang, Kucing, Burung, dan Ikan.   Tujuan dari semua contoh tersebut adalah untuk menunjukkan bagaimana cara kerja inheritance.   Tapi beberapa pelajar yang kritis selalu bertanya: seperti apa penerapan inheritance di kasus yang lebih serius selain kendaraan dan binatang?

Berikut adalah contoh sebuah class diagram yang menunjukkan inheritance:

Contoh Inheritance

Contoh Inheritance

Pada hampir semua jenis faktur, biasanya kita akan menjumpai beberapa atribut yang sama.   Misalnya nomor, tanggal dan detail baris per baris.   Selain itu, hampir semua faktur juga memiliki operasi yang sama, seperti menghitung total, menambah item, hapus, dan sebagainya.   Oleh sebab itu, saya meletakkan seluruh atribut dan operasi yang sama ke sebuah class yang diberi nama Faktur.   Seluruh turunan dari class Faktur akan memiliki atribut dan operasi miliknya, termasuk juga ‘cucu’-nya: FakturJual dan FakturBeli. Dengan demikian, saya bisa membuat kode program seperti:

FakturMutasi m = new FakturMutasi()
m.nomor = ...
m.tanggal = ...
m.detail = ...
m.total()

FakturJual j = new FakturJual()
j.nomor = ...
j.tanggal = ...
j.detail = ...
j.total()

FakturBeli b = new FakturBeli()
b.nomor = ...
b.tanggal = ...
b.detail = ...
b.total()

Apa manfaatnya? Setiap kali menambah class yang mewakili faktur baru, saya hanya perlu menurunkan class tersebut dari class Faktur dan secara otomatis class baru tersebut telah memiliki seluruh atribut dan operasi dasar untuk sebuah faktur. Ini adalah apa yang disebut memakai yang sudah ada tanpa copy paste, atau bahasa kerennya: reusable.

Pertanyaannya sekarang, apakah kode berikut ini valid?

Faktur f = new Faktur()
f.nomor = ...
f.detail = ...
f.total()

Variabel f merujuk ke faktur apa? Pembelian? Penjualan? Mutasi? Tidak jelas! Oleh sebab itu, saya membuat class Faktur dan FakturPembayaran menjadi abstract class. Sebuah abstract class adalah class yang tidak boleh dipakai secara langsung, melainkan harus dipakai melalui turunannya. Sebagai contoh:

Faktur f = new Faktur()  // <-- ERRROR!

Faktur m = new FakturMutasi()
Faktur j = new FakturJual()
Faktur b = new FakturBeli()

FakturPembayaran p = new FakturPembayaran()  // <-- ERROR!

FakturPembayaran pj = new FakturJual()
FakturPembayaran pb = new FakturBeli()

Apa manfaat abstact class? Hmmh.. Berjaga-jaga agar saya atau rekan kerja saya yang sedang ngantuk tidak secara sengaja membuat instance baru sebuah class! Bila semua developer memiliki instinct Spiderman sehingga mereka tidak pernah ceroboh dalam membuat kode program, maka abstract class tidak perlu ada di dunia ini 😉

Biasanya pelajar akan cepat sekali menangkap atribut yang sama di faktur, tapi ada satu calon operasi yang sering terlupakan. Apakah operasi yang selalu ada pada seluruh faktur tapi belum saya tuliskan di atas? Perubahan nilai stok (inventory)! Seluruh faktur akan mempengaruhi nilai inventory, hanya saja dengan nilai berbeda. Misalnya faktur jual akan mengurangi jumlah stok sementara faktur beli akan meningkatkan jumlah stok. Sepertinya ini adalah sebuah proses yang sama dengan nilai yang berbeda, bukan?

Oleh sebab itu saya menambahkan method konversiStok() di class Faktur. Method ini adalah abstract method. Dengan demikian, tidak ada implementasi kode programnya disini. Tapi, seluruh turunan class Faktur yang bukan abstract class wajib mendefinisikan method ini. Misalnya:

abstract class Faktur {

  String nomor

  ...

  // Mengembalikan perubahan stok (bisa plus atau minus) berdasarkan
  // total kuantitas faktur
  abstract Integer konversiStok()

}

class FakturMutasi extends Faktur {

  ...

  Integer konversiStok() {
     return -totalKuantitas()
  }

}

class FakturJual extends FakturPembayaran {

  ...

  Integer konversiStok() {
     return -totalKuantitas()
  }

}

class FakturBeli extends FakturPembayaran {

  ...

  Integer konversiStok() {
     return totalKuantitas()
  }
}

Perhatikan bahwa class FakturPembayaran tidak perlu membuat implementasi konversiStok() karena class tersebut juga merupakan class abstract. Tapi kewajiban untuk menentukan pengaruh stok tetap berlaku bagi seluruh turunan class FakturPembayaran yang tidak abstract. Jadi, apa yang didefinisikan di class Faktur berlaku turunan temurun tanpa pandang bulu termasuk sampai ke seluruh cucu dan cicitnya nanti!

Apa manfaatnya? Setiap class konkrit dari Faktur memberi tahu apa pengaruh masing-masing dari mereka terhadap kuantitas di stok. Dengan demikian, pada saat perhitungan kuantitas stok, saya cukup memakai kode program seperti:

void perubahanStok(Faktur faktur) {
  if (!faktur.hapus) {
      jumlahStok = jumlahStok + faktur.konversiStok()
  } else {
      jumlahStok = jumlahStok - faktur.konversiStok()
  }
}

Kode program di atas jauh lebih mudah dipahami, lebih simple (ingat prinsip KIS!), dan lebih aman terhadap kesalahan, bila dibandingkan dengan versi prosedural berikut ini:

//
// Ini contoh yang tidak disarankan!
//
if (tambahFaktur) {
  if (pembelian) {
    jumlahStok = jumlahStok + totalKuantitasFaktur
  } else if (penjualan) {
    jumlahStok = jumlahStok - totalKuantitasFaktur
  } else if (mutasi) {
    jumlahStok = jumlahStok - totalKuantitasFaktur
  }
} else if (hapusFaktur) {
  if (pembelian) {
    jumlahStok = jumlahStok - totalKuantitasFaktur
  } else if (penjualan) {
    jumlahStok = jumlahStok + totalKuantitasFaktur
  } else if (mutasi) {
    jumlahStok = jumlahStok + totalKuantitasFaktur
  }
}

P.S: Bicara soal prinsip KIS (Keep It Simple), sebenarnya istilah ini relatif terhadap pengetahuan developer terkait. Seorang developer yang terbiasa dengan konsep prosedural dan modular akan berteriak: “KIS!” saat melihat rancangan OOP yang mengklasifikasikan (arti kata ‘class’) masalah dalam banyak class-class terpisah.

Beberapa buku OOP yang saya baca menyarankan bahwa bila dijumpai banyak if seperti di atas, biasanya adalah calon yang dimana inheritance dapat diterapakan.

Sekarang mari mengasah kemampuan memikirkan kode program secara vertikal sekali lagi! Seandainya, saya memiliki class sederhana dengan rancangan seperti berikut ini:

Contoh rancangan dengan total()

Contoh rancangan dengan total()

Anggap saja saya sudah membuat implementasi kode program yang memakai class tersebut dan sudah berjalan selama berbulan-bulan. Lalu tiba-tiba saya menyadari bahwa ada sesuatu yang kurang: seluruh faktur beli maupun faktur jual dapat memiliki potongan retur. Nilai potongan retur ini akan mempengaruhi total! Sementara itu, method Faktur.total() sudah dipakai dimana-mana termasuk di laporan. Apa yang harus saya lakukan? Berikut adalah permasalahannya:

  1. Atribut potongan retur hanya berlaku untuk faktur beli dan faktur jual, dengan demikian ia adalah kandidat yang tepat untuk diletakkan di class FakturPembayaran. Tapi method total() ada di Faktur. Padahal potongan retur mempengaruhi perhitungan total.
  2. Bila saya meletakkan potongan retur di class Faktur, maka seluruh turunan class Faktur akan memiliki potongan retur. Bukankah ini terlalu berlebihan?
  3. Jadi?

Solusinya adalah dengan men-override method total() milik Faktur di FakturPembayaran. Bagi yang bingung, ingat contoh klasik: Binatang bisa bersuara, kucing akan mengeong dan anjing akan mengonggong. Bila diterapkan ke kasus ini: Faktur bisa menghitung total, dan FakturPembayaran memiliki perhitungan totalnya sendiri yang melibatkan potongan retur.

Override total() milik Faktur di FakturPembayaran

Override total() milik Faktur di FakturPembayaran

Berikut adalah contoh implementasinya:

abstract class Faktur {

  ...

  BigDecimal total() {
     ... // hitung berdasarkan seluruh item dan diskon
  }

}

class FakturPembayaran extends Faktur {

  ...

  BigDecimal potonganRetur

  BigDecimal total() {
     super.total() - (potonganRetur?: 0)
  }

}

Apa manfaatnya? Dari sisi penggunaan class, dampak perubahan hanya berpengaruh terhadap FakturBeli dan FakturJual saja. Dengan meng-enkapsulasi permasalahan sehingga membatasi ruang lingkup dampaknya, maka tingkat kesalahan akibat hal tak terduga dari perubahan bisa dikurangi. Misalnya:

Faktur m = new FakturMutasi()
m.total()  // <-- memanggil Faktur.total()

Faktur fj = new FakturJual()
fj.total()  // <-- memanggil FakturPembayaran.total()

Faktur fb = new FakturBeli()
fb.total()  // <-- memanggil FakturPembelian.total()

Bagi pemula, pemanggilan kode program secara vertikal memang sesuatu yang aneh bila dibandingkan dengan memanggil method per modul atau per class. Tapi bila diterapkan dengan benar, bisa jadi inheritance akan menjauhkan yang bersangkutan dari banyak masalah di kemudian hari 😉