SOLID di Go

1. Single Responsibility Principle (SRP)

Prinsip: Satu class/module hanya memiliki satu tanggung jawab. Aplikasi Dunia Nyata: Sistem manajemen pengguna di aplikasi e-commerce.

// ❌ Melanggar SRP: User struct menangani data & validasi
type User struct {
    Name  string
    Email string
}

func (u *User) Save() error {
    // Logika simpan ke database
    return nil
}

func (u *User) ValidateEmail() bool {
    // Logika validasi email
    return strings.Contains(u.Email, "@")
}

// ✅ Memenuhi SRP: Pemisahan tanggung jawab
type User struct {
    Name  string
    Email string
}

type UserRepository struct{} // Tanggung jawab: akses data
func (r *UserRepository) Save(u *User) error { /* ... */ }

type UserValidator struct{} // Tanggung jawab: validasi
func (v *UserValidator) ValidateEmail(u *User) bool { /* ... */ }

Keuntungan:

  • Perubahan validasi tidak memengaruhi logika penyimpanan data.

  • Kode lebih mudah di-maintain dan diuji.


2. Open/Closed Principle (OCP)

Prinsip: Software entities terbuka untuk ekstensi, tetapi tertutup untuk modifikasi. Aplikasi Dunia Nyata: Sistem pembayaran dengan metode pembayaran yang beragam.

// ❌ Melanggar OCP: Menambah metode pembayaran baru = modifikasi PaymentProcessor
type PaymentProcessor struct{}

func (p *PaymentProcessor) ProcessPayment(method string, amount float64) {
    switch method {
    case "credit_card":
        // Logika pembayaran kartu kredit
    case "paypal":
        // Logika pembayaran PayPal
    }
}

// ✅ Memenuhi OCP: Gunakan interface untuk ekstensi
type PaymentMethod interface {
    Process(amount float64) error
}

type CreditCard struct{} // Implementasi baru tanpa ubah PaymentProcessor
func (c *CreditCard) Process(amount float64) error { /* ... */ }

type PayPal struct{} // Implementasi baru
func (p *PayPal) Process(amount float64) error { /* ... */ }

type PaymentProcessor struct {
    methods []PaymentMethod
}

func (p *PaymentProcessor) ProcessPayment(amount float64) error {
    for _, method := range p.methods {
        if err := method.Process(amount); err != nil {
            return err
        }
    }
    return nil
}

Keuntungan:

  • Tambah metode pembayaran baru (misal: GoPay) tanpa ubah kode PaymentProcessor.

  • Kode lebih fleksibel terhadap perubahan bisnis.


3. Liskov Substitution Principle (LSP)

LSP adalah prinsip ke-3 dari SOLID. Intinya:

Sebuah subclass atau implementasi harus bisa menggantikan superclass/interface tanpa mengubah perilaku yang diharapkan dalam program.

Artinya, kalau kita punya fungsi yang bekerja dengan interface X, maka semua implementasi dari interface X harus bisa dipakai tanpa bikin bug atau perilaku aneh.


Analogi sederhana

Bayangkan ada interface Bird dengan method Fly(). Kalau kita punya Sparrow (burung pipit), dia bisa terbang. Kalau kita tambahkan Penguin, dia tidak bisa terbang.

Kalau kita masukkan Penguin ke dalam fungsi yang ekspektasinya semua Bird bisa Fly(), pasti ada masalah → inilah pelanggaran LSP.


Contoh di Golang

Contoh yang Melanggar LSP

package main

import "fmt"

type Bird interface {
	Fly()
}

type Sparrow struct{}

func (s Sparrow) Fly() {
	fmt.Println("Sparrow terbang...")
}

type Penguin struct{}

func (p Penguin) Fly() {
	panic("Penguin tidak bisa terbang!") // ❌ pelanggaran LSP
}

func MakeBirdFly(b Bird) {
	b.Fly()
}

func main() {
	var s Bird = Sparrow{}
	var p Bird = Penguin{}

	MakeBirdFly(s) // OK
	MakeBirdFly(p) // runtime panic ❌
}

Kenapa ini melanggar LSP? Karena Penguin secara konsep tidak sesuai dengan kontrak Bird yang punya Fly(). Ketika dipakai, malah menyebabkan error → program tidak bisa bekerja dengan substitusi Penguin.


Contoh yang Benar (Mematuhi LSP) Pisahkan interface berdasarkan kemampuan:

package main

import "fmt"

// Bird umum
type Bird interface {
	Walk()
}

type FlyingBird interface {
	Bird
	Fly()
}

type Sparrow struct{}

func (s Sparrow) Walk() {
	fmt.Println("Sparrow berjalan...")
}
func (s Sparrow) Fly() {
	fmt.Println("Sparrow terbang...")
}

type Penguin struct{}

func (p Penguin) Walk() {
	fmt.Println("Penguin berjalan...")
}

func MakeBirdWalk(b Bird) {
	b.Walk()
}

func MakeBirdFly(b FlyingBird) {
	b.Fly()
}

func main() {
	var s FlyingBird = Sparrow{}
	var p Bird = Penguin{}

	MakeBirdWalk(s) // ✅ Sparrow bisa jalan
	MakeBirdFly(s)  // ✅ Sparrow bisa terbang

	MakeBirdWalk(p) // ✅ Penguin bisa jalan
	// MakeBirdFly(p) // ❌ compile error, bagus! Tidak bisa karena penguin bukan FlyingBird
}

Dengan memisahkan interface, Penguin tetap bisa jadi Bird tanpa memaksa punya Fly(). Program lebih aman dan konsisten dengan LSP.


Inti LSP

  1. Gunakan interface kecil dan spesifik (mirip Interface Segregation Principle).

  2. Jangan memaksa implementasi melanggar kontrak interface.

  3. Fungsi yang menerima interface harus bekerja untuk semua implementasinya tanpa perlu tahu implementasi spesifiknya.


Jadi kunci LSP di Golang: jangan bikin interface yang memaksa implementasi melakukan hal yang tidak cocok dengan nature-nya.


Mau saya bikinkan contoh lain yang lebih ke use-case real (misalnya transaksi pembayaran, user repository, dsb.) biar lebih kebayang selain contoh "Bird"?


4. Interface Segregation Principle (ISP)

Prinsip: Klien tidak boleh dipaksa bergantung pada interface yang tidak digunakan. Aplikasi Dunia Nyata: Sistem IoT dengan device yang memiliki fitur berbeda.

// ❌ Melanggar ISP: Device dengan fitur terlalu banyak
type SmartDevice interface {
    TurnOn()
    TurnOff()
    GetTemperature() float64 // Tidak semua device punya sensor suhu
    CaptureImage() []byte     // Tidak semua device punya kamera
}

type SmartBulb struct{} // ❌ Dipaksa implementasi GetTemperature & CaptureImage
func (b *SmartBulb) TurnOn() {}
func (b *SmartBulb) TurnOff() {}
func (b *SmartBulb) GetTemperature() float64 { return 0 } // Tidak relevan
func (b *SmartBulb) CaptureImage() []byte { return nil } // Tidak relevan

// ✅ Memenuhi ISP: Pecah interface menjadi fitur spesifik
type PowerSwitch interface {
    TurnOn()
    TurnOff()
}

type TemperatureSensor interface {
    GetTemperature() float64
}

type Camera interface {
    CaptureImage() []byte
}

type SmartBulb struct{} // Hanya implementasi yang relevan
func (b *SmartBulb) TurnOn() {}
func (b *SmartBulb) TurnOff() {}

type Thermostat struct{} // Implementasi fitur yang dimiliki
func (t *Thermostat) TurnOn() {}
func (t *Thermostat) TurnOff() {}
func (t *Thermostat) GetTemperature() float64 { return 25.5 }

Keuntungan:

  • Device hanya implementasi fitur yang dimiliki.

  • Menghindari "noise" dari metode tidak relevan.


5. Dependency Inversion Principle (DIP)

Prinsip:

  • High-level module tidak bergantung pada low-level module.

  • Keduanya bergantung pada abstraksi (interface).

  • Abstraksi tidak bergantung pada detail.

Aplikasi Dunia Nyata: Service layer yang bergantung pada repository.

// ❌ Melanggar DIP: OrderService langsung bergantung pada MySQLRepository
type MySQLRepository struct{}

func (r *MySQLRepository) SaveOrder(order Order) error {
    // Simpan ke MySQL
    return nil
}

type OrderService struct {
    repo *MySQLRepository // Ketergantungan langsung ke implementasi
}

func (s *OrderService) CreateOrder(order Order) error {
    return s.repo.SaveOrder(order)
}

// ✅ Memenuhi DIP: Gunakan interface sebagai abstraksi
type OrderRepository interface {
    SaveOrder(order Order) error
}

type MySQLRepository struct{} // Low-level module
func (r *MySQLRepository) SaveOrder(order Order) error { /* ... */ }

type OrderService struct { // High-level module
    repo OrderRepository // Bergantung pada abstraksi
}

func NewOrderService(repo OrderRepository) *OrderService {
    return &OrderService{repo: repo}
}

func (s *OrderService) CreateOrder(order Order) error {
    return s.repo.SaveOrder(order)
}

Keuntungan:

  • Mudah ganti database (misal dari MySQL ke PostgreSQL) tanpa ubah OrderService.

  • Kode lebih mudah diuji (bisa mock OrderRepository).


Kesimpulan: Manfaat SOLID di Go

  1. Maintainability: Perubahan pada satu fitur tidak memengaruhi fitur lain (SRP, ISP).

  2. Extensibility: Tambah fitur baru tanpa ubah kode existing (OCP, DIP).

  3. Reliability: Substitusi komponen aman tanpa error (LSP).

  4. Testability: Dependency injection (DIP) memudahkan pembuatan unit test.

  5. Scalability: Arsitektur modular mendukung pertumbuhan aplikasi.

Best Practice di Go:

  • Gunakan interface untuk abstraksi (OCP, DIP, ISP).

  • Hindari struct dengan tanggung jawab ganda (SRP).

  • Pastikan implementasi interface konsisten (LSP).

  • Lakukan dependency injection melalui constructor/parameter.

Last updated