Tadinya gw ga kepikiran buat bikin design pattern ini karena dulunya saat pertama kali bikin seri tentang design pattern, gw jarang memakai design pattern ini di dunia nyata, hanya tau teorinya saja. Gw baru menemukan kasus yang cocok menggunakan design pattern ini kurang lebih beberapa bulan yang lalu. Tapi minggu lalu gw liat analytics pencarian blog gw, ada yang searching keyword "Flyweight". Ternyata ada juga peminatnya😁.
Flyweight Design Pattern adalah Structural Design Pattern yang meminimalisir penggunaan memory dengan cara melakukan sharing objek terhadap objek yang sama yang pernah digunakan tanpa harus membuat ulang objek-objek tersebut.
Design Pattern
Use Case
Untuk use case nya kita pakai class yang udah disediakan Java aja. Kita ingin menggunakan object DateTimeFormatter untuk mencetak String Tanggal dalam format tertentu secara berulang.
Contoh Code
public static void main(String[] args){
LocalDate localDate = LocalDate.of(1956, Month.JANUARY, 21);
DateTimeFormatter yyyyMMddFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
String yyyyMMddStr = yyyyMMddFormatter.format(localDate);
System.out.println("yyyyMMdd = " + yyyyMMddStr); // will create new "yyyyMMdd" formatter and print 19560121
DateTimeFormatter ddMMMMyyyyFormatter = DateTimeFormatter.ofPattern("dd MMMM yyyy");
String ddMMMMyyyy = ddMMMMyyyyFormatter.format(localDate);
System.out.println("ddMMMMyyyy = " + ddMMMMyyyy); // will create new "dd MMMM yyyy" formatter and print 21 January 1956
DateTimeFormatter yyyyMMddFormatter2 = DateTimeFormatter.ofPattern("yyyyMMdd");
String yyyyMMddStr2 = yyyyMMddFormatter2.format(localDate);
System.out.println("yyyyMMdd again = " + yyyyMMddStr2); // will create new "yyyyMMdd" formatter and print 19560121 again!
}
Problem
Code di atas sudah berjalan sesuai requirement yang kita inginkan. Cuma masalahnya adalah jika format tanggal yang sama digunakan beberapa kali di berbagai tempat, method, maupun class berbeda, maka dia akan membuat object DateTimeFormatter berulang kali setiap pemakaian. Jika penggunaannya terlalu sering dengan memory minimalis, tentu ini bisa jadi masalah😵.
Solusi
Salah satu solusinya adalah dengan menggunakan Flyweight Design Pattern😎.
Class DateTimeFormatFlyweight
public enum DateTimeFormatFlyweight{
INSTANCE;
public String format(TemporalAccessor temporalAccessor, String formatPattern){
DateTimeFormatter timeFormatter = FlyweightHolder.HOLDER.computeIfAbsent(formatPattern, formatPatternKey -> DateTimeFormatter.ofPattern(formatPattern));
return timeFormatter.format(temporalAccessor);
}
private static class FlyweightHolder{
private static final Map<String, DateTimeFormatter> HOLDER = new ConcurrentHashMap<>();
}
}
Contoh penggunaan
public static void main(String[] args){
LocalDate localDate = LocalDate.of(1956, Month.JANUARY, 21);
String yyyyMMddStr = DateTimeFormatFlyweight.INSTANCE.format(localDate, "yyyyMMdd");
System.out.println("yyyyMMdd = " + yyyyMMddStr); // will create & cache "yyyyMMdd" formatter, then print 19560121
String ddMMMMyyyy = DateTimeFormatFlyweight.INSTANCE.format(localDate, "dd MMMM yyyy");
System.out.println("ddMMMMyyyy = " + ddMMMMyyyy); // will create & cache "dd MMMM yyyy" formatter, then print 21 January 1956
String yyyyMMddStr2 = DateTimeFormatFlyweight.INSTANCE.format(localDate, "yyyyMMdd");
System.out.println("yyyyMMdd = " + yyyyMMddStr2); // will reuse "yyyyMMdd" formatter from cache😎, then print 19560121
}
Kita menggunakan enum DateTimeFormatFlyweight agar instance Flyweight-nya singleton. Kita memakai inner class FlyweightHolder seperti solusi Bill Pugh yang pernah dibahas sebelumnya. Kita juga menggunakan ConcurrentHashMap untuk menampung cache agar thread-safe. Sekarang pembuatan object DateTimeFormatter udah di-cache di dalam inner class FlyweightHolder. Jadi ga setiap penggunaan harus bikin object baru lagi, akan tetapi akan melakukan pengecekan terhadap FlyweightHolder terlebih dahulu melalui method computeIfAbsent. Kalau ada, maka pakai yang dari cache tanpa bikin object baru, dan jika belum di-cache maka akan bikin object DateTimeFormatter lalu objectnya disimpan ke dalam FlyweightHolder agar nantinya jika ada format yang sama, maka akan menggunakan object yang sudah disimpan di FlyweightHolder. Object-nya jadi shareable dan bisa digunakan di berbagai tempat class maupun method. Oh ya, untuk Java 8 perlu pengecekan dulu pakai method get sebelum melakukan computeIfAbsent karena ada performance bugs. Bugs tersebut baru di-fix pada Java 9 ke atas.
Kenapa menggunakan Flyweight Design Pattern?
Flyweight Design Pattern cocok untuk optimasi memori pada aplikasi, sehingga objek tertentu bisa di-reuse dan shareable tanpa harus bikin baru setiap pemakaian. Tentu saja object tersebut harus immutable, kalau mutable justru bakal buggy. Pada contoh di atas, kebetulan DateTimeFormatter itu immutable, makanya cocok. Lain halnya jika menggunakan SimpleDateFormat, itu mutable dan jangan sekali-kali dijadikan obejct yang shareable karena ga thread-safe. Bahaya dan sangat rentan terhadap bugs. Bayangkan nantinya jika ada developer yang melakukan mutasi pattern, bisa berubah format pattern-nya saat runtime🤯. Selain itu, jaman sekarang juga ada namanya Redis, jadi cache bisa dilakukan secara external, bukan dalam aplikasi lagi. Bedanya, kalau pakai Flyweight cache hanya bisa diakses oleh aplikasi itu sendiri, sedangkan Redis bisa terhubung dengan beberapa aplikasi lain. Jika aplikasi restart, cache masih aman di dalam Redis, berbeda dengan Flyweight yang kalau aplikasi restart, cache-nya juga ikut hilang. Dan yang paling utama, kalau pakai Redis kita bisa menentukan kapan value-nya bakal expire, sedangkan kalau Flyweight Pattern butuh beberapa code lagi yang agak ribet untuk bikin kayak gitu.
Verdict
Dengan Flyweight Design Pattern kita bisa meminimalisir pemakaian memory karena beberapa object immutable bisa kita cache tanpa harus bikin object lagi di setiap penggunaan. Sekilas, Design Pattern ini ada kemiripan Singleton Design Pattern karena sama-sama global variables yang shareable ke berbagai tempat. Bedanya, kalau Singleton instance object tersebut sudah pasti hanya satu, sedangkan kalau Flyweight instance-nya bisa lebih dari satu objek. Jaman sekarang cache bisa dilakukan menggunakan aplikasi pihak ketiga seperti Redis DB dengan fitur yang lebih lengkap. Java sendiri juga menggunakan Flyweight Design Pattern pada class Wrapper, contohnya Integer. Integer Wrapper menyimpan cache angka dari -128 hingga 127. Jadi ketika kita memanggil method Integer.valueOf(1)
atau secara literal seperti Integer x = 1
, maka Java akan mengambil Integer dengan angka tersebut dari cache. Kecuali jika angka tersebut diluar range yang disebutkan tadi, maka Java akan membuat object baru. Cache tersebut tentu saja tidak berlaku jika kita membuat object Integer menggunakan 'new' keyword seperti Integer x = new Integer(1)
, karena itu sudah pasti akan membuat object baru. Begitu juga dengan Wrapper lainnya seperti Boolean, Short, Character, BigDecimal, dll juga mengimplementasi Flyweight pattern. Termasuk String, ketika kita membuat String secara literal seperti String x = "hello world"
, maka JVM hanya akan membuat object String "hello world" saat pertama kali saja. Selanjutnya akan di-cache, sehingga ketika kita membuat object String dengan value yang sama secara literal seterusnya, maka tidak akan membuat object baru setiap pemakaian, melainkan ambil dari cache (kecuali String tersebut dihasilkan dari concatenation). Makanya best practice menggunakan object Wrapper dan String adalah dengan TIDAK menggunakan 'new' keyword. Biasanya akan muncul warning dari IDE yang kita gunakan jika menggunakan 'new' keyword.