Sebelum ada class, ada cerita. Pemilik produk bercerita: “pustakawan meminjamkan buku ke anggota, anggota membaca lalu mengembalikan, dan buku yang dikembalikan tersedia kembali”. Object-Oriented Design (OOD) adalah keahlian menerjemahkan cerita semacam itu menjadi class, method, dan relasi yang koheren — bukan sekadar memahami sintaks class dan interface. Desain yang benar membuat kode mudah dijelaskan kembali ke pemilik produk; desain yang keliru membuat kode hanya bisa dijelaskan kepada mesin. Chapter ini melatih tiga teknik praktis: menggali class dari kata benda, memetakan tanggung jawab lewat CRC card, dan menjaga relasi antarclass tetap minimal.
Kata Benda dan Kata Kerja #
Teknik paling sederhana untuk cerita pendek: soroti kata benda sebagai kandidat class, dan kata kerja sebagai kandidat method. Cerita perpustakaan di atas memuat kata benda pustakawan, buku, anggota; kata kerjanya meminjamkan, mengembalikan. Pemetaannya menjadi:
| Kata benda | Kandidat class | Kata kerja terdekat | Kandidat method |
|---|---|---|---|
| buku | Buku | tersedia, berkurang | tersedia(), kurangiStok() |
| anggota | Anggota | meminjam, mengembalikan | catatPinjaman(), hapusPinjaman() |
| perpustakaan | Perpustakaan | mengatur transaksi | pinjam(), kembalikan() |
Hasil sorotan bukan keputusan akhir. Kata benda yang hanya menjelaskan kata benda lain (misalnya “judul buku”) biasanya cukup menjadi field, bukan class. Kata kerja yang menuntut keputusan lintas objek — “meminjamkan” melibatkan anggota sekaligus stok buku — biasanya menjadi method milik class perantara, bukan milik salah satu pihak.
CRC Card untuk Memetakan Tanggung Jawab #
Teknik klasik dari komunitas OOP adalah CRC card — kartu index berisi nama Class, Responsibility (tanggung jawab), dan Collaborator (kolaborator). Kekuatan kartu ini bukan kertasnya, melainkan pemaksaan berpikir: satu kartu menampung satu tanggung jawab utama; tanggung jawab yang tidak muat meminta kartu baru. Versi perpustakaan kita:
| Class | Responsibility | Collaborator |
|---|---|---|
Buku | Mengetahui judul dan ketersediaan stok | — |
Anggota | Mencatat buku yang sedang dipinjam | Buku |
Perpustakaan | Mengatur aturan pinjam dan kembali | Buku, Anggota |
Perhatikan keputusan penting pada baris ketiga: aturan “stok harus tersedia” tidak ditaruh di Anggota maupun Buku, melainkan di Perpustakaan sebagai wasit. Buku hanya melaporkan dirinya; Anggota hanya mencatat miliknya; keputusan lintas keduanya milik mediator.
Relasi Minimal antar Class #
Desain yang baik menjaga relasi serendah mungkin. Pada diagram berikut Anggota menyimpan daftar Buku yang dipinjam (asosiasi satu arah), dan Perpustakaan mengenal keduanya — tetapi Buku tidak mengenal Anggota mana pun, dan tidak ada pewarisan di antara ketiganya.
Setiap panah yang kita tambahkan adalah dependensi yang harus dirawat. Bila kelak Buku perlu tahu siapa peminjamnya, tambahkan relasi saat kebutuhan itu nyata — bukan karena terasa lengkap.
Perpustakaan Mini yang Dapat Dijalankan #
Program berikut menerapkan ketiga kartu CRC di atas, lengkap dengan skenario sukses dan gagal:
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class PerpustakaanMini {
public static void main(String[] args) {
Perpustakaan perpus = new Perpustakaan();
perpus.tambahBuku("B-001", "Laskar Pelangi", 2);
perpus.tambahBuku("B-002", "Bumi Manusia", 1);
Anggota budi = perpus.daftar("Budi");
Anggota siti = perpus.daftar("Siti");
System.out.println(perpus.pinjam(budi, "B-001"));
System.out.println(perpus.pinjam(siti, "B-001"));
System.out.println(perpus.pinjam(siti, "B-002"));
System.out.println(perpus.pinjam(siti, "B-002"));
System.out.println(perpus.kembalikan(budi, "B-001"));
System.out.println(perpus.pinjam(budi, "B-002"));
System.out.println("Pinjaman Budi: " + budi.jumlahPinjaman()
+ ", pinjaman Siti: " + siti.jumlahPinjaman());
}
}
class Buku {
private final String kode;
private final String judul;
private int stok;
Buku(String kode, String judul, int stok) {
this.kode = kode;
this.judul = judul;
this.stok = stok;
}
String judul() {
return judul;
}
boolean tersedia() {
return stok > 0;
}
void kurangiStok() {
stok--;
}
void tambahStok() {
stok++;
}
}
class Anggota {
private final String nama;
private final List<Buku> pinjaman = new ArrayList<>();
Anggota(String nama) {
this.nama = nama;
}
String nama() {
return nama;
}
void catatPinjaman(Buku buku) {
pinjaman.add(buku);
}
void hapusPinjaman(Buku buku) {
pinjaman.remove(buku);
}
int jumlahPinjaman() {
return pinjaman.size();
}
}
// Mediator: menghubungkan Anggota dan Buku tanpa keduanya saling mengenal
class Perpustakaan {
private final Map<String, Buku> katalog = new LinkedHashMap<>();
private final Map<String, Anggota> anggota = new LinkedHashMap<>();
void tambahBuku(String kode, String judul, int stok) {
katalog.put(kode, new Buku(kode, judul, stok));
}
Anggota daftar(String nama) {
return anggota.computeIfAbsent(nama, Anggota::new);
}
String pinjam(Anggota peminjam, String kode) {
Buku buku = katalog.get(kode);
if (buku == null) {
return peminjam.nama() + " gagal: buku " + kode + " tidak ada";
}
if (!buku.tersedia()) {
return peminjam.nama() + " gagal meminjam '" + buku.judul() + "': stok habis";
}
buku.kurangiStok();
peminjam.catatPinjaman(buku);
return peminjam.nama() + " meminjam '" + buku.judul() + "'";
}
String kembalikan(Anggota pengembali, String kode) {
Buku buku = katalog.get(kode);
if (buku == null || pengembali.jumlahPinjaman() == 0) {
return pengembali.nama() + " gagal: tidak ada pinjaman " + kode;
}
buku.tambahStok();
pengembali.hapusPinjaman(buku);
return pengembali.nama() + " mengembalikan '" + buku.judul() + "'";
}
}Output yang Dihasilkan:
Budi meminjam 'Laskar Pelangi'
Siti meminjam 'Laskar Pelangi'
Siti meminjam 'Bumi Manusia'
Siti gagal meminjam 'Bumi Manusia': stok habis
Budi mengembalikan 'Laskar Pelangi'
Budi gagal meminjam 'Bumi Manusia': stok habis
Pinjaman Budi: 0, pinjaman Siti: 2Mari kita bedah keputusan desain pada program tersebut:
- Baris 92-103 (
pinjamdiPerpustakaan): method wasit ini berurutan: cari buku, periksa ketersediaan, mutasi stok, catat pinjaman. Seluruh aturan transaksi terkumpul di satu tempat — persis kartu CRC ketiga. - Baris 41-43 (
tersedia()diBuku):Bukumenjawab pertanyaan tentang dirinya sendiri, tidak pernah bertanya kepadaAnggota. Class kecil yang berdaulat seperti ini mudah diuji dengan dua baris kode. - Baris 66-68 (
catatPinjamandiAnggota):Anggotamencatat pinjaman tanpa tahu aturan stok. Bila kelak ada aturan “maksimal tiga buku”, tempatnya diPerpustakaan.pinjam— satu tempat berubah. - Baris 105-113 (
kembalikan): pengembalian memeriksa kondisi gagal (buku == null) lalu membalikkan dua mutasi pinjaman. Pesan gagal berisi kode buku sehingga pemanggil tahu penyebabnya.
Dari tiga class yang saling bekerja sama, langkah berikutnya adalah menyusun ribuan class menjadi struktur aplikasi utuh. Chapter berikutnya membahas pola penataan paling banyak dipakai industri: Layered Architecture.
Referensi #
- Oracle: Object-Oriented Programming Concepts
- Martin Fowler: Catalog of Patterns of Enterprise Application Architecture
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PerpustakaanMini.java
Unduh berkasimport java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class PerpustakaanMini {
public static void main(String[] args) {
Perpustakaan perpus = new Perpustakaan();
perpus.tambahBuku("B-001", "Laskar Pelangi", 2);
perpus.tambahBuku("B-002", "Bumi Manusia", 1);
Anggota budi = perpus.daftar("Budi");
Anggota siti = perpus.daftar("Siti");
System.out.println(perpus.pinjam(budi, "B-001"));
System.out.println(perpus.pinjam(siti, "B-001"));
System.out.println(perpus.pinjam(siti, "B-002"));
System.out.println(perpus.pinjam(siti, "B-002"));
System.out.println(perpus.kembalikan(budi, "B-001"));
System.out.println(perpus.pinjam(budi, "B-002"));
System.out.println("Pinjaman Budi: " + budi.jumlahPinjaman()
+ ", pinjaman Siti: " + siti.jumlahPinjaman());
}
}
class Buku {
private final String kode;
private final String judul;
private int stok;
Buku(String kode, String judul, int stok) {
this.kode = kode;
this.judul = judul;
this.stok = stok;
}
String judul() {
return judul;
}
boolean tersedia() {
return stok > 0;
}
void kurangiStok() {
stok--;
}
void tambahStok() {
stok++;
}
}
class Anggota {
private final String nama;
private final List<Buku> pinjaman = new ArrayList<>();
Anggota(String nama) {
this.nama = nama;
}
String nama() {
return nama;
}
void catatPinjaman(Buku buku) {
pinjaman.add(buku);
}
void hapusPinjaman(Buku buku) {
pinjaman.remove(buku);
}
int jumlahPinjaman() {
return pinjaman.size();
}
}
// Mediator: menghubungkan Anggota dan Buku tanpa keduanya saling mengenal
class Perpustakaan {
private final Map<String, Buku> katalog = new LinkedHashMap<>();
private final Map<String, Anggota> anggota = new LinkedHashMap<>();
void tambahBuku(String kode, String judul, int stok) {
katalog.put(kode, new Buku(kode, judul, stok));
}
Anggota daftar(String nama) {
return anggota.computeIfAbsent(nama, Anggota::new);
}
String pinjam(Anggota peminjam, String kode) {
Buku buku = katalog.get(kode);
if (buku == null) {
return peminjam.nama() + " gagal: buku " + kode + " tidak ada";
}
if (!buku.tersedia()) {
return peminjam.nama() + " gagal meminjam '" + buku.judul() + "': stok habis";
}
buku.kurangiStok();
peminjam.catatPinjaman(buku);
return peminjam.nama() + " meminjam '" + buku.judul() + "'";
}
String kembalikan(Anggota pengembali, String kode) {
Buku buku = katalog.get(kode);
if (buku == null || pengembali.jumlahPinjaman() == 0) {
return pengembali.nama() + " gagal: tidak ada pinjaman " + kode;
}
buku.tambahStok();
pengembali.hapusPinjaman(buku);
return pengembali.nama() + " mengembalikan '" + buku.judul() + "'";
}
}