Sistem kasir yang sama bisa memakai aturan potongan harga yang berbeda-beda: pelanggan reguler tanpa potongan, promo bertingkat berdasarkan total belanja, atau potongan khusus malam. Bila aturan-aturan tersebut ditulis sebagai percabangan if di dalam satu method besar, setiap promo baru menambah cabang dan setiap pengujian harus menyisir seluruh kombinasi. Strategy pattern mengeluarkan setiap algoritma dari percabangan dan mengemasnya sebagai objek yang bisa dipilih, diganti, dan disusun saat program berjalan. Chapter ini membahas pola tersebut lengkap dengan wujud modernnya di Java: lambda dan Comparator.

Algoritma sebagai Objek #

Inti strategy adalah satu interface dengan satu method yang mewakili algoritma — misalnya potongan(subtotal). Setiap implementasi menjadi class kecil yang berdiri sendiri, dan kode klien hanya mengenal interface tersebut. Mekanisme di baliknya adalah dynamic dispatch: saat JVM menjalankan strategi.potongan(subtotal), JVM melihat objek aktual di balik variabel bertipe interface, lalu melompat ke method milik class implementasinya. Karena pilihan objek ditentukan saat program berjalan, algoritma berganti tanpa satu baris if di kode pemanggil.

Manfaat susulannya tidak kalah penting: setiap strategi dapat diuji terpisah dengan kasus ujinya sendiri, dan strategi baru ditambah sebagai class baru tanpa membuka class lama — persis prinsip open/closed yang kita bahas pada chapter SOLID.

Strategi Diskon Bertingkat dalam Kode #

Program berikut menerapkan tiga strategi potongan pada data belanja yang sama, lalu menutup dengan pengurutan produk memakai Comparator — strategy resmi milik JDK:

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;

public class DiskonStrategi {
    public static void main(String[] args) {
        long[] subtotals = { 50_000, 150_000, 600_000, 1_200_000 };

        // Objek strategi: algoritma dikemas sebagai nilai yang bisa dipilih runtime
        List<StrategiDiskon> daftarStrategi = List.of(
                new DiskonReguler(),
                new DiskonBertingkat(),
                new DiskonMalam());

        for (StrategiDiskon strategi : daftarStrategi) {
            System.out.println("== " + strategi.nama() + " ==");
            for (long subtotal : subtotals) {
                System.out.println(String.format(Locale.US,
                        "  Rp%,10d -> potongan Rp%,8d", subtotal, strategi.potongan(subtotal)));
            }
        }

        // Comparator adalah strategy bawaan JDK: algoritma urut ditukar tanpa menyentuh data
        List<Produk> produk = new ArrayList<>(List.of(
                new Produk("Pena", 12_000),
                new Produk("Buku", 85_000),
                new Produk("Tas", 250_000)));

        produk.sort(Comparator.comparingLong(Produk::harga));
        System.out.println("termurah dulu : " + produk);

        produk.sort(Comparator.comparingLong(Produk::harga).reversed());
        System.out.println("termahal dulu : " + produk);
    }
}

interface StrategiDiskon {
    String nama();

    long potongan(long subtotal);
}

class DiskonReguler implements StrategiDiskon {
    @Override
    public String nama() {
        return "DiskonReguler";
    }

    @Override
    public long potongan(long subtotal) {
        return 0; // pelanggan reguler tidak menerima potongan
    }
}

class DiskonBertingkat implements StrategiDiskon {
    @Override
    public String nama() {
        return "DiskonBertingkat";
    }

    @Override
    public long potongan(long subtotal) {
        if (subtotal >= 1_000_000) {
            return subtotal / 10;       // tingkat tiga: 10%
        }
        if (subtotal >= 500_000) {
            return subtotal / 20;       // tingkat dua: 5%
        }
        if (subtotal >= 100_000) {
            return subtotal / 100 * 3;  // tingkat satu: 3%
        }
        return 0;
    }
}

class DiskonMalam implements StrategiDiskon {
    @Override
    public String nama() {
        return "DiskonMalam";
    }

    @Override
    public long potongan(long subtotal) {
        return subtotal >= 500_000 ? 50_000 : 0; // potongan tetap untuk belanja besar
    }
}

record Produk(String nama, long harga) {
    @Override
    public String toString() {
        return nama + "(Rp" + harga + ")";
    }
}

Output yang Dihasilkan:

text
== DiskonReguler ==
  Rp    50,000 -> potongan Rp       0
  Rp   150,000 -> potongan Rp       0
  Rp   600,000 -> potongan Rp       0
  Rp 1,200,000 -> potongan Rp       0
== DiskonBertingkat ==
  Rp    50,000 -> potongan Rp       0
  Rp   150,000 -> potongan Rp   4,500
  Rp   600,000 -> potongan Rp  30,000
  Rp 1,200,000 -> potongan Rp 120,000
== DiskonMalam ==
  Rp    50,000 -> potongan Rp       0
  Rp   150,000 -> potongan Rp       0
  Rp   600,000 -> potongan Rp  50,000
  Rp 1,200,000 -> potongan Rp  50,000
termurah dulu : [Pena(Rp12000), Buku(Rp85000), Tas(Rp250000)]
termahal dulu : [Tas(Rp250000), Buku(Rp85000), Pena(Rp12000)]

Mari kita bedah isi program tersebut:

  1. Baris 38-42 (interface StrategiDiskon): kontrak dua method — potongan sebagai algoritmanya, nama untuk pelaporan. Seluruh kode klien hanya mengenal kontrak ini; JVM yang memilih implementasi saat pemanggilan.
  2. Baris 56-75 (class DiskonBertingkat): aturan bertingkat hidup utuh di satu class. Promo baru — misalnya DiskonAkhirTahun — ditulis sebagai class baru dan langsung bisa dimasukkan ke daftarStrategi tanpa menyentuh satu pun strategi lama.
  3. Baris 23-31 (perulangan ganda di main): data belanja tidak berubah sedikit pun antar strategi; yang berganti hanya objek algoritma. Tidak ada switch atas jenis pelanggan — pemilihan strategi dipindah ke titik tempat konteksnya memang diketahui (misalnya layanan member).
  4. Baris 34-36 (Comparator.comparingLong(...).reversed()): Comparator adalah strategy milik JDK. Algoritma “termurah dulu” diputar menjadi “termahal dulu” lewat satu method reversed() tanpa membuat class baru — bukti bahwa strategi bisa juga disusun dari strategi lain.

Lambda sebagai Strategy Modern #

Sejak Java 8, interface dengan tepat satu method abstrak menjadi functional interface dan implementasinya bisa ditulis sebagai lambda: subtotal -> subtotal / 10. Untuk strategi satu-baris, lambda memotong upacara class sampai satu baris berarti. Kita sudah melihatnya bekerja: Comparator.comparingLong(Produk::harga) dibangun dari method reference — saudara dekat lambda.

Batasannya juga perlu jeli dilihat. Lambda tanpa nama tidak cocok saat strategi butuh lebih dari satu method (seperti nama() untuk pelaporan), butuh keadaan internal, atau dirujuk di banyak tempat. Ketika strategi tumbuh melebihi satu layar atau butuh identitas, kembalikan ke class atau record bernama; ketika strategi hanya rumus kecil, lambda adalah bentuk paling jujurnya.

Strategy mengganti cara menghitung. Pola berikutnya menangani pertanyaan lain: bagaimana satu peristiwa — stok berubah — memberi tahu banyak pihak yang mendaftar tanpa membuat toko mengenal semuanya satu per satu? Itu pekerjaan observer pattern.

Referensi #

Berkas Sumber

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

DiskonStrategi.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;

public class DiskonStrategi {
    public static void main(String[] args) {
        long[] subtotals = { 50_000, 150_000, 600_000, 1_200_000 };

        // Objek strategi: algoritma dikemas sebagai nilai yang bisa dipilih runtime
        List<StrategiDiskon> daftarStrategi = List.of(
                new DiskonReguler(),
                new DiskonBertingkat(),
                new DiskonMalam());

        for (StrategiDiskon strategi : daftarStrategi) {
            System.out.println("== " + strategi.nama() + " ==");
            for (long subtotal : subtotals) {
                System.out.println(String.format(Locale.US,
                        "  Rp%,10d -> potongan Rp%,8d", subtotal, strategi.potongan(subtotal)));
            }
        }

        // Comparator adalah strategy bawaan JDK: algoritma urut ditukar tanpa menyentuh data
        List<Produk> produk = new ArrayList<>(List.of(
                new Produk("Pena", 12_000),
                new Produk("Buku", 85_000),
                new Produk("Tas", 250_000)));

        produk.sort(Comparator.comparingLong(Produk::harga));
        System.out.println("termurah dulu : " + produk);

        produk.sort(Comparator.comparingLong(Produk::harga).reversed());
        System.out.println("termahal dulu : " + produk);
    }
}

interface StrategiDiskon {
    String nama();

    long potongan(long subtotal);
}

class DiskonReguler implements StrategiDiskon {
    @Override
    public String nama() {
        return "DiskonReguler";
    }

    @Override
    public long potongan(long subtotal) {
        return 0; // pelanggan reguler tidak menerima potongan
    }
}

class DiskonBertingkat implements StrategiDiskon {
    @Override
    public String nama() {
        return "DiskonBertingkat";
    }

    @Override
    public long potongan(long subtotal) {
        if (subtotal >= 1_000_000) {
            return subtotal / 10;       // tingkat tiga: 10%
        }
        if (subtotal >= 500_000) {
            return subtotal / 20;       // tingkat dua: 5%
        }
        if (subtotal >= 100_000) {
            return subtotal / 100 * 3;  // tingkat satu: 3%
        }
        return 0;
    }
}

class DiskonMalam implements StrategiDiskon {
    @Override
    public String nama() {
        return "DiskonMalam";
    }

    @Override
    public long potongan(long subtotal) {
        return subtotal >= 500_000 ? 50_000 : 0; // potongan tetap untuk belanja besar
    }
}

record Produk(String nama, long harga) {
    @Override
    public String toString() {
        return nama + "(Rp" + harga + ")";
    }
}