Belajar menilai seberapa sempurna rancangan REST API

Membuat API berbasis REST pada dasarnya adalah sesuatu yang sangat mudah: buat sebuah method di controller dan kembalikan hasil berupa JSON. Selesai! Yup, cukup sampai disini bagi programmer yang sedang belajar atau membuat proyek jangka pendek yang tidak ingin berkembang. Pada tulisan kali ini, saya akan melihat dari sisi arsitektur, dimana REST API tersebut akan dipakai dalam jangka lama dan mungkin akan dipanggil oleh klien baru yang belum terpikirkan saat ini.

Salah satu cara untuk menilai implementasi REST API adalah dengan membandingkannya dengan Richardson Maturity Model. Gambar berikut ini memperlihatkan perkembangan REST menurut model tersebut:

Richardson Maturity Model

Richardson Maturity Model

Pada Level 0, REST API dianggap sebagai API jarak jauh yang dipanggil melalui Web (Http) dan mengembalikan JSON. Tidak lebih dari sekedar pengganti function atau method (yang dikerjakan jarak jauh)! Sebagai contoh, API berikut ini masuk dalam kategori Level 0:

POST http://api.server.com/buatProduk
{
   nama: "produk1",
   harga: 1000
}

POST http://api.server.com/hapusProduk
{
   id: 1
}

POST http://api.server.com/daftarPelanggan
{
   email: "ocelat@dd.com",
   nama: "ocelot"
}

POST http://api.server.com/hapusPelanggan
{
   id: 1
}

Pada Level 1, terdapat konsep resource. Setiap API merupakan operasi yang akan melakukan sesuatu pada resource yang ada. Sebagai contoh, API berikut ini termasuk dalam kategori Level 1:

POST http://api.server.com/produk/buat
{
   nama: "produk1",
   harga: 1000
}

POST http://api.server.com/produk/hapus
{
   id: 1
}

POST http://api.server.com/pelanggan/daftar
{
   email: "ocelat@dd.com",
   nama: "ocelot"
}

POST http://api.server.com/pelanggan/hapus
{
   id: 1
}

Pada contoh di atas, setiap API masih dipanggil dengan request POST yang populer dari dulu. Biasanya, GET dipakai bila kita tidak ingin mengirim data tambahan seperti JSON. Bila ingin mengirimkan JSON atau mengirim data dalam jumlah besar, POST akan digunakan. Padahal selain GET dan POST, HTTP juga memiliki request method lainnya seperti HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT, dan PATCH.

Sebuah request yang sukses akan mengembalikan respon 200. Selain respon 200, HTTP juga memiliki definisi response status code yang lain seperti 500 untuk menandakan kesalahan, 201 untuk menandakan resource berhasil dibuat, dan sebagainya.

Pada Level 2, REST API akan memanfaatkan HTTP request method dan response status code. Sebagai contoh, API berikut ini masuk dalam kategori Level 2:

POST http://api.server.com/produk
{
   nama: "produk1",
   harga: 1000
}

PUT http://api.server.com/produk/1
{
   nama: "produk1 dengan nama baru",
   harga: 2000
}

DELETE http://api.server.com/produk/1

POST http://api.server.com/pelanggan
{
   email: "ocelat@dd.com",
   nama: "ocelot"
}

DELETE http://api.server.com/pelanggan/1

Pada Level 3, REST API akan menggunakan HATEOAS. Sebagai contoh, saya membuat REST API yang mendukung HATEOAS di artikel Membuat RESTFul Web Service Dengan Spring Data REST. Kenapa memakai HATEOAS? Beberapa keuntungan yang ditawarkannya antara lain:

  1. Klien tidak perlu men-hardcode link untuk melakukan operasi terhadap resource yang diterima.  Dengan demikian, perubahan nama atau lokasi API tidak menimbulkan perubahan di kode program klien.
  2. Klien dapat mengetahui operasi apa yang diperbolehkan dan tidak diperbolehkan terhadap resource.   Sebagai contoh, pada UI yang berisi daftar produk, bila tidak ada link untuk menghapus produk yang dikembalikan bersamaan dengan produk tersebut (misalnya karena user yang login tidak berhak untuk operasi tersebut), maka saya bisa menyembunyikan tombol hapus.

Sayangnya, hingga saat ini tidak ada implementasi yang standar untuk HATEOAS.  Salah satu yang menjanjikan adalah HAL (Hypertext Application Language).  Daftar library yang mengimplementasikan HAL dapat dijumpai di https://github.com/mikekelly/hal_specification/wiki/Libraries.

REST API dari Paypal dan Netflix adalah contoh API yang sudah mendukung HATEOAS.  Sementara itu, kebanyakan penyedia REST API publik lainnya hanya berada pada Level 2 karena mereka berfokus pada kemudahan penggunaan (sementara HATEOAS tidak begitu populer).   Perlu diingat ini bahwa mereka adalah API publik yang ditujukan untuk dikonsumsi oleh developer luar dari berbagai kalangan.  Fokus ini tentu akan berbeda, misalnya, pada saat merancang arsitektur microservices untuk dipakai dalam perusahaan.

Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama

Salah satu pertanyaan yang sering muncul pada saat akan memulai proyek baru dari awal adalah apakah kode program front end dan back end harus dipisahkan menjadi dua proyek yang berbeda? Proyek front end umumnya adalah proyek berbasis JavaScript yang menggunakan framework seperti Angular, Vue.js, React, dan sebagainya. Build tool yang umumnya digunakan oleh proyek front end meliputi Grunt, Yarn, Webpack, npm, dan sejenisnya. Sementara itu, proyek back end umumnya ditulis dalam Java, PHP, C#, dan sebagainya. Build tool yang umum meliputi Gradle, Ant, Composer, CMake, dan sebagainya. Karena dunia keduanya yang berbeda jauh, apakah pemisahan proyek secara fisik selalu merupakan pilihan yang tepat?

Tidak selamanya front end dan back end harus dipisahkan menjadi dua proyek berbeda. Bila anggota tim lebih banyak yang full stack, pemisahan proyek justru malah akan merepotkan. Metodologi pengembangan software seperti Scrum, misalnya, memiliki unit pekerjaan berupa story seperti “pelanggan bisa mendaftar baru” dan “pelanggan bisa melihat laporan laba rugi per tahun”. Agar bisa menyelesaikan story yang sedang dikerjakannya, developer umumnya harus mengubah front end dan juga back end.

Beberapa framework yang dogmatis mempermudah penggabungan kode program front end dan back end pada satu proyek yang sama. Sebagai contoh, Laravel 5.4 yang dilengkapi dengan Laravel Mix (berbasis Webpack) sangat mempermudah penggunaan front end Vue.js di dalam satu proyek Laravel yang sama. Lalu bagaimana dengan framework di dunia Java seperti Spring Boot? Walaupun tidak semudah di Laravel, saya akan mencoba membuat proyek Spring Boot (back end) + Webpack + Vue.js (front end) pada artikel ini.

Saya akan mulai dengan membuat sebuah proyek Spring Boot baru melalui Spring Initializr (https://start.spring.io). Setelah menjalankan IntelliJ IDEA, saya segera memilih New, Project…. Setelah memilih Spring Initializr, saya men-klik tombol Next. Karena saya ingin menggunakan Gradle, saya mengisi dialog yang muncul seperti yang terlihat pada gambar berikut ini:

Membuat Proyek Spring Boot Baru

Membuat Proyek Spring Boot Baru

Pada langkah berikutnya, saya memilih dependency yang diperlukan. Karena ini merupakan sebuah percobaan, saya hanya memberikan tanda centang pada Web. Setelah men-klik Next, saya bisa mengisi nama proyek dan lokasi penyimpanannya. Setelah men-klik Finish, IDEA akan men-download proyek dan menampilkan sebuah proyek Spring Boot yang masih kosong seperti yang terlihat pada gambar berikut ini:

Struktur Proyek Spring Boot Yang Baru Dibuat

Struktur Proyek Spring Boot Yang Baru Dibuat

Seandainya saja saya menggunakan front end yang ramah terhadap Java seperti Thymeleaf dan JSF, maka saya hanya perlu meletakkan kode program mereka ke direktori /src/main/webapp. Akan tetapi, ada kalanya saya perlu menggunakan front end yang lebih berat seperti Angular dan Vue.js. Keuntungannya adalah saya bisa memanfaatkan teknologi yang umum dipakai programmer front end seperti npm, Yarn, dan Webpack. Walaupun pada dunia Java sudah ada upaya menjadikan library front end ke dalam apa yang disebut sebagai Webjars, saya tetap merasa penggunaan npm lebih alami bagi pencinta JavaScript.

Bagaimana cara memasukkan proyek Vue.js ke dalam proyek Spring Boot yang baru saja dibuat ini? Cara yang paling adalah dengan menggunakan Vue Cli (https://github.com/vuejs/vue-cli). Jika ini adalah pertamakalinya saya menggunakan Vue Cli, saya perlu men-install-nya dengan memberikan perintah berikut ini:

$ npm install -g vue-cli

Setelah itu, masih berada di direktori proyek Spring Boot, saya memberikan perintah berikut ini:

$ vue init webpack frontend

Vue Cli akan menghasilkan sebuah proyek kosong untuk Vue.js berbasis Webpack seperti yang terlihat pada gambar berikut ini:

Proyek Vue.js di dalam proyek Spring Boot

Proyek Vue.js di dalam proyek Spring Boot

Perlu diperhatikan bahwa frontend hanyalah sebuah folder biasa. Sampai disini, saya bisa saja mulai bekerja dengan Vue.js dan memberikan perintah npm pada saat berada dalam folder frontend. Walaupun demikian, akan lebih baik bila saya bisa sama-sama mengendalikan front end dan back end melalui Gradle. Berhubung Gradle mendukung multi-projects, saya akan menjadikan folder frontend sebagai sebuah sub-project.

Langkah pertama yang saya lakukan adalah membuat sebuah file baru bernama build.gradle di folder frontend dengan isi seperti berikut ini:

plugins {
  id 'com.moowork.node' version '1.2.0'
  id 'java'
}

repositories {
  mavenCentral()
}

node {
  version = '8.1.3'
  yarnVersion = '0.27.5'
  download = true
}

task runBuild(dependsOn: yarn, type: YarnTask) {
  args = ['run', 'build']
}

task runDev(dependsOn: yarn, type: YarnTask) {
  args = ['run', 'dev']
}

jar {
  from project.projectDir.absolutePath + '/dist'
  eachFile { details ->
    details.path = details.path.startsWith('META-INF')?: 'static/' + details.path
  }
  includeEmptyDirs = false
}

jar.dependsOn runBuild

Pada file di atas, saya menggunakan plugin com.moowork.node yang akan men-download Node.js dan Yarn ke dalam folder .gradle. Sebagai informasi, Yarn adalah sebuah klien npm yang masih bisa menggunakan file package.json seperti klien npm standard. Salah satu kelebihan Yarn adalah ia memiliki penyimpanan global di folder ~/.cache/yarn (sama halnya dengan Gradle yang memiliki folder ~/.gradle). Bila saya membuat proyek baru yang memakai library yang sudah di-download sebelumnya, Yarn tidak akan membutuhkan koneksi internet lagi karena ia akan memakai yang sudah tersimpan di ~/.cache/yarn (proses build pun terasa lebih cepat karena tidak perlu men-download ulang). Ini berbeda dengan cache klien npm standard (~/.npm) yang hanya sementara dan tidak permanen. Karena Yarn kompatibel dengan klien npm standard, saya bisa mengganti perintah npm run build dengan yarn run build.

Spring Boot secara otomatis akan menyajikan file yang berada di folder /public dan /static sehingga bisa diakses oleh pengguna. Oleh sebab itu, file Gradle di atas akan menyalin seluruh file yang ada di folder dist ke dalam folder static di dalam file Jar yang dihasilkan nantinya.

Salah satu masalah yang saya jumpai pada proyek Vue.js yang dihasilkan oleh vue-cli adalah build scripts yang diletakkan di folder build. Hal ini akan menimbulkan sedikit kekacauan karena folder build memiliki arti berbeda di Gradle yang akan digunakan sebagai tempat output (misalnya file Jar yang dihasilkan Gradle akan diletakkan di folder ini). Oleh sebab itu, saya mengganti nama folder build menjadi buildScripts. Tidak lupa juga, saya men-update file package.json supaya menggunakan nama folder yang baru.

Berikutnya, pada proyek utama (Spring Boot), saya menambahkan sebuah file baru bernama settings.gradle yang isinya berupa:

rootProject.name = 'latihan'
include 'frontend'

Sebagai langkah terakhir, saya menambahkan dependency ke subproyek frontend di file build.gradle milik proyek utama, seperti pada:

dependencies {
   compile project(':frontend')
   ...
}

Sekarang, bila saya melihat daftar perintah Gradle yang bisa dijalankan untuk proyek ini, saya akan menjumpai seperti yang terlihat pada gambar berikut ini:

Daftar perintah Gradle untuk proyek ini

Daftar perintah Gradle untuk proyek ini

Mari jalankan proyek ini dengan men-double click bootRun dibagian application atau dengan memberikan perintah berikut ini:

$ ./gradlew bootRun
Starting a Gradle Daemon (subsequent builds will be faster)
:frontend:compileJava NO-SOURCE
:frontend:processResources NO-SOURCE
:frontend:classes UP-TO-DATE
:frontend:nodeSetup UP-TO-DATE
:frontend:yarnSetup UP-TO-DATE
:frontend:yarn UP-TO-DATE
:frontend:runBuild
yarn run v0.27.5
$ node buildScripts/build.js

Starting to optimize CSS...
Processing static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css...
Processed static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css, before: 431, after: 363, ratio: 84.22%
Hash: 424b3c987da691d210b2ING
Version: webpack 2.6.1ECUTING
Time: 5139msrunBuild
                                                  Asset       Size  Chunks             Chunk Names
                  static/js/app.eaedbed942638ee1e5dd.js    11.7 kB       0  [emitted]  app
               static/js/vendor.4e561224e1f68ab595ac.js     107 kB       1  [emitted]  vendor
             static/js/manifest.4373dcd698dd3ea60ed7.js    1.51 kB       2  [emitted]  manifest
    static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css  363 bytes       0  [emitted]  app
              static/js/app.eaedbed942638ee1e5dd.js.map    36.7 kB       0  [emitted]  app
static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css.map  910 bytes       0  [emitted]  app
           static/js/vendor.4e561224e1f68ab595ac.js.map     857 kB       1  [emitted]  vendor
         static/js/manifest.4373dcd698dd3ea60ed7.js.map    14.6 kB       2  [emitted]  manifest
                                             index.html  445 bytes          [emitted]  

  Build complete.

  Tip: built files are meant to be served over an HTTP server.
  Opening index.html over file:// won't work.

Done in 9.22s.
:frontend:jar UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:findMainClass
:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.4.RELEASE)

2017-07-10 20:07:40.825  INFO 4098 --- [           main] com.example.latihan.LatihanApplication   : Starting LatihanApplication on desktop with PID 4098 (/home/user/IdeaProjects/latihan/build/classes/main started by user in /home/userIdeaProjects/latihan)
2017-07-10 20:07:40.828  INFO 4098 --- [           main] com.example.latihan.LatihanApplication   : No active profile set, falling back to default profiles: default
2017-07-10 20:07:40.913  INFO 4098 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5f71c76a: startup date [Mon Jul 10 20:07:40 WIB 2017]; root of context hierarchy
2017-07-10 20:07:42.200  INFO 4098 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2017-07-10 20:07:42.213  INFO 4098 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2017-07-10 20:07:42.215  INFO 4098 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.15
2017-07-10 20:07:42.364  INFO 4098 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2017-07-10 20:07:42.366  INFO 4098 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1456 ms
2017-07-10 20:07:42.478  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2017-07-10 20:07:42.776  INFO 4098 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5f71c76a: startup date [Mon Jul 10 20:07:40 WIB 2017]; root of context hierarchy
2017-07-10 20:07:42.839  INFO 4098 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2017-07-10 20:07:42.841  INFO 4098 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2017-07-10 20:07:42.867  INFO 4098 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-10 20:07:42.868  INFO 4098 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-10 20:07:42.902  INFO 4098 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-10 20:07:42.922  INFO 4098 --- [           main] oConfiguration$WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
2017-07-10 20:07:43.017  INFO 4098 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-07-10 20:07:43.074  INFO 4098 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-07-10 20:07:43.081  INFO 4098 --- [           main] com.example.latihan.LatihanApplication   : Started LatihanApplication in 2.57 seconds (JVM running for 3.034)
<============-> 92% EXECUTING
> :bootRun

Bila perintah ini dikerjakan pertama kalinya, ia akan men-download Node.js dan Yarn. Selain itu, ia juga akan men-download library di package.json yang belum ada di folder node_modules. Setelah itu, ia akan menjalankan Webpack yang secara tidak langsung bekerja melalui loader-nya. Sebagai contoh, vue-loader akan bekerja menerjemahkan komponen Vue dalam file .vue dan babel-loader akan melakukan transpiling sehingga saya bisa memakai ES2016 tanpa khawatir tidak bisa dijalankan oleh browser-browser populer. Setelah selesai, saya bisa membuka http://localhost:8080 untuk mengakses aplikasi.

Sampai disini, saya sudah membuat proyek baru yang menggabungkan kode program front end dan back end. Saya juga memakai Gradle untuk mengendalikan kode program front end (yang secara tidak langsung akan memakai Yarn dan Webpack yang dipakai oleh proyek front end).

Bagaimana dengan deployment? Bila saya memberikan perintah:

$ ./gradlew war

Saya akan memperoleh sebuah file War bernama latihan-0.0.1-SNAPSHOT.war di lokasi build/libs. File ini dapat langsung di-deploy di application server produksi (misalnya Tomcat atau JBoss). Di dalam file War ini sudah termasuk proyek front end saya yang bisa ditemukan di WEB-INF\lib\frontend.jar. Jadi, Gradle akan membuat Jar untuk front end terlebih dahulu. Setelah itu, Jar akan dipaketkan bersama-sama dengan Jar lain yang dibutuhkan oleh back end di WEB-INF\lib.

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.