SOLID: My Interview Nightmare Turned Cheat Sheet

Jotted down by Jay after a rough interview.

Imagine this: I'm in the middle of a super intense interview for a sick engineering role. Things are going great. I'm casually dropping architectures and AI pipelines. Then, out of nowhere, the interviewer drops the most basic, classic question of all time:

"Can you explain the SOLID principles to me?"

Brain. Freeze. 🧊

I literally use these principles intuitively every single day, but suddenly, trying to put them into words felt like explaining how to ride a bike to an alien. I stumbled, mixed up Liskov with Interface Segregation, and basically made a total fool of myself.

So, I'm tossing this cheat sheet into my notebook so I never get caught lacking again, and hopefully hide my pain in a fun little cheat sheet for any junior engineers (or embarrassed seniors) out there!

1. Single Responsibility (SRP)

The Rule: A class should do exactly one thing. It should only have one reason to change.

Imagine you have an employee at a restaurant. If that employee is cooking the food, taking the orders, and doing the taxes, they are going to do a terrible job at all three. Your code is the same.

// ❌ Bad: The "Do It All" Class
class UserManager {
  createUser(data) { /* DB logic */ }
  sendWelcomeEmail(user) { /* SMTP logic */ }
}
Whoa! Email logic inside a DB manager?
// ✅ Good: Split the jobs!
class UserRepository {
  createUser(data) { /* DB logic only */ }
}

class EmailService {
  sendWelcomeEmail(user) { /* SMTP logic only */ }
}
Much better.
Clean & focused.

2. Open-Closed (OCP)

The Rule: Open for extension, closed for modification.

Think of your code like a smartphone. When you want new features, you don't open up the hardware and solder new chips (modification). You just download a new App (extension)!

// ❌ Bad: Every new shape breaks the class!
class AreaCalculator {
  calculate(shape) {
    if (shape.type === 'circle') return Math.PI * shape.r ** 2;
    if (shape.type === 'square') return shape.side ** 2;
    // adding a triangle means editing this file! :(
  }
}
Endless IF statements!

Instead, let the shapes calculate their own areas by implementing a common interface. The calculator just calls shape.getArea() unconditionally.

// ✅ Good: Open for extension!
class AreaCalculator {
  // Can sum areas of ANY shapes without modifying this method!
  calculateTotal(shapes) {
    return shapes.reduce((total, shape) => total + shape.getArea(), 0);
  }
}

class Circle {
  constructor(r) { this.r = r; }
  getArea() { return Math.PI * this.r ** 2; }
}

class Square {
  constructor(side) { this.side = side; }
  getArea() { return this.side ** 2; } // Adding this doesn't break Calculator!
}
New shape? Just add a class!

3. Liskov Substitution (LSP)

The Rule: If it looks like a duck and quacks like a duck, but needs batteries, you have the wrong abstraction!

More formally: If class B inherits from class A, you should be able to pass B into anything that expects A without breaking the app.

class Bird { fly() { ... } }

// ❌ Bad: Penguins can't fly!
class Penguin extends Bird {
  fly() { throw new Error("I swim, bro!"); }
}
LSP Violation! Boom!💥

If a piece of code loops through a list of Birds and calls fly(), the Penguin breaks the app. The fix? Create a narrower base class, or interfaces like IFlyable and ISwimmable.

// ✅ Good: Proper abstractions
interface IFlyable { fly(); }
interface ISwimmable { swim(); }

class Duck implements IFlyable, ISwimmable {
  fly() { ... }
  swim() { ... }
}

class Penguin implements ISwimmable {
  swim() { ... }
}
Penguins are safe now!

4. Interface Segregation (ISP)

The Rule: Don't force clients to depend on methods they don't use.

Imagine going to a restaurant and the waiter hands you a 500-page book that contains the menu, the staff payroll, and the restaurant's blueprint. You just want the menu!

// ❌ Bad: The "God" Interface
interface SmartDevice {
  print();
  fax();
  scan();
}
A basic printer can't fax!

Break it down into IPrinter, IFax, and IScanner. A basic printer only implements IPrinter, keeping things neat.

// ✅ Good: Segregated Interfaces
interface IPrinter { print(); }
interface IScanner { scan(); }

class BasicPrinter implements IPrinter {
  print() { console.log("Printing!"); } 
  // Doesn't need to fake a fax method!
}
Small interfaces!

5. Dependency Inversion (DIP)

The Rule: Rely on abstractions (interfaces), not concrete implementations (classes).

If you hardcode an MySQLDatabase inside your API service, you are glued to MySQL. If your boss asks to switch to MongoDB tomorrow, you'll be crying in the bathroom.

// ❌ Bad: Tightly coupled
class PaymentService {
  constructor() {
    this.db = new MySQLDatabase(); // Glued!
  }
}
Glued to MySQL forever!
// ✅ Good: Depending on an abstraction
class PaymentService {
  constructor(databaseInterface) {
    this.db = databaseInterface;
  }
}
Injected at runtime! Easy testing!

By injecting an interface, you can pass in a mock database during testing, or swap the entire database engine without touching the PaymentService logic.

Conclusion

And there you have it! Next time I'm in an interview and they ask about SOLID, I won't freeze. I'll just picture penguins breaking apps and waitresses handing out blueprints. Hope this helps you too!

SOLID: Mimpi Buruk Interview Berubah Jadi Contekan

Ditulis Jay abis interview yg bikin deg-degan.

Bayangin gini: Gue lagi duduk di interview buat posisi engineer idaman. Awalnya semua aman dan asik banget. Gue ngomongin soal arsitektur cloud, pipeline AI, lancar jaya deh pokoknya. Terus tiba-tiba, interviewernya nanya pertanyaan paling klasik sedunia:

"Bisa tolong jelasin prinsip SOLID ke saya?"

Otak. Nge-blank. 🧊

Sumpah, gue tuh pake prinsip ini hampir tiap hari. Tapi pas disuruh ngejelasin pake kata-kata, rasanya seribet ngejelasin cara naik sepeda ke alien. Gue belepotan, Liskov ketuker sama Interface Segregation, pokoknya ancur banget dah.

Makanya, gue nulis corat-coret ini di buku catatan biar gue nggak pernah kena mental lagi pas nge-blank, plus semoga bisa jadi contekan asik buat junior-junior (atau senior yang pernah malu-maluin juga) di luar sana!

1. Single Responsibility (SRP)

Aturannya: Satu class itu cuma boleh ngerjain satu hal doang. Titik.

Coba lo bayangin ada satu karyawan restoran. Kalau dia yang masak, angkat telpon pesenan, dan bikin laporan pajak bulanan, kerjaannya pasti bakal berantakan semua. Kode lo juga gitu, bro.

// ❌ Buruk: Kelas "Kerjakan Semua"
class UserManager {
  createUser(data) { /* Logika DB */ }
  sendWelcomeEmail(user) { /* Logika SMTP */ }
}
Wow! Logika Email di dalam DB manager?
// ✅ Baik: Pisahkan tugasnya!
class UserRepository {
  createUser(data) { /* Hanya logika DB */ }
}

class EmailService {
  sendWelcomeEmail(user) { /* Hanya logika SMTP */ }
}
Jauh lebih baik.
Rapi & fokus.

2. Open-Closed (OCP)

Aturannya: Boleh ditambahin fiturnya (extension), tapi jangan ngutak-ngatik daleman kodenya (modification).

Anggap kode lo itu kayak HP. Kalo lo butuh fitur atau fungsi baru, lo nggak bakal bongkar mesin HP-nya terus nyolder chip baru kan? Lo tinggal download App baru aja di AppStore!

// ❌ Buruk: Bentuk baru merusak kelas!
class AreaCalculator {
  calculate(shape) {
    if (shape.type === 'circle') return Math.PI * shape.r ** 2;
    if (shape.type === 'square') return shape.side ** 2;
    // menambah segitiga berarti harus mengedit file ini! :(
  }
}
Statement IF tanpa henti!

Sebaliknya, biarkan setiap "shape" menghitung luasnya sendiri dengan mengimplementasikan antarmuka (interface) umum. Kalkulator hanya perlu memanggil shape.getArea() tanpa syarat.

// ✅ Baik: Terbuka untuk ekstensi!
class AreaCalculator {
  // Bisa hitung total semua luas tanpa ngedit fungsi ini sama sekali!
  calculateTotal(shapes) {
    return shapes.reduce((total, shape) => total + shape.getArea(), 0);
  }
}

class Circle {
  constructor(r) { this.r = r; }
  getArea() { return Math.PI * this.r ** 2; }
}

class Square {
  constructor(side) { this.side = side; }
  getArea() { return this.side ** 2; } // Nambah ini ga perlu ngutak-ngatik Kalkulator!
}
Bentuk baru? Cukup bikin class baru!

3. Liskov Substitution (LSP)

Aturannya: Kalo kelihatannya kayak bebek, suaranya kayak bebek, tapi kelonggaran "bebek" itu butuh baterai buat hidup... fiks lo salah bikin abstraksi!

Intinya gini: Kalo class B itu keturunan dari class A, lo harus bisa naro B di posisi manapun yang butuh A, tanpa bikin satu aplikasi lo error (force-close).

class Bird { fly() { ... } }

// ❌ Buruk: Penguin tidak bisa terbang!
class Penguin extends Bird {
  fly() { throw new Error("Saya cuma bisa berenang, bro!"); }
}
Pelanggaran LSP! Boom!💥

Jika kode me-looping daftar objek Bird dan memanggil fly(), class Penguin akan merusak aplikasi. Solusinya? Buat kelas dasar atau interface yang lebih sempit seperti IFlyable dan ISwimmable.

// ✅ Baik: Abstraksi yang Benar
interface IFlyable { fly(); }
interface ISwimmable { swim(); }

class Duck implements IFlyable, ISwimmable {
  fly() { ... }
  swim() { ... }
}

class Penguin implements ISwimmable {
  swim() { ... }
}
Penguin sekarang aman!

4. Interface Segregation (ISP)

Aturannya: Jangan paksa fungsi atau class lo buat bergantung sama method yang sama sekali nggak mereka pake.

Bayangin lo nongkrong di cafe, terus pelayannya ngasih lo buku 500 halaman yang isinya menu, gaji karyawan cafe, sampe blueprint desain bangunannya. Bikin ribet kan? Padahal lo cuma butuh liat halam menu minuman doang!

// ❌ Bad: Interface "Dewa"
interface SmartDevice {
  print();
  fax();
  scan();
}
Printer kecil gak bisa fax!

Pecah menjadi IPrinter, IFax, dan IScanner. Sebuah printer dasar hanya perlu mengimplementasi IPrinter, sehingga kode tetap ramping.

// ✅ Baik: Interface Dipisah
interface IPrinter { print(); }
interface IScanner { scan(); }

class BasicPrinter implements IPrinter {
  print() { console.log("Nge-print!"); } 
  // Nggak usah maksa bikin method fax palsu!
}
Bikin interface yg kecil-kecil aja!

5. Dependency Inversion (DIP)

Aturannya: Bergantunglah sama abstraksi (interface), bukan implementasi aslinya (kelas kongkrit).

Kalo lo sengaja nge-hardcode MySQLDatabase di dalem API service, selamanya lo bakal kejebak dipeluk rindu sama MySQL. Begitu besok atasan lo minta migrasi ke MongoDB, lo bakal nangis guling-guling di toilet.

// ❌ Bad: Sangat terikat (Tightly coupled)
class PaymentService {
  constructor() {
    this.db = new MySQLDatabase(); // Nempel permanen!
  }
}
Terikat di MySQL selamanya!
// ✅ Good: Bergantung pada abstraksi
class PaymentService {
  constructor(databaseInterface) {
    this.db = databaseInterface;
  }
}
Injeksi saat runtime! Mudah di-test!

Dengan menginjeksi sebuah interface, Anda bisa meneruskan mock database selama unit test, atau menukar seluruh mesin database tanpa menyentuh logika dalam PaymentService.

Kesimpulan

Udah sih, gitu aja sebenernya. Amit-amit besok gue dapet pertanyaan soal SOLID lagi pas interview, gue jamin kaga bakal nge-blank lagi. Tinggal inget-inget aja soal si penguin yang bikin aplikasi meledak, atau pelayan cafe nyebelin yang ngasih buku cetak biru gedung. Moga contekan iseng gue ini bisa ngebantu lo juga ya, bro!