Law of Demeter — Clean Code

Law of Demeter (LoD) — juga disebut "Principle of Least Knowledge" — adalah aturan desain: sebuah objek hanya boleh berbicara dengan teman dekatnya, bukan…

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 address di data customer jadi daftar addresses (rumah dan kantor). Tiga fungsi di tiga file masih membaca order.customer.address.city. Apa yang terjadi saat pickCourier(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

  1. C sendiri (this),
  2. objek yang dibuat f,
  3. objek yang dioper ke f sebagai parameter,
  4. 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:

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

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.

Yang akan kamu pelajari