SOLID Principles — Clean Code

SOLID adalah 5 prinsip desain object-oriented yang membuat kode lebih mudah di-maintain, dipahami, dan diperluas. Dicetuskan Robert C. Martin. S — Single…

SOLID adalah lima prinsip desain supaya kode murah diubah: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion. Robert C. Martin mengumpulkannya di makalah "Design Principles and Design Patterns" (2000), dan Michael Feathers yang kemudian menyusun singkatan SOLID. Bayangin listrik di rumah: satu sakelar untuk satu lampu (S). Kipas baru tinggal dicolok, gak perlu membongkar kabel di tembok (O). Steker yang bilang cocok harus benar-benar menyalakan alatnya, bukan bikin korslet (L). Stopkontak cuma minta dua pin, bukan lima pin yang gak dipakai alatnya (I). Dan kabel rumah bergantung pada bentuk stopkontak standar, bukan pada merek kipas tertentu (D).

🧪 Tebakan cepat: keranjang member gratis ongkir, jadi FreeShippingCart menimpa addShipping dengan return 0, "karena ongkirnya nol". Keranjang C member (Rp 75.000) totalnya berapa: Rp 75.000, Rp 0, atau error? Tebak dulu, jawabannya ada di bagian "L: subclass menepati janji induknya".

Lima prinsip, lima perubahan di kasir

Tiap prinsip paling gampang dipahami dari perubahan yang jadi mahal kalau prinsip itu dilanggar:

Prinsip Perubahan di kasir Raka Tanpa prinsipnya
S CS mengganti sapaan email membuka method yang sama dengan rumus harga
O menambah voucher HEMAT20 mengedit switch yang sudah jalan, dan bisa merusak HEMAT10
L keranjang member gratis ongkir Total: Rp 0 untuk tiap member
I marketing menambah method promo 3 class notifier ikut diubah, checkout gak berubah apa-apa
D mengetes placeOrder 4 cek mengirim 4 email sungguhan

S: satu alasan berubah

Sudah dibahas di lesson Single Responsibility: OrderService.placeOrder yang memeriksa, menghitung harga, menyimpan dan mengirim email punya lima pihak yang bisa minta perubahan. Dipecah per yang minta, permintaan CS cuma menyentuh OrderMailer. Di latihan, UserService cuma mengurus data user dan EmailService cuma mengirim email, jadi UserService.prototype.sendEmail harus undefined.

O: tambah tanpa mengubah

Open/Closed (Bertrand Meyer, buku Object-Oriented Software Construction, 1988): kode terbuka untuk ekstensi, tertutup untuk modifikasi. Perilaku baru ditambah dengan menambah kode, bukan mengedit kode yang sudah jalan. Tanpa itu, HEMAT20 berarti mengedit switch:

function discountRate(voucherCode) {
  let rate = 0;
  switch (voucherCode) {
    case 'HEMAT20': rate = 0.2;      // baris baru, lupa break
    case 'HEMAT10': rate = 0.1; break;
  }
  return rate;
}
BEDA Total: Rp 220.500, harusnya Total: Rp 211.000

Tanpa break, HEMAT20 "jatuh" ke cabang HEMAT10 dan tarifnya ditimpa 0.1. Mengedit fungsi yang sudah jalan selalu bisa merusaknya. Tabel VOUCHERS dan class voucher dari dua lesson sebelumnya sudah tertutup untuk modifikasi: HEMAT20 cukup satu entri baru, HEMAT20: new PercentVoucher(0.2), dan nol baris lama berubah. Contoh lain, dari latihan: calculateArea(shape) cuma memanggil shape.area(), jadi bentuk baru gak mengubahnya.

L: subclass menepati janji induknya

Liskov Substitution (Barbara Liskov, 1987): objek subclass harus bisa dipakai di mana pun induknya dipakai, tanpa yang memanggil sadar bedanya. Janji Cart.addShipping(amount): mengembalikan amount ditambah ongkir.

class Cart {
  constructor(items) { this.items = items; }
  subtotal() { let s = 0; for (const item of this.items) s += item.price * item.qty; return s; }
  addShipping(amount) { return amount < FREE_SHIPPING_MIN ? amount + SHIPPING_FEE : amount; }
}
class FreeShippingCart extends Cart {   // keranjang member
  addShipping() { return 0; }           // "ongkirnya nol"
}

function calculateTotal(cart, voucherCode) {
  const discounted = applyVoucher(cart.subtotal(), voucherCode);
  return 'Total: ' + formatRupiah(cart.addShipping(discounted));
}
BEDA Total: Rp 0, harusnya Total: Rp 75.000

Rp 0, tanpa error. Itu jawaban tebakannya. Penulis subclass mengira addShipping mengembalikan ongkir, padahal janjinya mengembalikan total. Tiap member belanja gratis. Perbaikannya menepati janji: addShipping(amount) { return amount; }, dan keranjang C member jadi Total: Rp 75.000, keranjang D member dengan HEMAT10 Total: Rp 189.000.

I: minta yang dipakai saja

Interface Segregation (Martin): yang memakai sebuah kontrak gak boleh dipaksa bergantung pada method yang gak ia pakai. Kontrak Notifier kasir minta empat method: sendConfirmation, sendSms, sendWhatsapp, sendPromo. OrderService cuma memanggil sendConfirmation, tapi tiruan untuk test-nya harus menulis semuanya:

class FakeNotifier {
  constructor() { this.sent = []; }
  sendConfirmation(order) { this.sent.push(order.id); }
  sendSms() {}        // dipaksa kontrak, gak pernah dipanggil
  sendWhatsapp() {}   // dipaksa kontrak, gak pernah dipanggil
  sendPromo() {}      // dipaksa kontrak, gak pernah dipanggil
}

Marketing menambah sendPromoWithVoucher ke kontrak, dan EmailNotifier, WhatsappNotifier dan FakeNotifier ikut diubah: 3 class disentuh, checkout gak berubah apa-apa. Pecah kontraknya: OrderService minta OrderNotifier (cuma sendConfirmation), modul promo minta PromoSender (cuma sendPromo). JavaScript gak punya kata kunci interface; kontraknya ditulis di JSDoc @typedef atau interface TypeScript, dan TypeScript yang menolak class yang kurang method-nya.

D: bergantung pada kontrak, bukan pada email sungguhan

Dependency Inversion (Martin): modul tingkat tinggi (aturan checkout) gak bergantung pada modul tingkat rendah (cara mengirim email); keduanya bergantung pada abstraksi, di sini "apa pun yang punya sendConfirmation(order)".

class OrderService {
  constructor(mailer) { this.mailer = mailer; }   // diterima dari luar
  placeOrder(cart, voucherCode, email) {
    const order = { id: 1, email, total: calculateAmount(cart, voucherCode) };
    this.mailer.sendConfirmation(order);
    return order;
  }
}

const fake = { sent: [], sendConfirmation(order) { this.sent.push(order.total); } };
const service = new OrderService(fake);              // di test
// new OrderService(new EmailMailer())               // di toko sungguhan

Versi lama menulis new OrderMailer().sendConfirmation(order) di dalam placeOrder, jadi empat cek keranjang mengirim 4 email sungguhan. Dengan tiruan: 0 email terkirim, fake.sent berisi [220500,245000,90000,204000]. Cara mengoper dependensi dari luar lewat constructor ini disebut Dependency Injection: ia teknik untuk mewujudkan prinsip Dependency Inversion.

Latihan: UserService, EmailService dan Circle

Output-nya:

{"id":1,"name":"Pengguna 1"}
Email terkirim ke [email protected]: Selamat datang
153.93804002589985
true

⚠️ Jebakan yang sering ketemu

TL;DR: SOLID (dikumpulkan Robert C. Martin, singkatan dari Michael Feathers) adalah Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion, dan tiap prinsip mencegah satu perubahan mahal. S: satu alasan berubah, jadi sapaan email gak menyentuh rumus harga. O: terbuka untuk ekstensi, tertutup untuk modifikasi; HEMAT20 yang ditambah ke switch tanpa break merusak jadi Rp 220.500, sedangkan entri baru di tabel atau class mengubah nol baris lama. L: subclass menepati janji induknya; FreeShippingCart.addShipping yang mengembalikan 0 bikin Total: Rp 0, yang benar mengembalikan amount. I: minta method yang dipakai saja, supaya tiruan gak perlu tiga method kosong. D: bergantung pada abstraksi, dan Dependency Injection (mengoper mailer lewat constructor) adalah tekniknya; test dengan tiruan mengirim 0 email. Di latihan: UserService tanpa sendEmail, EmailService mengembalikan Email terkirim ke [email protected]: Selamat datang, dan Circle(7).area() dengan Math.PI jadi 153.93804002589985.

Yang akan kamu pelajari