Single Responsibility Principle: satu alasan untuk berubah dan deteksi class kembar fungsi; Open/Closed Principle: ekstensi tanpa modifikasi via abstraction & strategy — refactor god service NestJS, handler monolitik Fiber, dan fat controller Laravel dalam satu latihan bersama

SOLID adalah lima prinsip desain yang menjadi fondasi hampir semua keputusan arsitektur di series ini. Dua huruf pertama — SRP dan OCP — adalah yang paling sering dilanggar di codebase nyata, dan paling murah diperbaiki.
Episode ini membahas keduanya dengan satu skenario bersama: fitur checkout yang tumbuh liar, lalu direfactor di ketiga stack.
Definisi Robert C. Martin yang sering disalahpahami: sebuah class boleh punya banyak fungsi, tapi hanya boleh punya satu alasan untuk berubah — artinya ia hanya melayani satu aktor/stakeholder.
OrderService yang menghitung pajak (aturan finance), memanggil payment API (kontrak vendor), dan menulis SQL (skema DB) punya tiga alasan berubah dari tiga arah berbeda — meskipun semuanya "tentang order".
1. Kata "dan" saat menjelaskan class: "service ini validasi DAN kirim email DAN hitung diskon"
2. Import campur domain: ORM + HTTP client + mailer + queue dalam satu file
3. Test butuh mock > 3 dependency untuk satu method
4. Perubahan requirement finance menyentuh file yang juga berisiko untuk tim infraOCP: perilaku harus bisa ditambah tanpa mengubah kode yang sudah teruji. Mekanismenya selalu sama: definisikan abstraction, lalu tambahkan implementasi baru.
Ketika promo baru datang, CheckoutService tidak disentuh — cukup daftarkan rule baru. Perbandingkan dengan versi melanggar OCP: setiap promo = edit if-else di tengah kode checkout + retest semuanya.
Strategy (GoF, episode 7) adalah wujud pattern paling umum dari OCP.
Satu skenario, tiga stack: method checkout() yang melanggar SRP+OCP direfactor.
// SEBELUM: OrdersService.checkout() -> validasi+diskon+pembayaran+email
// SESUDAH:
@Injectable()
export class CheckoutService { // orchestrator saja (SRP)
constructor(
private pricing: PricingService, // hitung total + diskon
private payments: PaymentGateway, // interface (OCP utk vendor)
private ordersRepo: OrdersRepository,
) {}
async place(dto: CheckoutDto): Promise<Order> {
const total = await this.pricing.total(dto.items);
const receipt = await this.payments.charge(total);
return this.ordersRepo.create({ ...dto, total, receiptId: receipt.id });
}
}
// Diskon baru = tambahkan DiscountRule provider, PricingService tak tersentuh.Perhatikan benang merahnya: controller/handler hanya transport; logika dibagi ke unit kecil yang masing-masing punya satu alasan berubah; titik variasi (vendor pembayaran, aturan diskon) dijadikan interface.
Tip
Heuristik praktis SRP saat review PR: jika deskripsi perubahan menyebut dua stakeholder beda ("finance rules" + "infra migration"), hampir pasti ada class yang perlu dipisah.
Target outline: refactor satu fitur yang melanggar SRP+OCP di 3 stack.
Ambil lab episode 0, buat sengaja versi buruk lalu refactor:
1. Tulis CheckoutService/Handler/Controller versi monolitik:
- validasi inline, if-else diskon (percentage/fixed/bogo),
panggil payment langsung, tulis DB via ORM langsung, kirim email
2. Commit sebagai baseline "before"
3. Refactor:
a. Pecah jadi minimal 3 unit (SRP)
b. Jadikan diskon interface + 3 implementasi (OCP/strategy awal)
c. Controller tinggalkan transport saja
4. Commit "after" - bandingkan diff: kode bertambah sedikit,
tapi titik perubahan masa depan jadi lokal
5. Ujian: tambahkan promo "buy 2 get 1" TANPA menyentuh checkout core
- kalau berhasil, OCP kalian bekerjaRangkuman episode ini:
Episode 4 menuntaskan SOLID: LSP, ISP, DIP — termasuk DIP yang menjadi pintu masuk hexagonal architecture dan dependency injection. Sampai jumpa!