Lesson 0008 · Hari 2 · Desain · ±14 menit

SOLID, dari kode Product Catalog

SRP dan DIP dibahas paling dalam; OCP, LSP, dan ISP lewat satu contoh masing-masing

Target lesson ini: Anda bisa menjawab pertanyaan interview "Prinsip SOLID mana yang paling sering Anda terapkan?" dalam 60 detik, dengan SRP dan DIP plus contoh dari Product Catalog. Sebagai bonus, Anda bisa menunjuk prinsip mana yang dilanggar oleh sebuah potongan kode.

1. Lima prinsip, lima kalimat

Rumusan di bawah adalah versi ringkas dari Robert C. Martin sendiri, yang memperkenalkan SOLID. [4] Hafalkan kolom "Contoh"; itu yang membuat jawaban Anda terdengar berpengalaman, bukan hafalan.

PrinsipSatu kalimat [4]Contoh di Product CatalogPadanan Node/React
Single Responsibility"Gather together the things that change for the same reasons. Separate things that change for different reasons."Controller = HTTP, Service = aturan bisnis, Repository = akses dataRoute handler Express yang tipis; logika di services/
Open/Closed"A Module should be open for extension but closed for modification."IDiscountStrategy: diskon baru = class baruObject map strategi { member: p => p * 0.9 }
Liskov Substitution"A program that uses an interface must not be confused by an implementation of that interface."IProductRepository versi SQL Server dan in-memory berperilaku samaFake repo in-memory di Jest yang setia pada kontrak
Interface Segregation"Keep interfaces small so that users don't end up depending on things they don't need."IProductReader terpisah dari IProductWriterProps komponen Pick<Store, "products">, bukan seluruh store
Dependency Inversion"Depend in the direction of abstraction. High level modules should not depend upon low level details."ProductService bergantung pada IProductRepository, bukan AppDbContextProvider NestJS, atau createProductService(repo)

2. SRP: satu alasan untuk berubah

Martin: "each software module should have one and only one reason to change". Lalu ia menjelaskan apa itu "alasan": "This principle is about people." [2] Jadi pertanyaannya bukan "berapa method?", tapi "siapa saja yang bisa meminta class ini diubah?".

// ✗ Tiga pihak bisa meminta perubahan: tim produk, tim laporan, tim marketing
public class ProductService(IProductRepository repo)
{
    public Task CreateAsync(CreateProductRequest r)  { /* harga > 0, stok ≥ 0, nama unik */ }
    public string ExportToCsv(IEnumerable<Product> ps) { /* urutan kolom laporan */ }
    public void SendLowStockEmail(Product p)          { /* template HTML + SMTP */ }
}

// ✓ Satu class per alasan berubah
public class ProductService(IProductRepository repo)        { /* aturan bisnis saja */ }
public class ProductCsvExporter                             { /* format laporan */ }
public class StockAlertNotifier(IEmailSender email)         { /* isi email */ }

Di level arsitektur, SRP yang sama memberi Anda pembagian layer dari lesson 0001: Microsoft menyebut SRP "similar to separation of concerns", dengan presentation, data access, dan business logic dipisah. [1] Keuntungannya juga mereka tulis: "Adding new classes is always safer than changing existing classes, since no code yet depends on the new classes."

Gejala yang bisa Anda sebut di interview: constructor yang gemuk. "If a class has many injected dependencies, it might be a sign that the class has too many responsibilities and violates the Single Responsibility Principle (SRP)." [5] Padanan Express: route handler 200 baris yang memvalidasi, query Prisma, membuat CSV, dan mengirim email sekaligus.

3. DIP: arah dependency ke abstraksi

Tanpa DIP, dependency compile-time mengikuti arah eksekusi: Service memanggil Repository, jadi Service bergantung pada Repository. Dengan DIP, Service memanggil interface, dan Repository-lah yang bergantung pada interface itu: B "depend[s] on an interface controlled by A at compile time (thus, inverting the typical compile-time dependency)". [1]

ProductService→IProductRepository←ProductRepository

Panah = "bergantung pada" saat compile. "At run time, the flow of program execution remains unchanged": Service tetap memanggil Repository. Yang terbalik hanya arah dependency-nya. [1]

// ✗ Service (level tinggi) membuat detailnya sendiri
public class ProductService
{
    private readonly ProductRepository _repo = new ProductRepository(new AppDbContext(/* … */));
}

// ✓ Service hanya kenal abstraksi; composition root memilih implementasinya
public class ProductService(IProductRepository repo) : IProductService { /* … */ }

// Program.cs
builder.Services.AddScoped<IProductRepository, ProductRepository>();

Kenapa ini jawaban terkuat Anda

Versi ✗ "is difficult to unit test because it directly creates" dependency-nya; versi ✓ memakai interface, registrasi di container, dan injeksi. [5] Hasilnya, unit test cukup new ProductService(mockRepo.Object) dengan Moq, persis seperti memberi jest.fn() ke service di Node. Microsoft merangkumnya: aplikasi hasil DIP "are more testable, modular, and maintainable". [1] Itulah sebabnya ROADMAP menyarankan menjawab "SRP dan DIP": keduanya yang membuat test Hari 2 mungkin.

Jebakan istilah: Dependency Inversion adalah prinsip (arah dependency). Dependency Injection adalah tekniknya: "The practice of dependency injection is made possible by following the dependency inversion principle." [1] DI container ASP.NET Core dan composition root (Program.cs) adalah alat untuk menerapkan DIP.

4. OCP, LSP, ISP: satu contoh masing-masing

OCP: tambah perilaku tanpa membuka class lama

"You should be able to extend the behavior of a system without having to modify that system." [3]

public interface IDiscountStrategy { decimal Apply(decimal price); }
public class NoDiscount     : IDiscountStrategy { public decimal Apply(decimal p) => p; }
public class MemberDiscount : IDiscountStrategy { public decimal Apply(decimal p) => p * 0.9m; }

public class PriceCalculator(IDiscountStrategy strategy)
{
    public decimal Final(decimal price) => strategy.Apply(price);   // tidak berubah saat ada promo baru
}

Pelanggarannya: switch (customerType) yang bertambah satu case setiap ada promo.

LSP: implementasi tidak boleh mengejutkan

"A program that uses an interface must not be confused by an implementation of that interface." [4] Kalau GetByIdAsync versi SQL Server mengembalikan null untuk id yang tidak ada, versi in-memory untuk test juga harus begitu. Dua gejala klasik: implementasi yang melempar NotImplementedException, atau melempar exception di tempat kontraknya mengembalikan null.

ISP: interface sekecil kebutuhan konsumennya

"Keep interfaces small so that users don't end up depending on things they don't need." [4]

public interface IProductReader { Task<Product?> GetByIdAsync(int id); Task<IReadOnlyList<Product>> GetAllAsync(); }
public interface IProductWriter { Task AddAsync(Product p); Task SaveChangesAsync(); }
public class ProductRepository(AppDbContext db) : IProductReader, IProductWriter { /* … */ }

public class ProductReportService(IProductReader reader) { /* hanya membaca */ }

ISP dan LSP sering muncul bersama: ReadOnlyProductRepository yang melempar NotImplementedException di AddAsync melanggar LSP, dan akar masalahnya adalah interface yang terlalu besar (ISP).

Latihan 1: Prinsip mana yang dilanggar?

Baca potongan kodenya, lalu pilih satu prinsip. Jangan scroll ke atas dulu. Soal ini muncul lagi, diacak, di review campuran 0012.

Kuis di lesson ini adalah feedback cepat, bukan bukti penguasaan; yang dicatat hanya hasil di review campuran 0012.

Latihan 2: Alasannya

Pertanyaan "kenapa" yang biasa menyusul setelah Anda menyebut sebuah prinsip.

Latihan 3: Ucapkan dengan suara keras

Pasang timer 60 detik dan jawab dengan suara keras, seolah sedang interview. Jangan buka jawaban contoh sebelum selesai.

"Prinsip SOLID mana yang paling sering Anda terapkan?"
Bandingkan dengan jawaban contoh dan checklist-nya

"Paling sering SRP dan DIP, karena keduanya yang membuat kode saya bisa di-unit-test. SRP: satu class, satu alasan untuk berubah. Di Product Catalog, Controller hanya mengurus HTTP, ProductService memegang aturan bisnis seperti harga harus lebih dari nol, dan Repository mengurus akses data lewat EF Core. Kalau ada permintaan export CSV, saya buat class baru, bukan menambah method di Service. DIP: ProductService bergantung pada IProductRepository, bukan pada AppDbContext atau new ProductRepository(). Implementasinya didaftarkan di Program.cs lewat AddScoped, dan DI container menyuntikkannya lewat constructor. Hasilnya, di unit test saya cukup memberi mock IProductRepository dengan Moq, mirip jest.fn() di Node. Tandanya SRP mulai dilanggar biasanya constructor dengan terlalu banyak dependency."

Checklist: apakah Anda menyebut…

  • SRP dan DIP, dengan alasan: keduanya membuat kode bisa di-unit-test?
  • SRP = satu alasan untuk berubah, dengan pembagian Controller / Service / Repository?
  • contoh SRP konkret (export CSV atau email jadi class terpisah)?
  • DIP = Service bergantung pada IProductRepository, bukan class konkret atau new?
  • implementasi didaftarkan di Program.cs (composition root) dan diinjeksi lewat constructor?
  • unit test memakai mock (Moq), dengan padanan jest.fn() atau gejala constructor gemuk?

Lulus kalau minimal 4 dari 6. Kalau kurang, ulangi besok pagi sebelum review 0012 (spacing).

Bonus (90 detik)

"Sebutkan kelima prinsip SOLID, masing-masing dengan satu contoh dari Product Catalog."
Bandingkan dengan jawaban contoh dan checklist-nya

"S: Controller hanya HTTP, Service aturan bisnis, Repository akses data. O: PriceCalculator menerima IDiscountStrategy, jadi diskon baru cukup class baru. L: implementasi SQL Server dan in-memory dari IProductRepository berperilaku sama, misalnya sama-sama mengembalikan null kalau id tidak ada. I: konsumen read-only bergantung pada IProductReader, bukan interface gemuk. D: ProductService bergantung pada IProductRepository yang disuntikkan DI container dari Program.cs."

Checklist:

  • SRP dengan pembagian Controller / Service / Repository
  • OCP dengan IDiscountStrategy tanpa mengubah PriceCalculator
  • LSP: implementasi repository saling menggantikan tanpa perilaku mengejutkan
  • ISP: interface kecil seperti IProductReader / IProductWriter
  • DIP: Service bergantung pada abstraksi yang diinjeksi lewat DI
  • kelimanya urut dan selesai dalam 90 detik tanpa melihat catatan
Bacaan utama (±10 menit) Microsoft Learn: Architectural principles. Baca bagian "Dependency inversion", "Explicit dependencies", dan "Single responsibility". Untuk OCP/LSP/ISP, lanjut ke Robert C. Martin: Solid Relevance (pendek).

Ringkasan untuk dicetak: Reference: Prinsip SOLID di Product Catalog · Istilah: GLOSSARY.md

Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "apa bedanya SRP dengan separation of concerns?" atau "kalau Repository di atas EF Core, apakah itu masih DIP?" (yang terakhir dibahas lagi di lesson 0011).

Sumber: [1] Microsoft Learn: Architectural principles · [2] Robert C. Martin: The Single Responsibility Principle · [3] Robert C. Martin: The Open Closed Principle · [4] Robert C. Martin: Solid Relevance · [5] Dependency injection in ASP.NET Core