Pola desain (design pattern) adalah solusi baku yang sudah terbukti untuk masalah perancangan yang muncul berulang, lengkap dengan nama yang dipakai bersama oleh seluruh komunitas pengembang. Topik ini menerapkan lima pola yang paling sering dipakai dalam Java modern, yaitu Singleton, Factory, Builder, Observer, dan Strategy, semuanya ditulis dengan fitur terkini seperti record dan switch expression. Setiap pola dipraktikkan pada program utuh yang bisa dijalankan, sehingga yang dipelajari adalah penerapannya, bukan teorinya saja.
Pola desain sebagai kosakata desain#
Sebelum masuk ke kode, patut dipahami dulu apa yang sebenarnya dibeli dengan mempelajari pola. Kumpulan pola yang paling terkenal berasal dari buku Design Patterns yang mengenalkan dua puluh tiga pola, dan kelima pola di topik ini termasuk yang paling banyak muncul di kode nyata. Nilai utamanya ada dua sisi, yaitu struktur solusi yang sudah teruji dan kosakata untuk membicarakannya.
Sisi kosakatanya sering diremehkan padahal paling sering dipakai. Kalimat “pesan ini dikirim ke semua observer yang terdaftar” menyampaikan desain lengkap dalam satu klausa, tanpa perlu menjelaskan daftar antarmuka dan perulangan pemberitahuan. Tim yang menguasai nama pola berdiskusi lebih cepat karena tidak lagi menjelaskan mekanisme yang sama berulang-ulang.
Sisi lainnya perlu dijaga, yaitu godaan memaksa pola ke mana-mana. Pola adalah alat untuk masalah yang benar-benar ada, dan kode yang sederhana pun kadang adalah jawaban terbaik. Setiap pola di bawah diawali dengan masalah yang memicunya, sehingga penggunaannya selalu punya alasan yang jelas.
Singleton: satu objek untuk semua#
Masalah Singleton adalah memastikan sebuah kelas hanya punya satu objek di seluruh program, biasanya untuk sesuatu yang memang harus tunggal seperti konfigurasi atau penampung koneksi. Cara paling andal di Java modern adalah holder idiom, yaitu menaruh satu-satunya objek di kelas pembantu dalam yang baru dimuat JVM saat pertama kali dipakai. Inisialisasi kelas oleh JVM menjamin keamanan terhadap thread tanpa perlu synchronized sama sekali.
public class PolaSingleton {
static class Konfigurasi {
private final String bahasa;
private Konfigurasi() {
this.bahasa = "id-ID";
}
// holder idiom: JVM menginisialisasi KelasPembawa hanya saat pertama dipakai
private static class KelasPembawa {
private static final Konfigurasi EKSEMPLEN = new Konfigurasi();
}
static Konfigurasi getInstance() {
return KelasPembawa.EKSEMPLEN;
}
String bahasa() {
return bahasa;
}
}
public static void main(String[] args) {
Konfigurasi pertama = Konfigurasi.getInstance();
Konfigurasi kedua = Konfigurasi.getInstance();
System.out.println("sama objek? " + (pertama == kedua));
System.out.println("bahasa: " + pertama.bahasa());
}
}sama objek? true
bahasa: id-IDTiga kunci pada implementasi di atas layak dicatat. Konstruktor dibuat private agar tidak ada pihak luar yang bisa memanggil new, metode getInstance() selalu mengembalikan objek yang sama, dan perbandingan == pada keluaran membuktikannya. Setiap pemanggilan getInstance() pertama kali memicu pemuatan kelas KelasPembawa, sehingga objeknya juga dibuat dengan malas (lazy) tanpa pengecekan null apa pun.
Alternatif yang lebih pendek adalah enum satu konstanta, dan kebiasaan ini sudah disinggung pada topik Pendalaman Enum.
// tidak-dijalankan
public enum JamServer {
INSTANCE;
LocalDateTime sekarang() {
return LocalDateTime.now();
}
}Factory: menyerahkan keputusan pembuatan objek#
Masalah Factory adalah kode pemakai yang harus tahu terlalu banyak tentang kelas konkret mana yang harus dibuat. Pola ini memindahkan keputusan itu ke satu titik, yaitu metode pabrik yang mengembalikan objek bertipe interface, sehingga pemakai cukup menyebut kebutuhannya. Konsepnya bertumpu pada pemrograman ke interface yang sudah dibiasakan pada topik Pemrograman ke Interface.
public class PolaFactory {
interface EksporLaporan {
String susun(String judul, String isi);
}
static class EksporCsv implements EksporLaporan {
@Override
public String susun(String judul, String isi) {
return judul + ";" + isi + ";";
}
}
static class EksporTeks implements EksporLaporan {
@Override
public String susun(String judul, String isi) {
return judul + "\n" + isi + "\n";
}
}
// pabrik statis: satu-satunya tempat kelas konkret diputuskan
static EksporLaporan pilih(String jenis) {
return switch (jenis) {
case "csv" -> new EksporCsv();
case "teks" -> new EksporTeks();
default -> throw new IllegalArgumentException("jenis tak dikenal: " + jenis);
};
}
public static void main(String[] args) {
String judul = "Laporan Piket";
String isi = "Ali menyapu, Budi membuang sampah";
System.out.println("--- CSV ---");
System.out.println(pilih("csv").susun(judul, isi));
System.out.println("--- Teks ---");
System.out.println(pilih("teks").susun(judul, isi));
}
}--- CSV ---
Laporan Piket;Ali menyapu, Budi membuang sampah;
--- Teks ---
Laporan Piket
Ali menyapu, Budi membuang sampahMetode pilih memakai switch expression modern untuk mengembalikan implementasi yang sesuai, dan pemanggilnya tidak pernah menyentuh kelas EksporCsv maupun EksporTeks secara langsung. Menambah format baru berarti menambah satu kelas dan satu case, tanpa mengubah satu baris pun di kode pemakai. Pola yang sama sebenarnya sudah ditemui di library standar, karena metode seperti List.of dan Integer.parseInt adalah metode pabrik statis yang menyembunyikan detail pembuatan objeknya.
Builder: menyusun objek banyak field#
Masalah Builder muncul ketika objek membutuhkan banyak field dengan kombinasi yang bervariasi, sehingga konstruktor dengan parameter berjajar panjang sulit dipakai dan mudah tertukar. Pola ini menyediakan pembangun bertahap yang dipanggil dengan gaya berantai, dan setiap tahap menyebut nama fieldnya, sehingga tidak ada nilai yang salah tempat. Record menjadi dasar yang pas karena hasil akhirnya tetap imutabel, sebagaimana dibahas pada topik Pendalaman Record.
public class PolaBuilder {
record JadwalUjian(String pelajaran, String ruang, String pengawas, int durasiMenit) {
static Builder baru(String pelajaran) {
return new Builder(pelajaran);
}
static class Builder {
private String pelajaran;
private String ruang = "belum ditentukan";
private String pengawas = "belum ditentukan";
private int durasiMenit = 90;
Builder(String pelajaran) {
this.pelajaran = pelajaran;
}
Builder ruang(String nilai) {
this.ruang = nilai;
return this;
}
Builder pengawas(String nilai) {
this.pengawas = nilai;
return this;
}
Builder durasi(int menit) {
this.durasiMenit = menit;
return this;
}
JadwalUjian selesaikan() {
return new JadwalUjian(pelajaran, ruang, pengawas, durasiMenit);
}
}
}
public static void main(String[] args) {
JadwalUjian matematika = JadwalUjian.baru("Matematika")
.ruang("R-12")
.pengawas("Bu Citra")
.selesaikan();
JadwalUjian sejarah = JadwalUjian.baru("Sejarah")
.durasi(120)
.selesaikan();
System.out.println(matematika);
System.out.println(sejarah);
}
}JadwalUjian[pelajaran=Matematika, ruang=R-12, pengawas=Bu Citra, durasiMenit=90]
JadwalUjian[pelajaran=Sejarah, ruang=belum ditentukan, pengawas=belum ditentukan, durasiMenit=120]Builder menyimpan nilai-nilai sementara, memberi default yang masuk akal, dan metode selesaikan() membuat record yang sudah final. Pemanggilan kedua pada main membuktikan manfaatnya, karena jadwal tanpa ruang dan pengawas tetap sah dengan nilai bawaannya, tanpa perlu konstruktor kedua. Validasi pun lebih tepat diletakkan di selesaikan(), karena di sanalah semua nilai sudah terkumpul lengkap.
Observer: satu perubahan, banyak pemberitahuan#
Masalah Observer adalah satu perubahan data yang harus diketahui oleh pihak lain yang jumlahnya bisa berubah-ubah, tanpa subjeknya tahu detail pihak-pihak itu. Solusinya, subjek menyimpan daftar pengamat (observer) dan memberitahu semuanya setiap kali keadaannya berubah. Pola ini hidup di mana-mana pada aplikasi nyata, dari listener tombol antarmuka sampai pelanggan pesan di sistem terdistribusi.
import java.util.ArrayList;
import java.util.List;
public class PolaObserver {
interface Pengamat {
void diberiTahu(double suhu);
}
static class SensorSuhu {
private final List<Pengamat> daftarPengamat = new ArrayList<>();
private double suhu;
void daftar(Pengamat pengamat) {
daftarPengamat.add(pengamat);
}
void ubahSuhu(double suhuBaru) {
this.suhu = suhuBaru;
for (Pengamat pengamat : daftarPengamat) {
pengamat.diberiTahu(suhu);
}
}
}
public static void main(String[] args) {
SensorSuhu sensor = new SensorSuhu();
sensor.daftar(s -> System.out.printf(" perekam : catat %.1f C%n", s));
sensor.daftar(s -> {
if (s > 30) {
System.out.printf(" peringatan : suhu %.1f C di atas batas%n", s);
}
});
System.out.println("suhu 27.5 C:");
sensor.ubahSuhu(27.5);
System.out.println("suhu 32.0 C:");
sensor.ubahSuhu(32.0);
}
}suhu 27.5 C:
perekam : catat 27.5 C
suhu 32.0 C:
perekam : catat 32.0 C
peringatan : suhu 32.0 C di atas batasDua pengamat pada program ini berperilaku berbeda tanpa sensor perlu tahu bedanya. Perekam mencatat setiap perubahan, sedangkan peringatan hanya bereaksi bila batas terlewati, dan keduanya hanyalah lambda yang memenuhi interface Pengamat. Kelas java.util.Observer yang dulu resmi justru sudah deprecated, dan bentuk yang dijaga Java hari ini adalah listener seperti PropertyChangeListener beserta ribuan listener antarmuka di library standar.
Coba demo berikut untuk merasakan mekanisme pendaftaran itu. Geser suhu sensor, daftarkan dan lepas pengamat, lalu perhatikan siapa yang menerima pemberitahuan.
Strategy: algoritma yang dapat ditukar#
Masalah Strategy adalah satu pekerjaan yang perlu dikerjakan dengan cara berbeda-beda, misalnya mengurutkan berdasarkan nama atau berdasarkan nilai. Pola ini menaruh setiap cara ke dalam strategi yang dapat dipilih saat berjalan, dan antarmuka Comparator beserta lambdanya di Java adalah contoh Strategy yang paling sering dipakai tanpa sadar, sebagaimana sudah dipraktikkan pada topik Pengurutan.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class PolaStrategy {
public static void main(String[] args) {
record Peserta(String nama, int nilai) {}
List<Peserta> peserta = new ArrayList<>(List.of(
new Peserta("Ali", 78),
new Peserta("Budi", 92),
new Peserta("Citra", 85)));
Comparator<Peserta> urutNilai = Comparator.comparingInt(Peserta::nilai).reversed();
Comparator<Peserta> urutNama = Comparator.comparing(Peserta::nama);
peserta.sort(urutNilai);
System.out.println("urut nilai : " + peserta);
peserta.sort(urutNama);
System.out.println("urut nama : " + peserta);
}
}urut nilai : [Peserta[nama=Budi, nilai=92], Peserta[nama=Citra, nilai=85], Peserta[nama=Ali, nilai=78]]
urut nama : [Peserta[nama=Ali, nilai=78], Peserta[nama=Budi, nilai=92], Peserta[nama=Citra, nilai=85]]Daftar yang sama disusun dua kali dengan dua strategi berbeda, dan logika sort tidak berubah sedikit pun. Strategi bisa jadi lambda seperti di atas, bisa juga kelas lengkap bila strateginya membawa keadaan sendiri. Bentuk modernnya memang ringkas, tetapi inti polanya tetap sama, yaitu algoritma dipisahkan dari pemilik datanya dan dipilih lewat tipe yang sama.
Kesalahan umum#
- Menjadikan Singleton sebagai tempat menaruh state yang berubah-ubah. State global yang menyatu dengan singleton membuat pengujian saling mencemari dan urutan eksekusi jadi penting. Simpan hanya yang memang benar-benar tunggal dan imutabel.
- Membangun Builder untuk objek dua field. Konstruktor biasa lebih pendek dan lebih jelas pada kasus kecil, sehingga polanya justru menambah kerumitan. Bangun Builder saat parameter mulai banyak dan sebagian bersifat opsional.
- Lupa melepas pengamat yang tidak lagi diperlukan. Subjek memegang referensi pengamat selamanya, sehingga objek pengamat tak pernah terbawa ke garbage collection. Sedekakan metode lepas daftar sejak awal.
- Mengganti if yang sederhana dengan Strategy. Dua jalur perilaku yang tidak akan bertambah tidak memerlukan hierarki antarmuka. Pola masuk akal saat strateginya banyak, tumbuh, atau ditentukan dari luar.
- Menyembunyikan pilihan yang penting di dalam pabrik. Factory yang menengok konfigurasi global untuk memilih implementasi membuat pemanggil kehilangan kendali tanpa sadar. Terima masukan yang menentukan pilihan bila pilihannya memang urusan pemanggil.
Ringkasan#
- Singleton menjaga satu objek tunggal, dan holder idiom menyelesaikannya dengan inisialisasi kelas oleh JVM yang malas dan aman dari thread.
- Factory memusatkan keputusan pembuatan objek di satu metode yang mengembalikan tipe interface, sehingga pemakai tidak menyentuh kelas konkret.
- Builder menyusun objek bertahap dengan nama field yang jelas dan menutupnya menjadi record imutabel di metode akhir.
- Observer menyalurkan pemberitahuan dari subjek ke daftar pengamat yang bisa bertambah dan berkurang, sedangkan Strategy menukar algoritma lewat tipe yang sama seperti
Comparator.
Referensi#
- Design Patterns — Refactoring.Guru — penjelasan ilustratif pola-pola klasik beserta struktur dan trade-off-nya.
- Class Record — Oracle Java SE 21 — dasar record sebagai pembawa data imutabel yang menjadi hasil akhir Builder.
- Interface PropertyChangeListener — Oracle Java SE 21 — bentuk listener resmi yang mewakili pola Observer di library Java.