Membuat Fitur “Produk Serupa” Dengan PredictionIO

PredictionIO (https://prediction.io) adalah sebuah engine yang dirancang untuk mudah dipakai dalam menerapkan machine learning. Lalu apa manfaatnya? Produk ini tidak hanya berguna untuk peneliti, tetapi juga bisa diterapkan langsung pada situs e-commerce. Salah satunya adalah PredictionIO bisa digunakan untuk menghasilkan daftar “produk serupa” berdasarkan perilaku dari pengunjung lain. Tanpa machine learning, pembuat situs biasanya menampilkan produk yang acak berdasarkan tag tertentu. Dengan menghasilkan daftar “produk serupa” secara pintar, pengguna akan memperoleh saran yang lebih akurat sehingga penjualan bisa meningkat, terutama bagi situs e-commerce yang memiliki banyak produk bervariasi.

Sebagai latihan, saya akan menjalankan engine PredictionIO pada sebuah server Linux yang terpisah dari server web. Salah satu kelebihan PredictionIO adalah masing-masing algoritma dikelompokkan dalam apa yang disebut dengan engine template. Saya dapat menemukan daftar engine template siap pakai di https://templates.prediction.io. Sesungguhnya sebuah engine template bukan hanya mengandung algoritma, tetapi komponen lengkap yang disebut DASE (Data, Algorithm, Serving, Evaluator). Developer juga bisa memodifikasi kode program engine template (dalam bahasa Scala) sesuai dengan kebutuhan.

Karena ingin memunculkan “produk serupa”, saya akan menggunakan template PredictionIO/template-scala-parallel-complementarypurchase yang dapat dijumpai di https://templates.prediction.io/PredictionIO/template-scala-parallel-complementarypurchase. Berdasarkan informasi dari dokumentasi, algoritma yang dipakai oleh engine template ini mengikuti konsep association rule learning (https://en.wikipedia.org/wiki/Association_rule_learning).

Sebagai langkah awal, saya memberikan perintah berikut ini untuk men-download kode program engine template:

$ pio template get PredictionIO/template-scala-parallel-complementarypurchase latihanEngine
Please enter author's name: latihan
Please enter the template's Scala package name (e.g. com.mycompany): com.latihan
Please enter author's e-mail address: 
Author's name:         latihan
Author's e-mail:       
Author's organization: com.latihan
Would you like to be informed about new bug fixes and security updates of this template? (Y/n) n
Retrieving PredictionIO/template-scala-parallel-complementarypurchase
There are 5 tags
Using tag v0.3.1
Going to download https://github.com/PredictionIO/template-scala-parallel-complementarypurchase/archive/v0.3.1.zip
Redirecting to https://codeload.github.com/PredictionIO/template-scala-parallel-complementarypurchase/zip/v0.3.1
Replacing org.template.complementarypurchase with com.latihan...
Processing latihanEngine/build.sbt...
Processing latihanEngine/engine.json...
Processing latihanEngine/src/main/scala/Algorithm.scala...
Processing latihanEngine/src/main/scala/DataSource.scala...
Processing latihanEngine/src/main/scala/Engine.scala...
Processing latihanEngine/src/main/scala/Preparator.scala...
Processing latihanEngine/src/main/scala/Serving.scala...
Engine template PredictionIO/template-scala-parallel-complementarypurchase is now ready at latihanEngine

Perintah di atas akan menciptakan sebuah folder bernama latihanEngine yang berisi kode program yang bisa saya modifikasi sesuai keperluan. Karena akan bekerja pada folder ini, maka saya perlu pindah ke folder ini dengan memberikan perintah:

$ cd latihanEngine

Setelah itu, saya siap untuk menjalankan PredictionIO dengan memberikan perintah seperti berikut ini:

$ pio-start-all

Saya bisa memerika apakah semua komponen dijalankan dengan baik dengan menggunakan perintah seperti berikut ini:

$ pio status
PredictionIO
  Installed at: /home/snake/PredictionIO
  Version: 0.9.2

Apache Spark
  Installed at: /home/snake/PredictionIO/vendors/spark-1.3.0
  Version: 1.3.0 (meets minimum requirement of 1.3.0)

Storage Backend Connections
  Verifying Meta Data Backend
  Verifying Model Data Backend
  Verifying Event Data Backend
  Test write Event Store (App Id 0)
[INFO] [HBLEvents] The table predictionio_eventdata:events_0 doesn't exist yet. Creating now...
[INFO] [HBLEvents] Removing table predictionio_eventdata:events_0...

(sleeping 5 seconds for all messages to show up...)
Your system is all ready to go.

Perintah di atas memerika apakah komponen yang dipakai oleh PredictionIO semuanya sudah siap dipakai. Terlihat bahwa PredictionIO memakai Apache Spark untuk mengerjakan algoritma secara paralel dan scalable. Selain itu, data yang dikumpulkan akan disimpan ke dalam Apache HBase. Ini adalah sebuah database No-SQL yang dirancang untuk dipakai pada HDFS (Hadoop File System).

Langkah awal pada machine learning adalah mengumpulkan data sebanyak mungkin untuk keperluan training. PredictionIO akan membuat sebuah web service server berbasis REST yang dapat dipakai oleh aplikasi web untuk memberikan data. Untuk itu, saya perlu memperoleh sebuah access key terlebih dahulu dengan memberikan perintah:

$ pio app new LatihanWeb
[INFO] [HBLEvents] The table predictionio_eventdata:events_2 doesn't exist yet. Creating now...
[INFO] [App$] Initialized Event Store for this app ID: 2.
[INFO] [App$] Created new app:
[INFO] [App$]       Name: LatihanWeb
[INFO] [App$]         ID: 2
[INFO] [App$] Access Key: A7iLxxkf7RGBpiTtxMGu8zi4EZ0Kfcox05HTwYDCteCn5weqtdnoGyEaRCRYX2CM

Bagian yang paling penting dari output di atas adalah access key yang perlu saya berikan saat memanggil event server yang diciptakan oleh PredictionIO. Untuk memastikan event server berjalan dengan baik, saya bisa mengakses URL seperti http://192.168.10.100:7070 dari browser dimana 192.168.10.100 adalah IP server yang menjalankan PredictionIO. Saya akan memperoleh hasil seperti {"status":"alive"}. Bila memakai terminal, saya juga bisa menggunakan perintah curl seperti berikut ini:

$ curl 192.168.10.100:7070
{"status":"alive"}

Sekarang, saya siap untuk mengirimkan event kepada event server. Sebagai latihan, saya akan memakai access logs untuk web distribution dari Amazon CloudFront milik sebuah situs. Saya hanya tertarik pada kolom ke-12 (cs-uri-query) dimana saya mengambil nilai parameter item_id yang mewakili item yang dilihat pengguna dan visitor_id yang mewakili pengenal unik untuk pengguna tersebut. Engine complementary purchase yang saya pakai secara default mendukung event buy yang membutuhkan parameter berupa user dan item. Agar sederhana, saya tidak akan melakukan perubahan kode program dan menganggap event buy tersebut sama seperti view. Saya kemudian membuat sebuah script Perl untuk membaca dan mengakses event server seperti berikut ini:

#!/usr/bin/perl

$LOGFILE = "access.log";
open(LOGFILE) or die("Tidak dapat membaca $LOGFILE");
while () {
  next if 1..2;
  $uri = (split(' ', $_))[11];
  if ($uri =~ m/item_id%253D(\w+)%2526/) {
    $itemId = $1;
  } else {
    print "Tidak dapat menemukan item_id di $_\n";
  }
  if ($uri =~ m/visitor_id%253D([\w-]+)&/) {
    $browserId = $1;
  } else {
    print "Tidak dapat menemukan visitor_id di $_\n";
  }
  ($postEvent = <<"CURL") =~ s/\n+//gm;
curl -i -X POST http://192.168.10.100:7070/events.json?accessKey=A7iLxxkf7RGBpiTtxMGu8zi4EZ0Kfcox05HTwYDCteCn5weqtdnoGyEaRCRYX2CM \
-H "Content-Type: application/json" \
-d '{
  "event": "buy",
  "entityType": "user",
  "entityId": "$browserId",
  "targetEntityType": "item",
  "targetEntityId": "$itemId"
}'
CURL
  system($postEvent);
}

Script di atas pada dasarnya akan menggunakan curl untuk menambahkan event pada event server. Seluruh event yang terkumpul akan diletakkan pada database Apache HBase. Untuk melihat apakah event sudah tersimpan dengan baik, saya bisa membuka URL seperti http://192.168.10.100:7070/events.json?accessKey=A7iLxxkf7RGBpiTtxMGu8zi4EZ0Kfcox05HTwYDCteCn5weqtdnoGyEaRCRYX2CM melalui browser.

Bila selama percobaan, event yang diberikan tidak benar atau perlu dihapus, saya bisa menggunakan perintah seperti berikut ini:

$ pio app data-delete LatihanWeb
[INFO] [App$] Data of the following app (default channel only) will be deleted. Are you sure?
[INFO] [App$]     App Name: LatihanWeb
[INFO] [App$]       App ID: 2
[INFO] [App$]  Description: None
Enter 'YES' to proceed: YES
[INFO] [HBLEvents] Removing table predictionio_eventdata:events_2...
[INFO] [App$] Removed Event Store for this app ID: 2
[INFO] [HBLEvents] The table predictionio_eventdata:events_2 doesn't exist yet. Creating now...
[INFO] [App$] Initialized Event Store for this app ID: 2.
[INFO] [App$] Done.

Setelah event terkumpul, sekarang saatnya untuk menjalankan engine. Tapi sebelumnya, saya perlu membuka file engine.json dan menguna nilai appName menjadi latihanWeb (sesuai dengan nama yang sama berikan saat mengerjakan pio app new). Setelah itu, saya memberikan perintah berikut ini:

$ pio build --verbose
...
[INFO] [Console$] Your engine is ready for training.

Perintah di atas akan men-download dependency Ivy yang dibutuhkan (dengan menggunakan Scala sbt, sejenis Gradle di Groovy) dan men-compile kode program yang sebelumnya dihasilkan oleh engine template. Setelah proses building selesai, langkah berikutnya adalah melakukan training untuk predictive model dengan memberikan perintah ini:

$ pio train
...
[INFO] [CoreWorkflow$] Training completed successfully.

Perintah ini akan menjalankan proses training di Apache Spark. Bila terdapat kesalahan yang berkaitan dengan java.lang.StackOverflowError, maka stack size bisa ditingkatkan menjadi lebih besar, misalnya 16 MB dengan membuat environment variable bernama SPARK_JAVA_OPTS yang memiliki isi seperti -Xss16M.

Setelah proses training selesai, saya bisa men-deploy engine agar hasilnya dapat diakses melalui REST. Sebagai contoh, saya memberikan perintah berikut ini:

$ pio deploy
...
[INFO] [MasterActor] Bind successful. Ready to serve.

Untuk memastikan engine sudah berjalan dengan baik, saya bisa mencoba mengakses URL seperti http://192.168.10.100:8000. Bila hasilnya muncul seperti harapan, maka saya siap memodifikasi aplikasi web untuk mengakses engine ini.

Untuk mendapatkan rekomendasi produk sejenis, server web (atau server lain yang membutuhkan) perlu mengirimkan JSON yang mengandung sebuah id item di key "items" dan jumlah rekomendasi yang dibutuhkan di key "num". Sebagai contoh, saya bisa mengakses engine seperti berikut ini:

$ curl -H "Content-Type: application/json" -d '{"items": ["xxx"], "num": 3}' http://192.168.10.100:8000/queries.json
{
  "rules": [
    {
     "cond": ["xxx"], 
     "itemScores":[
        {"item":"hasil1", "support":3.92541707556427E-4, "confidence":0.166666,"lift":424.583333},
        {"item":"hasil2","support":3.925417075564279E-4,"confidence":0.166666,"lift":424.58333333},
        {"item":"hasil3", "support":3.925417075564279E-4,"confidence":0.1666666,"lift":424.58333333}]
    }
  ]
}

Sebagai contoh, pada hasil yang saya peroleh, 3 rekomendasi untuk sebuah produk iPhone 5s adalah produk ZenFone 2, iPhone 5s dari penjual berbeda, dan iPhone 6. Hasil ini cukup masuk akal secara sekilas. Yang perlu diperhatikan adalah hasil ini tidak menyertakan riwayat produk yang sudah pernah dilihat oleh user tersebut, melainkan rekomendasi produk serupa berdasarkan pola kunjungan pengguna lain.

Bila mencoba dengan data dengan variasi kunjungan yang terbatas, misalnya snapshot untuk beberapa menit dimana masing-masing item barang hanya dikunjungi satu dua kali oleh pengguna, saya bisa mengubah parameter algoritma di engine.json menjadi seperti berikut ini:

"algorithms": [
    {
      "name": "algo",
      "params": {
        "basketWindow" : 300,
        "maxRuleLength" : 2,
        "minSupport": 0,
        "minConfidence": 0,
        "minLift" : 0,
        "minBasketSize" : 2,
        "maxNumRulesPerCond": 5
      }
    }
]

Mengubah nilai minSupport, minConfidence dan minLift menjadi 0 akan mengurangi kualitas hasil yang diperoleh. Sebagai contoh, nilai default untuk minSupport adalah 0.1 yang berarti item harus muncul minimal 10% untuk seluruh transaksi agar ia disertakan pada hasil. Oleh sebab itu, pengaturan seperti ini sebaiknya tidak dipakai pada kasus nyata.

Memakai Temporal Pattern Di Aplikasi Inventory

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

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

Class `Periode`

Class `Periode`

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

Temporal patterns dalam bentuk abstract class

Temporal patterns dalam bentuk abstract class

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

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

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

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

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

Penerapan pada stok produk

Penerapan pada stok produk

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

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

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

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

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

Penerapan pada kas

Penerapan pada kas

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

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

Belajar Menerapkan Domain Driven Design Pada Aplikasi Inventory

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

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

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

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

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

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

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

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

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

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

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

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

Contoh event listener

Contoh event listener

Implementasinya pada kode program berupa:

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

    KasRepository kasRepository
    KategoriKasRepository kategoriKasRepository
    JenisTransaksiKasRepository jenisTransaksiKasRepository

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

}

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

Contoh penggunaan domain event

Contoh penggunaan domain event

Implementasi pada kode program akan terlihat seperti:

@DomainClass @Entity
class PencairanPoinTukarUang extends PencairanPoin {

    public PencairanPoinTukarUang() {}

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

    @Override
    boolean valid() {
        true
    }

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

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

}

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

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

Contoh aggregate

Contoh aggregate

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

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

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

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

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

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

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

Class `Konsumen`

Class `Konsumen`

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

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

Contoh service

Contoh service

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

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

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

Menerapkan State Pattern Untuk Workflow Sederhana

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

Rancangan awal

Rancangan awal

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

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

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

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

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

Rancangan class yang mewakili state

Rancangan class yang mewakili state

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

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

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

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

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

Rancangan untuk class context

Rancangan untuk class context

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

Implementasi pada class FakturJual bisa berupa kode program berikut ini:

abstract OperasiFakturJual getOperasiFakturJual()

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

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

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

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

Contoh implementasi untuk method getOperasiFakturJual() bisa berupa:

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

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

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

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

class FakturJualOlehSalesSingkatLunas extends FakturJualOlehSalesLunas {

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

}

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

Mendeteksi sensor oleh ISP dengan OONI

Salah satu indikasi adanya diktator adalah sensor terhadap informasi. Dengan melakukan pembodohan seperti menghalangi pendidikan dan akses terhadap informasi, diktator berharap dapat mempertahankan posisinya. Diktator juga cenderung melakukan penyadapan karena ia ingin segalanya berada dalam kendali. Ia perlu melakukan semua ini karena ia tidak akan sanggup bertahan pada persaingan yang bebas, jujur dan adil.

Open Observatory of Network Interference (https://ooni.torproject.org/) adalah sebuah global observation network yang dikembangkan untuk mendeteksi sensor dan penyadapan yang dilakukan oleh ISP. Pengguna melakukan serangkaian pengujian melalui ooniprobe yang menghubungi server ooni-backend. Semakin banyak jumlah pengguna dan semakin sering pengguna melakukan pengujian, maka peneliti jaringan dapat semakin memastikan praktek sensor dan penyadapan yang dilakukan oleh ISP atau pihak lainnya. Laporan hasil pengujian dapat diakses secara terbuka oleh siapa saja di https://ooni.torproject.org/reports/0.1/ (dikelompokkan berdasarkan negara).

Saya akan mulai dengan melakukan instalasi ooniprobe pada Debian Live (yang merupakan versi stable bukan testing). Pada saat tulisan ini dibuat, versi terbaru ooniprobe di wheezy-backport adalah 1.1.1-1. Saya dapat menemukan versi yang lebih baru di PyPI yang sudah mencapai 1.2.2. Oleh sebab itu, saya akan mencoba men-install ooniprobe yang ada di PyPI. Sebelumnya, saya perlu men-download beberapa package yang dibutuhkan terlebih dahulu dahulu dengan membuka root terminal dan memberikan perintah berikut ini:

# apt-get update && apt-get install python-dev python-pip python-dumbnet libpcap libgeoip-dev libffi-dev tor

Setelah ini, saya dapat men-download ooniprobe di PyPI melalui pip dengan perintah seperti berikut ini:

# pip install ooniprobe

Setelah proses instalasi selesai, saya dapat melihat apa saja pengujian yang dapat dilakukan oleh ooniprobe dengan memberikan perintah berikut ini:

# ooniprobe -s

Perintah di atas juga akan membuat file konfigurasi ooniprobe.conf di lokasi ~/.ooni.

Sebelum memulai pengujian, saya akan men-download resources terbaru dengan memberikan perintah berikut ini:

# ooniresources --update-inputs --update-geoip

Pengujian yang dilakukan oleh OONI dideskripsikan dalam file yang disebut sebagai deck. Dengan demikian, sebuah deck adalah kumpulan dari net test. Saya dapat melihat deck bawaan di di lokasi /usr/share/ooni/decks:

# ls /usr/share/ooni/decks
complete.deck  complete_no_root.deck  fast.deck  fast_no_root.deck  mlab.deck

Saya juga membuat deck khusus untuk mewakili negara saya dengan memberikan perintah berikut ini:

# oonideckgen
Unable to lookup the probe IP via traceroute
Looking up your IP address via torproject
Found your IP via a GeoIP service: x.x.x.x
Deck written to /home/user/deck-id/0.0.1-id.user.deck
Run ooniprobe like so:
ooniprobe -i /home/user/deck-id/0.0.1-id-user.deck

Perintah di atas akan menghasilkan folder dengan nama deck-id. id adalah kode negara untuk Indonesia berdasarkan standar ISO 3166-2. oonideckgen memperoleh informasi mengenai negara saya melalui layanan GeoIP.

Sesuai dengan output perintah oonideckgen, saya dapat memulai pengujian dengan memberikan perintah berikut ini:

# ooniprobe -i /home/user/deck-id/0.0.1-id-user.deck

Setelah pengujian selesai dilakukan, ooniprobe akan menghasil file yaml yang berisi informasi hasil untuk masing-masing net test. File ini akan dikirim ke server laporan sehingga dapat diakses oleh publik di lokasi https://ooni.torproject.org/reports/.

Pengujian blocking/dns_consistency akan mendeteksi sensor yang dilakukan melalui DNS. Ini adalah metode sensor yang sederhana dimana ISP mengalihkan request ke URL yang sah menjadi ke lokasi yang berbeda. Pengujian ini bisa menghasilkan false positive karena website modern biasanya memiliki load-balancing yang akan mengalihkan request ke server terdekat sesuai dengan wilayah geografis.

Pengujian blocking/http_requests akan mendeteksi sensor yang dilakukan secara MITM (yang masuk dalam kategori penyadapan). Pengujian ini akan membandingkan request HTTP normal dari komputer dan request HTTP melalui jaringan Tor. Bila blocking/http_requests mendeteksi perbedaan, ia akan menampilkan pesan seperti:

The two body lengths appear to not match
cencorship could be happening
Headers appear to *not* match

Jaringan Tor adalah jaringan ‘bawang’ dimana saya tidak menghubungi server tujuan secara langsung melainkan ‘berputar-putar’ terlebih dahulu ke sesama pengguna Tor. Tujuannya adalah agar ISP tidak mengetahui destinasi pengguna yang sesungguhnya. Kemungkinan sensor dari ISP juga menjadi sangat kecil karena request HTTP versi Tor akan dilakukan oleh komputer lain yang memakai ISP berbeda (sama seperti pada VPN!). Tentu saja ISP lokal masih memiliki peluang melakukan sensor konten bila mereka berhasil memecahkan enkripsi paket yang dilakukan oleh Tor atau men-install backdoor langsung pada komputer pengguna.

Pengujian manipulation/http_invalid_request_line akan berusaha mengirimkan request ilegal dengan harapan proxy penyadap mengembalikan pesan kesalahan. Pada laporan yaml untuk http_invalid_request_line, saya dapat membandingkan nilai received dan send. Bila berbeda, tampering akan bernilai true. Sebagai contoh, pada pengujian yang saya lakukan untuk sebuah ISP lokal, terlihat secara jelas bahwa nilai received berbeda dengan send. Ini menunjukkan bahwa ISP lokal tersebut memiliki proxy yang ‘menyadap’ komunikasi saya.

Pengujian manipulation/http_header_field_manipulation memeriksa apakah header yang dikirim sama persis dengan header yang diterima oleh server; aktifitas penyadapan bisa saja menyebabkan perubahan http header. Sebagai contoh, pada laporan yaml untuk http_header_field_manipulation, saya menemukan bahwa ISP lokal (atau bisa juga router dan modem) yang saya pakai membuang header acCepT-EncODINg karena HTTP header tersebut tidak sampai di server tujuan. Bila dilihat secara positif, ia membuat pengguna terhindar dari serangan seperti CRIME dan BREACH. Bila dilihat secara negatif, ia membuat akses Internet semakin lambat dan konten lebih mudah disadap.

Menerapkan MVC di Swing Dengan Griffon

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

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

Screen yang hendak dibuat

Screen yang hendak dibuat

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

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

class ItemStokModel {

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

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

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


}

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

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

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

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

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

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

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

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

Binding dari view ke model

Binding dari view ke model

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

Binding dari model ke view

Binding dari model ke view

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

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

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

Perubahan view secara otomatis melalui binding

Perubahan view secara otomatis melalui binding

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

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

Kode program controller yang saya buat terlihat seperti berikut ini:

class ItemStokController {

    def model
    def view

    ProdukRepository produkRepository

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

    def cari = {
        ...
    }

    def tutup = {
        ...
    }

}

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

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

Belajar Memakai Object Di OOP

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

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

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

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

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

UML Object Diagram yang menggambarkan object yang terbentuk

UML Object Diagram yang menggambarkan object yang terbentuk

Tampilan di program akan terlihat seperti pada gambar berikut ini:

Contoh tampilan program

Contoh tampilan program

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

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

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

Object yang terbentuk di memori setelah tab kedua dibuat

Object yang terbentuk di memori setelah tab kedua dibuat

Tampilan di program akan terlihat seperti pada gambar berikut ini:

Contoh tampilan program

Contoh tampilan program

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

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

Belajar Memakai Git Rebase

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

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

Kondisi awal riwayat commit

Kondisi awal riwayat commit

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

Membuat branch baru dari master

Membuat branch baru dari master

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

Melakukan perubahan pada branch lama

Melakukan perubahan pada branch lama

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

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

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

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

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

$ vi application.properties

$ git add application.properties

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

Sekarang, riwayat akan terlihat seperti pada gambar berikut ini:

Riwayat commit setelah rebase

Riwayat commit setelah rebase

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

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

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

$ vi application.properties

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

Hasil dari merge adalah history seperti pada gambar berikut ini:

Riwayat commit setelah merge

Riwayat commit setelah merge

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

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

Kondisi awal riwayat history

Kondisi awal riwayat history

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

$ git rebase -i HEAD~10

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

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

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

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

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

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

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

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

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

$ git reflog

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

$ git reset --hard HEAD@{1}

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

Belajar Menerapkan Object Oriented Programming (OOP)

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

Contoh Flowchart

Contoh Flowchart

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

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

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

Contoh UML Class Diagram

Contoh UML Class Diagram

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

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

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

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

public class Produk {
  String nama;
  long hargaEceran;
}

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

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

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

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

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

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

Contoh UML Sequence Diagram

Contoh UML Sequence Diagram

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

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

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

Menyimpan Ke Database

Menyimpan Ke Database

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

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

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

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

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

Memakai Repository

Memakai Repository

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

Sequence Diagram Setelah Penggunaan Repository

Sequence Diagram Setelah Penggunaan Repository

Kode program yang memakai class akan terlihat seperti:

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

Kesimpulan:

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

Memakai DNSCrypt Untuk Menghindari DNS Injection

Selama ini saya tidak pernah memakai DNS server dari ISP, melainkan selalu memakai DNS server dari pihak ketiga. Alasannya adalah DNS server dari ISP sangat lambat dan tidak aman. Semua berjalan dengan lancar sampai ketika beberapa hari yang lalu, saya menemukan banyak masalah pada jaringan saya. Kode program yang menulis log kesalahan ke server chat Slack tiba-tiba mengeluh server tidak ditemukan. Saya juga mulai kesulitan mengakses server GitHub (terkadang sukses tapi terkadang gagal). Seperti masalah jaringan lain pada umumnya, saya mulai melakukan troubleshooting dengan memantau packet melalui Wireshark. Hasilnya, saya menemukan respon dari server milik ISP padahal saya tidak mengaksesnya.

Mengapa demikian? Banyak kemungkinan yang bisa saja terjadi di infrastruktur ISP (atau rahasia lainnya yang tidak saya ketahui karena saya bukan pegawai disitu). ISP bisa saja memakai transparant proxy yang melakukan filtering pada seluruh data yang keluar masuk. Tapi, saya akan mulai dengan memeriksa sesuatu yang lebih masuk akal, misalnya memeriksa DNS aktual yang saya pakai melalui situs https://www.dnsleaktest.com. Saya cukup terkejut ketika mendapati laporan bahwa DNS server yang dipakai adalah DNS server milik ISP. Loh, bukankah saya sudah memakai DNS server milik pihak ketiga? Mungkin sekali ISP telah mengubah setiap request DNS pada port UDP 53 menjadi merujuk ke server DNS milik mereka. Jadi, tidak peduli apapun DNS server yang saya tentukan di router, ISP secara diam-diam akan ‘menggantinya’ ke server DNS yang berbeda. Teknik ini sering kali disebut DNS Injection yang diimplementasikan dengan memakai Transparant DNS Proxy.

Salah satu cara untuk menghindari DNS Injection adalah dengan memakai DNSCrypt yang dikembangkan oleh OpenDNS. DNSCrypt memakai protokol DNSCurve yang ditujukan untuk menggantikan protokol DNS (yang sampai sekarang masih dipakai dimana-mana). Protokol ini lebih aman karena request DNS di-enkripsi sehingga upaya untuk memodifikasi respon dari server akan lebih sulit. Saat ini OpenDNS adalah penyedia terbesar untuk layanan DNS yang mendukung DNSCurve.

Saya segera membuka halaman http://dnscrypt.org/dnscrypt-proxy/download untuk men-download source DNSCrypt. Karena memakai sistem operasi Linux UBuntu, saya men-download file dnscrypt-proxy-1.4.0.tar.gz dan men-extract-nya.

DNSCrypt bergantung pada libsodium. Oleh sebab itu, saya perlu men-download source terbaru library tersebut di https://github.com/jedisct1/libsodium/releases. Setelah men-extract-nya, saya akan men-install library tersebut dengan memberikan perintah berikut ini:

# sudo ./configure
# sudo make
# sudo make install
# sudo ldconfig

Untuk memastikan libsodium telah ter-install, saya dapat memeriksanya dengan perintah berikut ini:

# sudo ldconfig -p | grep libsodium

Berikutnya, saya akan berpindah ke lokasi source code DNSCrypt dan men-install-nya dengan memberikan perintah berikut ini:

# sudo ./configure
# sudo make
# sudo make install

Binary DNSCrypt secara otomatis akan ter-install pada lokasi /usr/local/sbin. Untuk menguji apakah DNSCrypt dapat bekerja dengan baik, saya memberikan perintah berikut ini:

# dnscrypt-proxy --resolver-name=opendns --test=0

Agar lebih aman, saya dapat memakai parameter --user sehingga dnscrypt-proxy melakukan chroot() ke user dengan hak akses yang lebih terbatas. Untuk membuat user baru, saya dapat menggunakan perintah berikut ini:

# sudo adduser --disabled-login --no-create-home --system dnscrypt

Untuk menjalankan DNSCrypt sebagai user baru tersebut, saya dapat menggunakan perintah:

# sudo dnscrypt-proxy --resolver-name=opendns --user=dnscrypt --test=0

Berikutnya, saya ingin DNSCrypt dijalankan secara otomatis. Pada Linux UBuntu yang memakai Upstart, saya bisa menambah file baru di /etc/init dengan nama seperti dnscrypt-proxy.conf yang isinya adalah:

# DNSCrypt Proxy

description "DNSCrypt proxy using OpenDNS resolver"

start on net-device-up

exec /usr/local/sbin/dnscrypt-proxy --resolver-name=opendns --user=dnscrypt

respawn

Penggunaan respawn pada script Upstart di atas menyebabkan DNSCrypt proxy akan dijalankan ulang secara otomatis bila terjadi service tersebut mengalami kegagalan.

Sekarang, bila saya me-restart komputer, DNSCrypt akan tetap dijalankan secara otomatis. Saya bisa memeriksanya dengan menggunakan perintah:

# sudo status dnscrypt-proxy
dnscrypt-proxy start/running, process 199

Setelah DNSCrypt dijalankan, ia akan menyediakan layanan DNS biasa pada port 53. Saya perlu men-konfigurasi jaringan agar tidak lagi mengakses server DNS milik siapapun lagi karena pada akhirnya akan dialihkan ke server milik ISP. Saya perlu men-konfigurasi jaringan agar mengakses DNS di 127.0.0.l (localhost) pada port 53. Setelah menerima request di port ini, DNSCrypt akan melakukan koneksi ter-enkripsi ke port 443 milik server OpenDNS dan mengembalikan hasilnya ke pengguna.

Tapi ada satu masalah yang perlu saya selesaikan terlebih dahulu.

Pada versi Ubuntu baru, saya menemukan bahwa Dnsmasq akan dijalankan secara otomatis. Dnsmasq juga akan memakai port 53 pada 127.0.0.1 sebagai layanan DNS. Untuk itu, saya perlu mematikan layanan Dnsmasq karena tidak dibutuhkan lagi. Caranya adalah dengan mengubah isi file /etc/NetworkManager/NetworkManager.conf dan memberikan komentar pada baris berikut ini:

# dns=dnsmasq

Sebagai langkah terakhir, saya perlu mengatur masing-masing jaringan agar memakai DNS server pada lokasi 127.0.0.1, seperti pada gambar berikut ini:

Mengatur DNS untuk jaringan di Linux Ubuntu

Mengatur DNS untuk jaringan di Linux Ubuntu

Setelah me-restart jaringan, DNS injection dari ISP tidak lagi bekerja. Saya bisa memastikan diri memakai DNS resolver dari OpenDNS dan bukan milik ISP dengan membuka halaman http://www.opendns.com/welcome. Bila memakai DNS server dari OpenDNS, akan terlihat tanda centang besar.