Masalah Penggunaan Interface

Interface adalah kumpulan fungsi yang menggambarkan perilaku suatu kelas, dan kelas tersebut harus mengimplementasikan fungsi-fungsi yang didefinisikan oleh interface tersebut.

Salah satu masalah umum yang muncul adalah “interface pollution” atau “fat interface”, yaitu ketika sebuah interface memiliki banyak fungsi yang harus diimplementasikan. Hal ini membuat interface menjadi terlalu besar dan memanjangkan proses pendefinisian.

Misalnya, terdapat sebuah repository dengan banyak fungsi yang harus diimplementasikan:

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)
}

Dalam contoh di atas, interface tersebut memiliki banyak fungsi untuk operasi pengambilan data. Bagaimana jika ada fungsi baru yang harus ditambahkan berdasarkan kondisi tertentu? Hal ini akan membuat interface semakin panjang dan kompleks.

Bagaimana cara mengatasi masalah ini? Salah satu pendekatan yang dapat digunakan adalah Embedding Interface, yaitu menyisipkan interface ke dalam interface lain. Prinsip ini mirip dengan embedding struct ke dalam struct lain.

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

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

Dalam kode di atas, kita memisahkan fungsi-fungsi ke dalam interface terpisah dan melakukan Embedding Interface ke interface utama untuk mencegah interface utama menjadi terlalu kompleks.

Selain itu, terdapat masalah lain yang disebut “bloated interface”. Masalah ini terjadi ketika ada fungsi yang harus diimplementasikan oleh client, meskipun client sebenarnya tidak membutuhkan fungsi tersebut.

Misalnya, kita ingin membuat fungsionalitas untuk mesin printer, dimana printer Epson dapat melakukan pencetakan (print) dan pemindaian (scan), sedangkan printer HP hanya dapat melakukan pencetakan.

Kita dapat membuat sebuah interface untuk print dan scan:

type PrinterScanner interface {
    Print()
    Scan()
}

Kemudian kita implementasikan interface tersebut pada objek Epson:

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{}
}

Selanjutnya, implementasikan interface pada objek HP:

type HP struct {
    // 
}

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

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

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

Terakhir, jalankan pada fungsi main:

package main

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

    h := NewHP()
    h.Print()
    h.Scan()
}
$ go run main.go
Mencetak menggunakan Epson..
Memindai menggunakan Epson..
Mencetak menggunakan HP..
Memindai menggunakan HP..

Apakah ada masalah di sini? Tentu saja. Awalnya, kita sepakat bahwa printer merk HP hanya dapat melakukan pemindaian (scan). Namun, dalam implementasi di atas, client atau objek yang mengimplementasikan interface harus tetap mengimplementasikan fungsi yang tidak mereka butuhkan.

Namun, setiap objek yang ingin mengimplementasikan interface harus mengimplementasikan semua fungsi yang ada di interface tersebut, meskipun tidak semua fungsi tersebut dibutuhkan. Ini merupakan perilaku dari interface.

Bagaimana cara mengatasi masalah ini? Kita dapat memisahkan fungsi-fungsi tersebut ke dalam interface terpisah. Yang pertama, buatlah interface terpisah untuk print dan scan:

type Printer interface {
    Print()
}

type Scanner interface {
    Scan()
}

Kemudian, buatlah interface baru yang menggabungkan interface-interface yang dibutuhkan. Ini disebut Embedding Interface, yang sudah dibahas pada bagian sebelumnya.

type PrinterScanner interface {
    Printer
    Scanner
}

Apa fungsi dari interface PrinterScanner? Interface ini memungkinkan objek baru yang memiliki fungsi print dan scan dapat menggunakan interface ini. Sebelumnya, objek Epson dapat melakukan kedua fungsi tersebut, sehingga interface ini dapat digunakan.

Untuk printer merk HP, kita hanya mengimplementasikan interface Scanner karena kita sudah menetapkan bahwa printer merk HP hanya dapat melakukan pemindaian.

Berikut adalah contoh kode untuk printer Epson yang telah diperbaiki:

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{}
}

Dan untuk printer HP:

type HP struct {
    // 
}

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

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

Terakhir, jalankan dalam fungsi main:

package main

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

    h := NewHP()
    h.Scan()
}
$ go run main.go
Mencetak menggunakan Epson..
Memindai menggunakan Epson..
Memindai menggunakan HP..

Dengan perubahan ini, masalah terkait interface yang “bloated” dapat diatasi. Sekarang objek HP hanya mengimplementasikan fungsi yang relevan dengan fungsionalitasnya.