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
FreeShippingCartmenimpaaddShippingdenganreturn 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
UserServicepunyagetUser(id)yang mengembalikan{ id, name: 'Pengguna ' + id }danupdateUser(id, data)yang mengembalikan{ id, ...data }. Gak adasendEmaildi sini.EmailService.sendEmail(to, subject)mengembalikan`Email terkirim ke ${to}: ${subject}`.Circlemenyimpanradius, danarea()mengembalikanMath.PI * this.radius * this.radius(atauMath.PI * this.radius ** 2). Jangan dibulatkan: untuk jari-jari 7 hasilnya153.93804002589985.2 * Math.PI * 7(43.98...) itu keliling, bukan luas.
Output-nya:
{"id":1,"name":"Pengguna 1"}
Email terkirim ke [email protected]: Selamat datang
153.93804002589985
true
⚠️ Jebakan yang sering ketemu
- Menghafal singkatannya, bukan perubahannya. Tiap prinsip menjawab "perubahan apa yang jadi mahal?". Kalau gak ada perubahan seperti itu di kodemu, jangan menambah class demi prinsip.
- Subclass yang mengubah arti method.
addShippingyang mengembalikan ongkir, bukan total, lolos tanpa error dan bikinRp 0. switchyang diedit untuk tiap jenis baru. Satubreakyang lupa merusak cabang lama.new EmailMailer()di dalam service. Test jadi mengirim email sungguhan; terima dari constructor.- Di latihan,
sendEmaildiUserServiceatau luas yang dibulatkan.typeof UserService.prototype.sendEmail === 'undefined'harustrue, dan luasnya153.93804002589985.
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.