Tiga belas chapter sebelumnya menimbun kita dengan alat: class dan object, encapsulasi, pewarisan, polimorfisme, interface, record, hingga sealed classes. Alat yang banyak justru menuntut rasa pilih — kegagalan desain OOP jarang terjadi karena kurang fitur, melainkan karena fitur yang tepat dipakai di tempat yang keliru. Chapter penutup ini merangkum empat prinsip kerja harian yang melintasi seluruh alat tersebut, lalu menerapkannya pada satu studi kasus refactoring utuh: class “serba bisa” dirombak menjadi kumpulan class kecil yang masing-masing berdiri sendiri. Prinsip berbentuk akronim SOLID akan dibahas mendalam pada Section khusus, jadi di sini kita fokus pada kebiasaan praktis yang bisa langsung dipraktikkan.

Empat Prinsip Kerja Harian #

Empat kebiasaan berikut adalah ringkasan praktis dari seluruh Section ini:

  • Favor komposisi daripada pewarisan. Simpan objek lain sebagai field dan delegasikan pekerjaan; pakai extends hanya untuk relasi is-a yang sejati. Chapter Composition vs Inheritance membedakan keduanya lewat kasus mobil dan mesin.
  • Program ke interface. Nyatakan dependensi sebagai interface (PenyimpananBuku, Mesin), sehingga implementasi bisa berganti tanpa menyentuh konsumennya. Chapter Interfaces dan Default dan Static Interface Methods membangun fondasinya.
  • Encapsulasi ketat. Field private, constructor yang menjaga invarian, dan defensive copy untuk data mutable yang masuk atau keluar. Chapter Encapsulation dan Immutability merinci caranya.
  • Class kecil dengan satu tanggung jawab. Setiap class punya satu alasan untuk berubah; jika Anda kesulitan menamai tanggung jawabnya dalam satu kalimat pendek, class tersebut kandidat dipecah.

Keempat prinsip saling menguatkan: class kecil mudah dienkapsulasi ketat, interface membuat class kecil mudah disusun lewat komposisi, dan komposisi menjaga setiap class tetap fokus.

Studi Kasus: Class Serba Bisa #

Berikut perpustakaan mini yang seluruh pekerjaannya ditumpuk pada satu class. PerpustakaanSerbaBisa menyimpan data katalog, memformat laporan, sekaligus mengirim notifikasi — tiga alasan berubah dalam satu tempat:

import java.util.ArrayList;
import java.util.List;

// Class "serba bisa": menyimpan data, mencetak laporan,
// dan mengirim notifikasi sekaligus dalam satu tempat.
class PerpustakaanSerbaBisa {
    private final List<String> judulBuku = new ArrayList<>();

    public void tambahBuku(String judul) {
        judulBuku.add(judul);
    }

    // Getter menyerahkan referensi internal apa adanya.
    public List<String> daftarBuku() {
        return judulBuku;
    }

    public void pinjam(String judul, String peminjam) {
        System.out.println("[notifikasi] Halo " + peminjam
                + ", buku \"" + judul + "\" siap diambil.");
    }

    public void laporanBulanan() {
        System.out.println("== Laporan Perpustakaan ==");
        System.out.println("Jumlah buku: " + judulBuku.size());
        for (String judul : judulBuku) {
            System.out.println("  - " + judul);
        }
    }
}

public class SebelumRefactor {
    public static void main(String[] args) {
        PerpustakaanSerbaBisa app = new PerpustakaanSerbaBisa();
        app.tambahBuku("Java Dasar");
        app.tambahBuku("Java Lanjutan");
        app.pinjam("Java Dasar", "Budi");

        // Pemanggil memegang list internal dan bisa mengubahnya dari luar.
        app.daftarBuku().clear();

        app.laporanBulanan();
    }
}

Output yang Dihasilkan:

text
[notifikasi] Halo Budi, buku "Java Dasar" siap diambil.
== Laporan Perpustakaan ==
Jumlah buku: 0

Output tersebut memperlihatkan kebocoran nyata: dua buku sudah didaftarkan, tetapi laporan mencatat Jumlah buku: 0 karena app.daftarBuku().clear() mengosongkan list internal lewat referensi yang bocor dari getter. Tiga masalah desain saling menumpuk di sini: getter tanpa defensive copy melanggar encapsulasi; tiga tanggung jawab menyatu dalam satu class sehingga sulit mengetes dan mengganti bagian mana pun; dan konsumen terikat langsung pada class konkret tanpa interface pemisah.

Hasil Refactoring #

Versi kedua memecah program yang sama menjadi empat bagian dengan peran jelas. KatalogBuku hanya menyimpan data, LaporanPerpustakaan hanya memformat laporan, NotifikasiPeminjaman hanya mengirim pesan, dan kontrak di antara mereka dinyatakan sebagai interface PenyimpananBuku:

import java.util.ArrayList;
import java.util.List;

// Kontrak penyimpanan: konsumen bergantung pada interface, bukan class konkret.
interface PenyimpananBuku {
    void tambah(String judul);

    int jumlah();

    List<String> daftarJudul();
}

// Satu tanggung jawab: menyimpan data katalog.
class KatalogBuku implements PenyimpananBuku {
    private final List<String> judulBuku = new ArrayList<>();

    @Override
    public void tambah(String judul) {
        judulBuku.add(judul);
    }

    @Override
    public int jumlah() {
        return judulBuku.size();
    }

    @Override
    public List<String> daftarJudul() {
        return List.copyOf(judulBuku); // salinan beku: internal tetap tertutup
    }
}

// Satu tanggung jawab: memformat laporan; berkomposisi dengan interface.
class LaporanPerpustakaan {
    private final PenyimpananBuku penyimpanan;

    public LaporanPerpustakaan(PenyimpananBuku penyimpanan) {
        this.penyimpanan = penyimpanan;
    }

    public void cetak() {
        System.out.println("== Laporan Perpustakaan ==");
        System.out.println("Jumlah buku: " + penyimpanan.jumlah());
        for (String judul : penyimpanan.daftarJudul()) {
            System.out.println("  - " + judul);
        }
    }
}

// Satu tanggung jawab: mengirim pemberitahuan peminjaman.
class NotifikasiPeminjaman {
    public void kirim(String judul, String peminjam) {
        System.out.println("[notifikasi] Halo " + peminjam
                + ", buku \"" + judul + "\" siap diambil.");
    }
}

public class SesudahRefactor {
    public static void main(String[] args) {
        PenyimpananBuku katalog = new KatalogBuku();
        LaporanPerpustakaan laporan = new LaporanPerpustakaan(katalog);
        NotifikasiPeminjaman notifikasi = new NotifikasiPeminjaman();

        katalog.tambah("Java Dasar");
        katalog.tambah("Java Lanjutan");
        notifikasi.kirim("Java Dasar", "Budi");

        // Upaya mutasi dari luar kini ditolak: data internal terlindungi.
        try {
            katalog.daftarJudul().clear();
        } catch (UnsupportedOperationException e) {
            System.out.println("daftar internal terlindungi dari mutasi luar");
        }

        laporan.cetak();
    }
}

Output yang Dihasilkan:

text
[notifikasi] Halo Budi, buku "Java Dasar" siap diambil.
daftar internal terlindungi dari mutasi luar
== Laporan Perpustakaan ==
Jumlah buku: 2
  - Java Dasar
  - Java Lanjutan

Mari kita bedah perubahan terpentingnya:

  1. interface PenyimpananBuku: LaporanPerpustakaan hanya mengenal interface ini, bukan KatalogBuku. Menyimpan katalog di database, di berkas, atau di memori cukup mengganti implementasi PenyimpananBuku — laporan tidak berubah satu baris pun.
  2. class KatalogBuku: satu tanggung jawab murni — menyimpan data. List.copyOf(judulBuku) mengembalikan salinan beku, sehingga clear() dari luar melempar UnsupportedOperationException dan laporan tetap mencatat dua buku.
  3. class LaporanPerpustakaan: class ini tidak menyimpan buku; class tersebut memakai penyimpanan lewat field penyimpanan — komposisi, bukan pewarisan.
  4. class NotifikasiPeminjaman: channel pengiriman (terminal hari ini, email atau WhatsApp besok) menjadi urusan class ini saja, terpisah dari data dan laporan.

Perilaku program tetap terjaga — notifikasi dan laporan tercetak seperti semula — tetapi struktur kini siap tumbuh: setiap bagian bisa diganti, diuji, atau dipindahkan sendiri-sendiri.

Kebiasaan Kecil yang Berdampak #

Refactoring di atas memperlihatkan prinsip bekerja dalam sekali sapu; dalam praktik, prinsip diterapkan lewat kebiasaan kecil yang berulang. Mulai dari field private dan defensive copy sejak baris pertama. Nyatakan dependensi sebagai interface ketika lebih dari satu konsumen akan memakainya. Tanyakan “ini is-a atau has-a?” setiap kali tangan mulai menulis extends. Pilih record untuk tipe data bernilai tetap daripada menulis class manual. Dan ketika sebuah class mulai menyimpan komentar berjudul “bagian laporan” dan “bagian notifikasi”, perlakukan komentar tersebut sebagai sinyal untuk memecah class.

Dengan rangkuman ini, perjalanan kita melalui object-oriented programming di Java tuntas: dari konsep hingga alat modern, semuanya bermuara pada desain yang bisa dirawat. Keterampilan berikutnya yang menanti adalah cara berpikir fungsional — lambda, method reference, dan stream — yang berdampingan sempurna dengan OOP di Java modern. Selamat melanjutkan ke Section Functional Programming.

Referensi #

Berkas Sumber

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

SebelumRefactor.java

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

// Class "serba bisa": menyimpan data, mencetak laporan,
// dan mengirim notifikasi sekaligus dalam satu tempat.
class PerpustakaanSerbaBisa {
    private final List<String> judulBuku = new ArrayList<>();

    public void tambahBuku(String judul) {
        judulBuku.add(judul);
    }

    // Getter menyerahkan referensi internal apa adanya.
    public List<String> daftarBuku() {
        return judulBuku;
    }

    public void pinjam(String judul, String peminjam) {
        System.out.println("[notifikasi] Halo " + peminjam
                + ", buku \"" + judul + "\" siap diambil.");
    }

    public void laporanBulanan() {
        System.out.println("== Laporan Perpustakaan ==");
        System.out.println("Jumlah buku: " + judulBuku.size());
        for (String judul : judulBuku) {
            System.out.println("  - " + judul);
        }
    }
}

public class SebelumRefactor {
    public static void main(String[] args) {
        PerpustakaanSerbaBisa app = new PerpustakaanSerbaBisa();
        app.tambahBuku("Java Dasar");
        app.tambahBuku("Java Lanjutan");
        app.pinjam("Java Dasar", "Budi");

        // Pemanggil memegang list internal dan bisa mengubahnya dari luar.
        app.daftarBuku().clear();

        app.laporanBulanan();
    }
}

SesudahRefactor.java

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

// Kontrak penyimpanan: konsumen bergantung pada interface, bukan class konkret.
interface PenyimpananBuku {
    void tambah(String judul);

    int jumlah();

    List<String> daftarJudul();
}

// Satu tanggung jawab: menyimpan data katalog.
class KatalogBuku implements PenyimpananBuku {
    private final List<String> judulBuku = new ArrayList<>();

    @Override
    public void tambah(String judul) {
        judulBuku.add(judul);
    }

    @Override
    public int jumlah() {
        return judulBuku.size();
    }

    @Override
    public List<String> daftarJudul() {
        return List.copyOf(judulBuku); // salinan beku: internal tetap tertutup
    }
}

// Satu tanggung jawab: memformat laporan; berkomposisi dengan interface.
class LaporanPerpustakaan {
    private final PenyimpananBuku penyimpanan;

    public LaporanPerpustakaan(PenyimpananBuku penyimpanan) {
        this.penyimpanan = penyimpanan;
    }

    public void cetak() {
        System.out.println("== Laporan Perpustakaan ==");
        System.out.println("Jumlah buku: " + penyimpanan.jumlah());
        for (String judul : penyimpanan.daftarJudul()) {
            System.out.println("  - " + judul);
        }
    }
}

// Satu tanggung jawab: mengirim pemberitahuan peminjaman.
class NotifikasiPeminjaman {
    public void kirim(String judul, String peminjam) {
        System.out.println("[notifikasi] Halo " + peminjam
                + ", buku \"" + judul + "\" siap diambil.");
    }
}

public class SesudahRefactor {
    public static void main(String[] args) {
        PenyimpananBuku katalog = new KatalogBuku();
        LaporanPerpustakaan laporan = new LaporanPerpustakaan(katalog);
        NotifikasiPeminjaman notifikasi = new NotifikasiPeminjaman();

        katalog.tambah("Java Dasar");
        katalog.tambah("Java Lanjutan");
        notifikasi.kirim("Java Dasar", "Budi");

        // Upaya mutasi dari luar kini ditolak: data internal terlindungi.
        try {
            katalog.daftarJudul().clear();
        } catch (UnsupportedOperationException e) {
            System.out.println("daftar internal terlindungi dari mutasi luar");
        }

        laporan.cetak();
    }
}