Law of Demeter (juga disebut Principle of Least Knowledge) adalah aturan desain: sebuah objek cuma boleh bicara dengan teman dekatnya, bukan dengan teman dari temannya. Aturan ini lahir di Demeter Project, Northeastern University (Ian Holland, 1987), ditulis resmi oleh Karl Lieberherr dan Ian Holland di IEEE Software (1989), dan dipopulerkan Robert C. Martin di buku Clean Code (2008). Bayangin kamu mau tahu paket Sari dikirim ke kota mana. Kamu tanya petugas ekspedisi, "Paket ini ke kota mana?", dan dia jawab "Bandung". Kamu gak perlu tahu dia membuka map, mencari lembar pesanan, lalu membaca kolom alamat baris ketiga. Waktu ekspedisinya ganti formulir, kamu tetap bertanya dengan kalimat yang sama.
🧪 Tebakan cepat: tim Raka mengganti
addressdi data customer jadi daftaraddresses(rumah dan kantor). Tiga fungsi di tiga file masih membacaorder.customer.address.city. Apa yang terjadi saatpickCourier(order)dipanggil:'JNE',undefined, atau error? Tebak dulu, jawabannya ada di bagian "Saat bentuk alamat berubah".
Tiga file, satu rantai
Toko Kaos Raka sekarang mengirim paket. Pesanan Sari:
const order = {
id: 1,
customer: {
name: 'Sari',
address: { street: 'Jl. Kopi 7', city: 'Bandung' },
},
};
Tiga fungsi, di tiga file berbeda, butuh kota tujuannya:
// kurir.js: toko punya kurir sendiri untuk Bandung
function pickCourier(order) {
if (order.customer.address.city === 'Bandung') return 'Kurir Toko Raka';
return 'JNE';
}
// estimasi.js: lama pengiriman dalam hari
function deliveryDays(order) {
return order.customer.address.city === 'Bandung' ? 1 : 3;
}
// laporan.js
function reportLine(order) {
return 'Pesanan #' + order.id + ' ke ' + order.customer.address.city;
}
Output-nya Kurir Toko Raka, 1, Pesanan #1 ke Bandung. Semua jalan. Tapi tiap titik di order.customer.address.city adalah satu hal yang fungsi itu tahu: pesanan punya customer, customer punya satu address, address punya city. Rantai seperti ini, objek demi objek disambung titik, disebut train wreck di Clean Code: gerbong-gerbong yang saling terkait.
Saat bentuk alamat berubah
Raka ingin customer bisa menyimpan alamat rumah dan kantor:
customer: {
name: 'Sari',
addresses: [
{ label: 'Rumah', street: 'Jl. Kopi 7', city: 'Bandung' },
{ label: 'Kantor', street: 'Jl. Asia Afrika 8', city: 'Bandung' },
],
},
Ketiga fungsi melempar error yang sama (pesan Chrome dan Node):
TypeError: Cannot read properties of undefined (reading 'city')
Error, bukan 'JNE'. Itu jawaban tebakannya. order.customer.address sekarang undefined, dan membaca .city dari undefined gagal. Satu perubahan bentuk data memaksa tiga file diubah, persis Shotgun Surgery dari lesson Code Smells. Lebih buruk lagi kalau ada yang "mengamankan" rantainya dengan optional chaining, order.customer.address?.city: error-nya hilang, hasilnya undefined, dan Sari yang tinggal di Bandung dikirimi lewat JNE tanpa ada yang sadar.
Bicara dengan teman dekat
Aturannya, dalam rumusan Clean Code: method f di class C cuma boleh memanggil method milik
Csendiri (this),- objek yang dibuat
f, - objek yang dioper ke
fsebagai parameter, - objek yang disimpan di field
C.
Yang gak boleh: memanggil method pada objek yang dikembalikan oleh salah satu di atas, alias teman dari teman. Jadi pemanggil cukup bertanya ke order, dan tiap objek menjawab dari datanya sendiri:
class Customer {
constructor(data) {
this.data = data; // baris data customer dari database
}
city() {
return this.data.address.city; // cuma Customer yang tahu bentuk datanya
}
}
class Order {
constructor(id, customer) {
this.id = id;
this.customer = customer;
}
shippingCity() {
return this.customer.city(); // teman dekat: field Order
}
}
const sari = new Customer({
name: 'Sari',
address: { street: 'Jl. Kopi 7', city: 'Bandung' },
});
const order = new Order(1, sari);
function pickCourier(order) {
if (order.shippingCity() === 'Bandung') return 'Kurir Toko Raka';
return 'JNE';
}
deliveryDays dan reportLine juga cukup memanggil order.shippingCity(). Saat data alamat jadi daftar, ketiganya tetap gagal sampai kodenya diperbarui, tapi yang diperbarui cuma satu baris: city() jadi return this.data.addresses[0].city;. Ketiga pemanggil gak disentuh: dari 3 tempat yang membaca alamat jadi 1. Memindah rantai ke dalam objek yang punya datanya disebut Hide Delegate (Martin Fowler, Refactoring).
Tell, don't ask
Pasangan Law of Demeter adalah Tell, Don't Ask: suruh objek mengerjakan sesuatu, jangan tarik datanya keluar untuk diputuskan di luar. Toko Raka punya saldo dompet untuk customer:
// ASK: checkout membongkar dompet dan memutuskan sendiri
if (customer.wallet.balance >= total) {
customer.wallet.balance -= total;
}
// TELL: checkout menyuruh, customer yang tahu aturannya
if (customer.canPay(total)) customer.pay(total);
Raka menambah aturan "dompet yang dibekukan gak bisa dipakai". Versi ask: tiap tempat yang membaca wallet.balance harus ingat menambah cek frozen. Versi tell: cuma canPay di class Customer yang berubah.
Hitung tipe, bukan titik
Gak semua titik beruntun itu pelanggaran. Yang dihitung adalah perpindahan ke tipe yang berbeda:
cart.filter((item) => item.qty > 0).map((item) => item.price * item.qty)aman: tiap langkah mengembalikan Array lagi, jadi kamu terus bicara dengan objek yang sama jenisnya. Begitu juga' hemat10 '.trim().toUpperCase()(String) dan query builder sepertiquery.where(...).orderBy(...).order.getCustomer().getBillingAddress().getStreet()melanggar: Order, Customer, Address, String, empat tipe dengan strukturnya masing-masing. Obatnya Hide Delegate,order.getBillingStreet().- Data murni lebih longgar. Clean Code membedakan objek (punya perilaku, menyembunyikan datanya) dan struktur data (cuma data, memang untuk dibaca). JSON dari API yang dibaca sekali,
response.data.address.city, wajar dibaca langsung. Tapi begitu bentuk itu dibaca di banyak file sepertiordertadi, ia jadi kontrak yang gak sengaja kamu buat.
Jangan jadi Middle Man
Kebablasan juga ada smell-nya. Order yang isinya getCustomerName, getCustomerEmail, getCustomerPhone, getCustomerCity dan dua puluh method lain yang cuma meneruskan ke customer adalah Middle Man (Fowler): class yang kerjanya cuma mengoper. Tambahkan method untuk pertanyaan yang memang ditanyakan pemanggil (shippingCity, canPay), bukan untuk tiap field yang ada.
Latihan: getCustomerName dan getCustomerCity
Latihannya memakai class dengan getter: Address punya getCity(), Customer punya getName() dan getAddress(), Order punya getCustomer(). Tambahkan dua method di Order:
getCustomerName() {
return this.customer.getName();
}
getCustomerCity() {
return this.customer.getAddress().getCity();
}
Pemanggil cukup order.getCustomerName() dan order.getCustomerCity(), tanpa rantai order.getCustomer().getAddress(). Output-nya tiga baris: Budi, Jakarta, true (yang terakhir dari typeof Order.prototype.getCustomerCity === 'function'). Di dalam Order masih ada rantai pendek ke alamat, tapi sekarang di satu tempat. Kalau mau ketat seperti contoh Customer.city() tadi, beri Customer method getCity() dan Order cukup memanggil this.customer.getCity().
⚠️ Jebakan yang sering ketemu
- Rantai yang sama di banyak file.
order.customer.address.citydi tiga tempat berarti bentuk alamat dikunci tiga kali. - Menambal dengan
?.. Error-nya hilang, hasilnya diam-diam salah (JNEuntuk pembeli di Bandung). Optional chaining menanganinull, bukan coupling. - Menghitung titik, bukan tipe.
filter().map().reduce()pada Array bukan pelanggaran. - Middle Man. Jangan bikin delegasi untuk tiap field; cukup untuk pertanyaan yang benar-benar dibutuhkan pemanggil.
- Di latihan, cuma memendekkan rantai di pemanggil. Method
getCustomerName()dangetCustomerCity()harus ditulis di classOrder, dan duaconsole.log(null)di starter diganti dengan pemanggilan keduanya.
TL;DR: Law of Demeter (Holland 1987, Lieberherr dan Holland 1989, dipopulerkan Clean Code 2008): bicara dengan teman dekat, yaitu this, parameter, objek yang kamu buat, dan field-mu sendiri, bukan dengan objek yang dikembalikan mereka. order.customer.address.city di tiga file membuat perubahan address jadi addresses melempar TypeError: Cannot read properties of undefined (reading 'city') di ketiganya; dengan order.shippingCity() dan Customer.city() (Hide Delegate) cuma satu tempat yang berubah. Tell, Don't Ask: customer.canPay(total) dan customer.pay(total), bukan membongkar wallet.balance di luar. Hitung perpindahan tipe, bukan titik: rantai pada tipe yang sama (Array, String, query builder) aman, data murni seperti JSON boleh dibaca langsung, dan order.getCustomer().getBillingAddress().getStreet() adalah train wreck. Jangan kebablasan jadi Middle Man. Di latihan: getCustomerName() dan getCustomerCity() di Order, output Budi, Jakarta, true.