Interface adalah kumpulan method yang mendefinisikan perilaku yang harus diimplementasikan oleh sebuah tipe. Dalam bahasa Go, interface sangat berguna untuk mendukung prinsip pemrograman berbasis kontrak dan memudahkan pengujian serta pemisahan tanggung jawab.

Masalah Umum: Fat Interface dan Interface Pollution

Salah satu masalah yang sering muncul adalah “interface pollution” atau “fat interface”, yaitu ketika sebuah interface memiliki terlalu banyak method. Hal ini menyebabkan setiap tipe yang mengimplementasikan interface tersebut harus mengimplementasikan semua method, meskipun tidak semuanya dibutuhkan. Akibatnya, interface menjadi sulit dipelihara dan tidak fleksibel.

Contoh kasus pada repository:

type UserRepository interface {
    FindAll() (*[]User, error)
    FindByID(ID string) (*User, error)
    FindByUsername(username string) (*User, error)
    FindByEmail(email string) (*User, error)
    AddUser(username string, email string) (*User, error)
}

Jika ada kebutuhan baru, misalnya menambah method DeleteUser, maka interface akan semakin membesar. Hal ini tidak sesuai dengan prinsip Interface Segregation Principle (ISP) yang menyarankan agar interface tetap kecil dan spesifik.

Solusi: Memecah Interface dan Embedding Interface

Untuk mengatasi masalah ini, kita dapat memecah interface menjadi beberapa bagian kecil yang lebih spesifik, lalu menggabungkannya menggunakan teknik embedding interface. Dengan cara ini, tipe hanya perlu mengimplementasikan interface yang benar-benar dibutuhkan.

type UserRetriever interface {
    FindAll() (*[]User, error)
    FindByID(ID string) (*User, error)
    FindByUsername(username string) (*User, error)
    FindByEmail(email string) (*User, error)
}

type UserAdder interface {
    AddUser(username string, email string) (*User, error)
}

type UserRepository interface {
    UserRetriever
    UserAdder
}

Dengan pemisahan ini, jika ada tipe yang hanya perlu mengambil data user, cukup mengimplementasikan UserRetriever saja.

Masalah Lain: Bloated Interface

Masalah lain yang sering terjadi adalah “bloated interface”, yaitu ketika sebuah interface memaksa tipe untuk mengimplementasikan method yang tidak relevan. Contoh klasiknya adalah pada perangkat printer:

Misalkan ada dua jenis printer:

  • Epson: Bisa print dan scan.
  • HP: Hanya bisa print.

Jika kita membuat interface seperti berikut:

type PrinterScanner interface {
    Print()
    Scan()
}

Maka kedua tipe harus mengimplementasikan kedua method, meskipun HP tidak bisa scan. Ini menyalahi prinsip ISP.

Implementasi yang Salah

type Epson struct{}

func (e Epson) Print() {
    fmt.Println("Mencetak menggunakan Epson..")
}

func (e Epson) Scan() {
    fmt.Println("Memindai menggunakan Epson..")
}

type HP struct{}

func (h HP) Print() {
    fmt.Println("Mencetak menggunakan HP..")
}

func (h HP) Scan() {
    // Tidak relevan, tapi tetap harus diimplementasikan
    fmt.Println("HP tidak mendukung scan.")
}

Pada contoh di atas, HP terpaksa mengimplementasikan method Scan() walaupun tidak mendukung fitur tersebut.

Solusi: Interface Segregation

Solusi yang tepat adalah memisahkan interface menjadi lebih kecil dan spesifik:

type Printer interface {
    Print()
}

type Scanner interface {
    Scan()
}

Jika ada perangkat yang mendukung kedua fitur, kita bisa membuat interface gabungan:

type PrinterScanner interface {
    Printer
    Scanner
}

Implementasi yang Benar

type Epson struct{}

func (e Epson) Print() {
    fmt.Println("Mencetak menggunakan Epson..")
}

func (e Epson) Scan() {
    fmt.Println("Memindai menggunakan Epson..")
}

func NewEpson() PrinterScanner {
    return &Epson{}
}

type HP struct{}

func (h HP) Print() {
    fmt.Println("Mencetak menggunakan HP..")
}

func NewHP() Printer {
    return &HP{}
}

Contoh Penggunaan

package main

func main() {
    e := NewEpson()
    e.Print()
    e.Scan()

    h := NewHP()
    h.Print()
    // h.Scan() // Tidak tersedia, sesuai dengan fitur HP
}

Output:

Mencetak menggunakan Epson..
Memindai menggunakan Epson..
Mencetak menggunakan HP..

Dengan pemisahan interface seperti ini, tipe hanya perlu mengimplementasikan method yang relevan dengan kemampuannya. Ini membuat kode lebih bersih, mudah dipelihara, dan mengikuti prinsip desain yang baik.

Kesimpulan

  • Hindari membuat interface yang terlalu besar (fat/bloated interface).
  • Pecah interface menjadi bagian-bagian kecil yang spesifik.
  • Gunakan embedding interface untuk menggabungkan beberapa interface kecil jika diperlukan.
  • Terapkan prinsip Interface Segregation Principle agar kode lebih fleksibel dan mudah diuji.

Dengan memahami dan menerapkan prinsip-prinsip ini, penggunaan interface dalam Go akan menjadi lebih efektif dan efisien.