cv.cm

Pemrograman · Pola desain

Prinsip SOLID dijelaskan dengan contoh

6 menit baca

Jawaban singkat

SOLID adalah akronim dari lima prinsip desain berorientasi objek: Single responsibility, Open/closed, Liskov substitution, Interface segregation, dan Dependency inversion. Prinsip-prinsip ini dikumpulkan oleh Robert C. Martin, dan akronimnya dicetuskan oleh Michael Feathers. Bersama-sama, tujuannya membuat kode lebih mudah diubah: setiap kelas punya satu alasan untuk berubah, perilaku baru ditambahkan tanpa mengedit kode lama, subtipe aman sebagai pengganti, interface tetap kecil, dan kode tingkat tinggi bergantung pada abstraksi.

Arti SOLID

HurufPrinsipAturan singkat
SPrinsip single responsibility (SRP)Sebuah kelas hanya boleh punya satu alasan untuk berubah.
OPrinsip open/closed (OCP)Perangkat lunak harus terbuka untuk ekstensi tetapi tertutup untuk modifikasi.
LPrinsip Liskov substitution (LSP)Subtipe harus dapat dipakai di mana pun tipe dasarnya diharapkan.
IPrinsip interface segregation (ISP)Klien tidak boleh dipaksa bergantung pada method yang tidak mereka pakai.
DPrinsip dependency inversion (DIP)Bergantunglah pada abstraksi, bukan pada detail konkret.

Gagasan-gagasan ini lebih tua daripada akronimnya. Prinsip open/closed berasal dari buku Bertrand Meyer, Object-Oriented Software Construction (1988), dan prinsip substitusi dari keynote Barbara Liskov tahun 1987, Data Abstraction and Hierarchy. Robert C. Martin menguraikan kelima prinsip itu dalam tulisannya sekitar tahun 2000, dan Michael Feathers kemudian menyusunnya menjadi kata SOLID.

S: Prinsip single responsibility

Sebuah kelas harus punya satu alasan untuk berubah, artinya melayani satu aktor atau satu perhatian. Ketika sebuah kelas mencampur aturan bisnis, pemformatan, dan penyimpanan, perubahan pada salah satunya berisiko merusak yang lain.

Sebelum: satu kelas menghitung, memformat, dan menyimpan laporan.

class Report {
  constructor(private sales: number[]) {}
  total(): number { return this.sales.reduce((a, b) => a + b, 0); }
  toHtml(): string { return `<p>Total: ${this.total()}</p>`; }
  saveToFile(path: string): void { /* write this.toHtml() to disk */ }
}

Sesudah: setiap perhatian berada di unit kecilnya sendiri, sehingga format keluaran baru tidak menyentuh perhitungan.

class SalesReport {
  constructor(private sales: number[]) {}
  total(): number { return this.sales.reduce((a, b) => a + b, 0); }
}
class HtmlFormatter {
  format(r: SalesReport): string { return `<p>Total: ${r.total()}</p>`; }
}
class ReportWriter {
  save(content: string, path: string): void { /* write to disk */ }
}
const html = new HtmlFormatter().format(new SalesReport([10, 20, 5]));
console.log(html); // <p>Total: 35</p>

O: Prinsip open/closed

Anda seharusnya bisa menambah perilaku baru dengan menambah kode baru, bukan dengan mengedit kode yang sudah berfungsi dan teruji. Alat yang biasa dipakai adalah interface ditambah polimorfisme.

Sebelum: setiap bentuk baru berarti mengedit fungsi area.

type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number };
function area(s: Shape): number {
  if (s.kind === "circle") return Math.PI * s.r ** 2;
  if (s.kind === "square") return s.side ** 2;
  throw new Error("unknown shape");
}

Sesudah: bentuk baru adalah kelas baru; kode yang ada tidak berubah.

interface Shape { area(): number; }
class Circle implements Shape {
  constructor(private r: number) {}
  area(): number { return Math.PI * this.r ** 2; }
}
class Square implements Shape {
  constructor(private side: number) {}
  area(): number { return this.side ** 2; }
}
const totalArea = (shapes: Shape[]) => shapes.reduce((sum, s) => sum + s.area(), 0);
console.log(totalArea([new Square(2), new Square(3)])); // 13

L: Prinsip Liskov substitution

Jika kode bekerja dengan tipe dasar, kode itu harus tetap bekerja ketika diberi subtipe apa pun. Subtipe tidak boleh memperkuat prasyarat, memperlemah jaminan, atau melempar error di tempat yang tidak dilakukan tipe dasarnya.

Sebelum: masalah klasik persegi/persegi panjang. Square memperluas Rectangle tetapi melanggar ekspektasi bahwa lebar dan tinggi saling independen.

class Rectangle {
  constructor(public width: number, public height: number) {}
  setWidth(w: number): void { this.width = w; }
  setHeight(h: number): void { this.height = h; }
  area(): number { return this.width * this.height; }
}
class Square extends Rectangle {
  setWidth(w: number): void { this.width = w; this.height = w; }
  setHeight(h: number): void { this.width = h; this.height = h; }
}
function stretch(r: Rectangle): number {
  r.setWidth(5);
  r.setHeight(4);
  return r.area(); // caller expects 20
}
console.log(stretch(new Rectangle(1, 1))); // 20
console.log(stretch(new Square(1, 1)));    // 16, surprise

Sesudah: jangan memodelkan hubungan itu sebagai inheritance. Kedua tipe mengimplementasikan interface read-only bersama, dan tidak ada yang berharap bisa mengubah sisi persegi secara independen.

interface Shape { area(): number; }
class Rectangle implements Shape {
  constructor(readonly width: number, readonly height: number) {}
  area(): number { return this.width * this.height; }
}
class Square implements Shape {
  constructor(readonly side: number) {}
  area(): number { return this.side ** 2; }
}
console.log([new Rectangle(5, 4), new Square(4)].map((s) => s.area())); // [ 20, 16 ]

I: Prinsip interface segregation

Interface yang besar memaksa kelas mengimplementasikan method yang tidak bisa mereka dukung, biasanya dengan melempar error. Pecah menjadi interface kecil yang terfokus, dan biarkan setiap kelas hanya mengimplementasikan apa yang benar-benar dilakukannya.

Sebelum: printer sederhana harus berpura-pura bisa memindai dan mengirim faks.

interface Machine {
  print(doc: string): void;
  scan(): string;
  fax(doc: string): void;
}
class BasicPrinter implements Machine {
  print(doc: string): void { console.log("print", doc); }
  scan(): string { throw new Error("not supported"); }
  fax(doc: string): void { throw new Error("not supported"); }
}

Sesudah: interface peran yang kecil dan bisa digabungkan.

interface Printer { print(doc: string): void; }
interface Scanner { scan(): string; }
class BasicPrinter implements Printer {
  print(doc: string): void { console.log("print", doc); }
}
class OfficeMachine implements Printer, Scanner {
  print(doc: string): void { console.log("print", doc); }
  scan(): string { return "scanned page"; }
}
function printAll(p: Printer, docs: string[]): void { docs.forEach((d) => p.print(d)); }
printAll(new BasicPrinter(), ["a.pdf"]); // print a.pdf

D: Prinsip dependency inversion

Kebijakan tingkat tinggi tidak boleh bergantung pada detail tingkat rendah seperti database atau layanan email tertentu. Keduanya harus bergantung pada abstraksi, dan modul tingkat tinggi biasanya yang memiliki abstraksi itu. Dalam praktiknya, Anda memasukkan dependensi dari luar (dependency injection) alih-alih membuatnya di dalam.

Sebelum: layanan membuat klien database konkret, sehingga tidak bisa diuji atau diganti.

class MySqlUserStore {
  find(id: string): string { return "user " + id; }
}
class UserService {
  private store = new MySqlUserStore(); // hard-wired detail
  greet(id: string): string { return "Hello, " + this.store.find(id); }
}

Sesudah: layanan bergantung pada interface; implementasi apa pun dapat disuntikkan, termasuk tiruan dalam pengujian.

interface UserStore { find(id: string): string; }
class UserService {
  constructor(private store: UserStore) {}
  greet(id: string): string { return "Hello, " + this.store.find(id); }
}
class InMemoryUserStore implements UserStore {
  constructor(private users: Record<string, string>) {}
  find(id: string): string { return this.users[id] ?? "unknown"; }
}
const service = new UserService(new InMemoryUserStore({ u1: "Ada" }));
console.log(service.greet("u1")); // Hello, Ada

Kapan menerapkan SOLID, dan kapan tidak

Prinsip SOLID adalah pedoman untuk kode yang sering berubah, bukan hukum. Jika diterapkan membabi buta, hasilnya banyak kelas dan interface kecil dengan satu implementasi saja, yang membuat kode lebih sulit diikuti.

  • Terapkan ketika Anda merasakan masalah yang ditangani prinsip itu: kelas yang rusak setiap kali ada perubahan, pengujian yang butuh database sungguhan, atau pernyataan switch yang disalin ke banyak tempat.
  • Jangan menambahkan interface untuk setiap kelas sejak awal. Ekstrak interface ketika implementasi kedua atau test double muncul.
  • Pada skrip kecil dan prototipe, kode yang sederhana dan langsung biasanya lebih baik.
  • Banyak design pattern adalah penerapan konkret SOLID: pola Strategy dan pola Decorator mendukung open/closed, pola Adapter dan pola Facade membantu mengisolasi detail, dan pola Abstract Factory mendukung dependency inversion.

FAQ

Siapa yang menciptakan prinsip SOLID?

Robert C. Martin mengumpulkan dan menguraikan kelima prinsip, dengan bertumpu pada karya sebelumnya dari Bertrand Meyer (open/closed) dan Barbara Liskov (substitusi). Michael Feathers mencetuskan akronim SOLID.

Apakah SOLID berlaku untuk pemrograman fungsional?

Gagasannya bisa diterjemahkan: fungsi kecil dengan satu tujuan, menyusun perilaku alih-alih mengeditnya, dan meneruskan dependensi sebagai parameter. Rumusan berbasis kelas khusus untuk kode berorientasi objek.

Apa beda dependency inversion dan dependency injection?

Dependency inversion adalah prinsip bahwa kode tingkat tinggi harus bergantung pada abstraksi. Dependency injection adalah teknik yang menyediakan dependensi itu dari luar, misalnya lewat konstruktor.

Prinsip SOLID mana yang paling penting?

Tidak ada peringkat resmi. Single responsibility dan dependency inversion biasanya memberi manfaat tercepat, karena membuat kode lebih mudah dipahami dan diuji.

Apakah buruk melanggar prinsip SOLID?

Tidak selalu. Setiap prinsip punya biaya berupa kode tambahan. Langgar satu prinsip dengan sengaja ketika desain yang lebih sederhana lebih jelas dan perubahan yang dilindunginya tidak mungkin terjadi.

Semua tutorial pemrograman