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:

text
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:

  1. Baris 9-14 (method tampilkan): kode ini adalah konteks strategi — menyalin data agar aslinya aman, mengurutkan dengan strategi yang disuntik, lalu mencetak. Method ini tidak tahu dan tidak peduli algoritma pengurutan mana yang bekerja.
  2. Baris 22 (Comparator.comparing(Produk::harga)): strategy pertama — bandingkan berdasarkan field harga lewat method reference Produk::harga sebagai key extractor. Comparator.comparing membangun Comparator lengkap dari satu fungsi kecil.
  3. 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 dengan Comparator.comparing(Produk::harga).thenComparing(Produk::nama) tanpa membuka method tampilkan sama 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:

text
OK    : [Budi Santoso]
TOLAK : wajib terisi
TOLAK : tidak boleh berangka: Siti2026
OK    : [Dewi Lestari dengan]

Bedah rantainya:

  1. Baris 7-24 (tiga simpul UnaryOperator<String>): setiap simpul satu tanggung jawab — wajibTerisi menolak teks kosong, tanpaAngka menolak teks berangka, maksimal20 memotong teks panjang. Ketiganya field static, jadi deklarasi berikutnya tinggal menyambung, bukan menulis class baru.
  2. Baris 27-29 (wajibTerisi.andThen(tanpaAngka).andThen(maksimal20)): andThen menggabungkan simpul menjadi Function<String, String> utuh. Perhatikan tipe variabelnya: hasil andThen bertipe Function, bukan UnaryOperator, karena andThen dideklarasikan di Function — detail tipe yang menjelaskan mengapa deklarasi rantai memakai Function<String, String>.
  3. Baris 38-43 (loop + try/catch): "Budi Santoso" lolos tiga simpul; "" ditolak simpul pertama dengan pesan wajib terisi; "Siti2026" ditolak simpul kedua; nama panjang dipotong menjadi [Dewi Lestari dengan]. Penolakan menghentikan alur — handler setelah simpul yang melempar tidak pernah dijalankan.
  4. 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:

text
=== Ringkas ===
Kopi x30
Teh x12
--- selesai ---
=== Rupiah ===
Kopi   Rp750,000
Teh    Rp144,000
--- selesai ---

Bedah komposisinya:

  1. 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.
  2. Baris 24 dan 25: dua format berbeda disuntik sebagai lambda. Format Ringkas memakai p.barang() + " x" + p.kuantitas(); format Rupiah memakai String.format("%-6s Rp%,d", ...) sehingga angka tampil dengan pemisah ribuan dan kolom rata kiri.
  3. 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

LaporanFleksibel.java

Unduh berkas
java
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()));
    }
}

RantaiValidasi.java

Unduh berkas
java
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());
            }
        }
    }
}

StrategiSortir.java

Unduh berkas
java
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");
    }
}