Pola desain klasik GoF seperti Strategy dan Chain of Responsibility lahir di dunia yang setiap behavior harus dibungkus class tersendiri: satu interface, satu class per strategi, satu class per simpul rantai. Lambda dan method reference mengubah hitungannya — karena behavior kini adalah nilai, pola-pola itu tetap hidup tetapi tanpa boilerplate: strategi menjadi Comparator atau Predicate yang ditulis sebaris, rantai menjadi Function yang disambung andThen. Pada Chapter penutup Section ini kita menerapkan tiga pola favorit dalam bentuk fungsionalnya: strategy untuk pengurutan, chain of responsibility untuk validasi berlapis, dan template method untuk generator laporan.
Strategy Tanpa Class Baru #
Inti pola Strategy: algoritma yang bisa ditukar dipisahkan dari kode yang memakainya. Versi lamanya butuh interface StrategiSortir plus class SortirHarga, SortirNama, dan seterusnya. Versi fungsionalnya hanya butuh parameter bertipe Comparator<Produk> — dan setiap strategi ditulis sebagai lambda atau Comparator.comparing(...):
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class StrategiSortir {
record Produk(String nama, int harga) {}
// Method utilitas: strategi pengurutan disuntik sebagai Comparator
static void tampilkan(List<Produk> data, Comparator<Produk> strategi, String judul) {
List<Produk> salinan = new ArrayList<>(data);
salinan.sort(strategi);
System.out.println(judul + " : " + salinan);
}
public static void main(String[] args) {
List<Produk> etalase = List.of(
new Produk("Kopi", 25000),
new Produk("Teh", 12000),
new Produk("Susu", 18000));
tampilkan(etalase, Comparator.comparing(Produk::harga), "Termurah");
tampilkan(etalase, Comparator.comparing(Produk::nama), "Abjad");
tampilkan(etalase, Comparator.comparing(Produk::harga).reversed(), "Termahal");
}
}Output yang Dihasilkan:
Termurah : [Produk[nama=Teh, harga=12000], Produk[nama=Susu, harga=18000], Produk[nama=Kopi, harga=25000]]
Abjad : [Produk[nama=Kopi, harga=25000], Produk[nama=Susu, harga=18000], Produk[nama=Teh, harga=12000]]
Termahal : [Produk[nama=Kopi, harga=25000], Produk[nama=Susu, harga=18000], Produk[nama=Teh, harga=12000]]Mari kita bedah pola pada program tersebut:
- Baris 9-14 (method
tampilkan): kode ini adalah konteks strategi — menyalin data agar aslinya aman, mengurutkan denganstrategiyang disuntik, lalu mencetak. Method ini tidak tahu dan tidak peduli algoritma pengurutan mana yang bekerja. - Baris 22 (
Comparator.comparing(Produk::harga)): strategy pertama — bandingkan berdasarkan fieldhargalewat method referenceProduk::hargasebagai key extractor.Comparator.comparingmembangunComparatorlengkap dari satu fungsi kecil. - Baris 23-24: strategy kedua berdasar
nama, dan strategy ketiga hanya membungkus strategy pertama dengan.reversed(). Tiga strategi, nol class tambahan — dan menyusun strategi keempat (misalnya berdasar harga lalu nama) cukup denganComparator.comparing(Produk::harga).thenComparing(Produk::nama)tanpa membuka methodtampilkansama sekali.
Chain of Responsibility dengan andThen #
Pola Chain of Responsibility memproses masukan melalui deretan handler: setiap handler boleh meneruskan, mengubah, atau menolak. Bentuk fungsionalnya memetakan handler ke Function lalu menyambungkannya dengan andThen — masukan mengalir dari handler pertama hingga terakhir. Penolakan kita nyatakan dengan IllegalArgumentException, sehingga rantai berhenti tepat di simpul yang menemukan masalah:
import java.util.List;
import java.util.function.Function;
import java.util.function.UnaryOperator;
public class RantaiValidasi {
// Simpul 1: menolak teks kosong
static UnaryOperator<String> wajibTerisi = teks -> {
if (teks.isBlank()) {
throw new IllegalArgumentException("wajib terisi");
}
return teks;
};
// Simpul 2: menolak teks yang memuat angka
static UnaryOperator<String> tanpaAngka = teks -> {
if (teks.matches(".*\\d.*")) {
throw new IllegalArgumentException("tidak boleh berangka: " + teks);
}
return teks;
};
// Simpul 3: memotong teks melebihi 20 karakter
static UnaryOperator<String> maksimal20 = teks ->
teks.substring(0, Math.min(teks.length(), 20)).trim();
public static void main(String[] args) {
Function<String, String> rantai = wajibTerisi
.andThen(tanpaAngka)
.andThen(maksimal20);
List<String> masukan = List.of(
"Budi Santoso",
"",
"Siti2026",
"Dewi Lestari dengan nama panjang sekali");
for (String teks : masukan) {
try {
System.out.println("OK : [" + rantai.apply(teks) + "]");
} catch (IllegalArgumentException e) {
System.out.println("TOLAK : " + e.getMessage());
}
}
}
}Output yang Dihasilkan:
OK : [Budi Santoso]
TOLAK : wajib terisi
TOLAK : tidak boleh berangka: Siti2026
OK : [Dewi Lestari dengan]Bedah rantainya:
- Baris 7-24 (tiga simpul
UnaryOperator<String>): setiap simpul satu tanggung jawab —wajibTerisimenolak teks kosong,tanpaAngkamenolak teks berangka,maksimal20memotong teks panjang. Ketiganya fieldstatic, jadi deklarasi berikutnya tinggal menyambung, bukan menulis class baru. - Baris 27-29 (
wajibTerisi.andThen(tanpaAngka).andThen(maksimal20)):andThenmenggabungkan simpul menjadiFunction<String, String>utuh. Perhatikan tipe variabelnya: hasilandThenbertipeFunction, bukanUnaryOperator, karenaandThendideklarasikan diFunction— detail tipe yang menjelaskan mengapa deklarasi rantai memakaiFunction<String, String>. - Baris 38-43 (loop +
try/catch):"Budi Santoso"lolos tiga simpul;""ditolak simpul pertama dengan pesanwajib terisi;"Siti2026"ditolak simpul kedua; nama panjang dipotong menjadi[Dewi Lestari dengan]. Penolakan menghentikan alur — handler setelah simpul yang melempar tidak pernah dijalankan. - Menyusun ulang itu murah: ingin pemeriksaan angka lebih dulu? Tulis
tanpaAngka.andThen(wajibTerisi).andThen(maksimal20)— urutan baru tanpa menyentuh satu simpul pun.
Template Method dengan Behavior sebagai Parameter #
Pola template memisahkan alur tetap dari langkah yang bervariasi. Versi class memakai inheritance dan method yang di-override; versi fungsional cukup menerima Function untuk langkah yang berubah. Generator laporan berikut menjaga alur judul-isi-kaki tetap, sementara format tiap baris disuntik sebagai Function<Penjualan, String>:
import java.util.List;
import java.util.function.Function;
public class LaporanFleksibel {
record Penjualan(String barang, int kuantitas, int total) {}
static List<Penjualan> data = List.of(
new Penjualan("Kopi", 30, 750000),
new Penjualan("Teh", 12, 144000));
// Template: alur laporan tetap, format baris disuntik sebagai behavior
static void cetakLaporan(String judul, Function<Penjualan, String> formatBaris) {
System.out.println("=== " + judul + " ===");
for (Penjualan p : data) {
System.out.println(formatBaris.apply(p));
}
System.out.println("--- selesai ---");
}
public static void main(String[] args) {
cetakLaporan("Ringkas", p -> p.barang() + " x" + p.kuantitas());
cetakLaporan("Rupiah", p -> String.format("%-6s Rp%,d", p.barang(), p.total()));
}
}Output yang Dihasilkan:
=== Ringkas ===
Kopi x30
Teh x12
--- selesai ---
=== Rupiah ===
Kopi Rp750,000
Teh Rp144,000
--- selesai ---Bedah komposisinya:
- Baris 13-20 (method
cetakLaporan): alur cetak — judul, iterasi data, kaki laporan — ditulis sekali di method utilitas ini. Method tidak tahu formatnya apa;formatBaris.apply(p)memanggil behavior yang datang dari luar. - Baris 24 dan 25: dua format berbeda disuntik sebagai lambda. Format
Ringkasmemakaip.barang() + " x" + p.kuantitas(); formatRupiahmemakaiString.format("%-6s Rp%,d", ...)sehingga angka tampil dengan pemisah ribuan dan kolom rata kiri. - Dua laporan, satu alur: keluaran membuktikan hanya bagian tengah yang berubah; judul dan kaki identik karena keduanya dikelola satu tempat. Menambah format ketiga (misalnya CSV) berarti menambah satu lambda, bukan menyalin alur cetak.
Ketiga pola ini menutup lingkaran Section Functional Programming: kita mulai dari kontras imperatif-deklaratif, membangun fondasi functional interface dan lambda, menaikkan level lewat komposisi, kemudian menjaga kualitas dengan kemurnian — dan di penghujungnya, pola desain klasik runtuh menjadi deretan Comparator, Predicate, dan andThen yang ringkas. Langkah alami berikutnya adalah memasukkan pola-pola ini ke aliran data besar: Section Stream API akan menunjukkan bahwa filter, map, dan reduce adalah pola-pola yang baru saja kita tulis tangan, kali ini disediakan pustaka sebagai pipeline siap pakai.
Referensi #
- Oracle: java.util.Comparator
- Oracle: java.util.function (Package Summary)
- dev.java: Lambda Expressions
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
LaporanFleksibel.java
Unduh berkasimport java.util.List;
import java.util.function.Function;
public class LaporanFleksibel {
record Penjualan(String barang, int kuantitas, int total) {}
static List<Penjualan> data = List.of(
new Penjualan("Kopi", 30, 750000),
new Penjualan("Teh", 12, 144000));
// Template: alur laporan tetap, format baris disuntik sebagai behavior
static void cetakLaporan(String judul, Function<Penjualan, String> formatBaris) {
System.out.println("=== " + judul + " ===");
for (Penjualan p : data) {
System.out.println(formatBaris.apply(p));
}
System.out.println("--- selesai ---");
}
public static void main(String[] args) {
cetakLaporan("Ringkas", p -> p.barang() + " x" + p.kuantitas());
cetakLaporan("Rupiah", p -> String.format("%-6s Rp%,d", p.barang(), p.total()));
}
}
RantaiValidasi.java
Unduh berkasimport java.util.List;
import java.util.function.Function;
import java.util.function.UnaryOperator;
public class RantaiValidasi {
// Simpul 1: menolak teks kosong
static UnaryOperator<String> wajibTerisi = teks -> {
if (teks.isBlank()) {
throw new IllegalArgumentException("wajib terisi");
}
return teks;
};
// Simpul 2: menolak teks yang memuat angka
static UnaryOperator<String> tanpaAngka = teks -> {
if (teks.matches(".*\\d.*")) {
throw new IllegalArgumentException("tidak boleh berangka: " + teks);
}
return teks;
};
// Simpul 3: memotong teks melebihi 20 karakter
static UnaryOperator<String> maksimal20 = teks ->
teks.substring(0, Math.min(teks.length(), 20)).trim();
public static void main(String[] args) {
Function<String, String> rantai = wajibTerisi
.andThen(tanpaAngka)
.andThen(maksimal20);
List<String> masukan = List.of(
"Budi Santoso",
"",
"Siti2026",
"Dewi Lestari dengan nama panjang sekali");
for (String teks : masukan) {
try {
System.out.println("OK : [" + rantai.apply(teks) + "]");
} catch (IllegalArgumentException e) {
System.out.println("TOLAK : " + e.getMessage());
}
}
}
}
StrategiSortir.java
Unduh berkasimport java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class StrategiSortir {
record Produk(String nama, int harga) {}
// Method utilitas: strategi pengurutan disuntik sebagai Comparator
static void tampilkan(List<Produk> data, Comparator<Produk> strategi, String judul) {
List<Produk> salinan = new ArrayList<>(data);
salinan.sort(strategi);
System.out.println(judul + " : " + salinan);
}
public static void main(String[] args) {
List<Produk> etalase = List.of(
new Produk("Kopi", 25000),
new Produk("Teh", 12000),
new Produk("Susu", 18000));
tampilkan(etalase, Comparator.comparing(Produk::harga), "Termurah");
tampilkan(etalase, Comparator.comparing(Produk::nama), "Abjad");
tampilkan(etalase, Comparator.comparing(Produk::harga).reversed(), "Termahal");
}
}