Interface Untuk Penggantian Data Source

Interface adalah kumpulan fungsi yang harus diimplementasikan oleh kelas yang mengadopsinya. Tujuan dari interface adalah menciptakan sesuatu yang modular dan fleksibel. Modular berarti membagi sistem kompleks menjadi bagian-bagian kecil yang disebut modul. Sedangkan fleksibel mengacu pada kemampuan sistem beradaptasi tanpa perubahan besar.

Masalah umum pada aplikasi adalah ketika ada kebutuhan baru untuk mengganti data source, yang dapat menyebabkan perubahan besar pada aplikasi, terutama jika tidak menggunakan ORM (Object Relational Mapping). Dalam konteks ini, modul adalah data source yang berbeda, sedangkan fleksibel adalah penerapan interface.

Masalah Tanpa Menggunakan Interface

Contoh berikut menggambarkan sebuah aplikasi yang memiliki dua tipe data source yang berbeda, namun masing-masing data source memiliki perilaku yang sama. Dalam contoh ini, ada dua tipe data source yaitu Redis dan In Memory yang disebut sebagai modul.

Berikut adalah kode untuk file redis.go:

type Redis struct {
    //
}

var usersFromRedis = []map[string]any{
    {
        "id":   1,
        "name": "Anakin Skywalker",
        "age":  46,
    },
    {
        "id":   2,
        "name": "Han Solo",
        "age":  66,
    },
}

func (r Redis) GetAll() []map[string]any {
    return usersFromRedis
}

func (r Redis) Find(id int) map[string]any {
    for _, user := range usersFromRedis {
        if user["id"] == id {
            return user
        }
    }
    return nil
}

Berikut adalah kode untuk inmemory.go:

type InMemory struct {
    //
}

var usersFromInMemory = []map[string]any{
    {
        "id":   1,
        "name": "Luke Skywalker",
        "age":  53,
    },
    {
        "id":   2,
        "name": "Yoda",
        "age":  900,
    },
}

func (r InMemory) GetAll() []map[string]any {
    return usersFromInMemory
}

func (r InMemory) Find(id int) map[string]any {
    for _, user := range usersFromInMemory {
        if user["id"] == id {
            return user
        }
    }
    return nil
}

Masing-masing dari dua modul di atas memiliki perilaku yang sama dan implementasi yang sama, namun perbedaannya terletak pada data yang dihasilkannya. Cara menggunakan salah satu modul data source di atas adalah dengan kode pada file main.go:

func main() {
    redis := Redis{}
    inmemory := InMemory{}

    storageType := "redis"

    var data []map[string]any
    var user map[string]any

    switch storageType {
    case "redis":
        data = redis.GetAll()
        user = redis.Find(1)
    case "inmemory":
        data = inmemory.GetAll()
        user = inmemory.Find(1)
    default:
        fmt.Println("Invalid storage type")
        return
    }

    fmt.Println(data)
    fmt.Println(user)
}

Output ketika menggunakan data source redis:

$ go run *.go
[map[age:46 id:1 name:Anakin Skywalker] map[age:66 id:2 name:Han Solo]]
map[age:46 id:1 name:Anakin Skywalker]

Output ketika menggunakan data source inmemory:

$ go run *.go
[map[age:53 id:1 name:Luke Skywalker] map[age:900 id:2 name:Yoda]]
map[age:53 id:1 name:Luke Skywalker]

Cara menggunakan data source yang berbeda adalah dengan mengganti nilai variabel storageType dengan modul data source yang diinginkan.

Penambahan Data Source Baru

Apa yang terjadi jika ada data source baru, seperti MongoDB? Buat file baru bernama mongodb.go, dan tambahkan kode baru di file main.go pada switch case dengan menambahkan pengkondisian untuk MongoDB:

func main() {
    redis := Redis{}
    inmemory := InMemory{}
    mongodb := MongoDB{} // <-- Penambahan mongodb

    storageType := "mongodb"

    var data []map[string]any
    var user map[string]any

    switch storageType {
    case "redis":
        data = redis.GetAll()
        user = redis.Find(1)
    case "inmemory":
        data = inmemory.GetAll()
        user = inmemory.Find(1)
    default:
    case "mongodb": // <-- Pengkondisian untuk _data source_ mongodb
        data = mongodb.GetAll()
        user = mongodb.Find(1)
    default:
        fmt.Println("Invalid storage type")
        return
    }

    fmt.Println(data)
    fmt.Println(user)
}

Output ketika menggunakan data source mongodb:

$ go run *.go
[map[age:53 id:1 name:Mace Windu] map[age:650 id:2 name:Jabba The Hut]]
map[age:53 id:1 name:Mace Windu]

Apa masalah yang muncul? Ketika ada modul data source baru, akan melakukan banyak redundansi pada file main.go dengan menambahkan baris kode pada blok switch case, yang akan membuat pengkondisian menjadi panjang dan tidak efisien. Masalah ini akan semakin parah dengan penambahan data source yang lebih banyak.

Menyelesaikan Masalah Menggunakan Interface

Untuk mengatasi masalah ini, gunakan interface. Buat file baru bernama storage.go yang berisi fungsi-fungsi yang harus diimplementasikan oleh masing-masing modul data source:

package main

type Storage interface {
    GetAll() []map[string]any
    Find(id int) map[string]any
}

Kemudian, buat fungsi constructor pada masing-masing data source yang ada. Perlu diingat bahwa masing-masing source harus mengembalikan nilai interface Storage yang telah dibuat sebelumnya:

redis.go:

func NewRedis() Storage {
    return Redis{}
}

inmemory.go:

func NewInMemory() Storage {
    return InMemory{}
}

mongodb.go:

func NewMongoDB() Storage {
    return MongoDB{}
}

Perubahan pada file main.go sekarang menjadi lebih ringkas dan hanya memerlukan penggunaan data source yang diinginkan:

func main() {
    // storage := NewRedis()
    // storage := NewInMemory()
    storage := NewMongoDB()

    fmt.Println(storage.GetAll())
    fmt.Println(storage.Find(1))
}
$ go run *.go
[map[age:53 id:1 name:Mace Windu] map[age:650 id:2 name:Jabba The Hut]]
map[age:53 id:1 name:Mace Windu]

Jika menambahkan data source baru seperti PostgreSQL, buat file postgresql.go dengan mengimplementasikan interface Storage:

type PostgreSQL struct {
    //
}

var usersFromInMemory = []map[string]any{
    //
}

func (r InMemory) GetAll() []map[string]any {
    //
}

func (r InMemory) Find(id int) map[string]any {
    //
}

func NewPostgreSQL() Storage {
    return PostgreSQL{}
}

Lalu perubahan pada file main.go:

func main() {
    // storage := NewRedis()
    // storage := NewInMemory()
    // storage := NewMongoDB()
    storage := NewPostgreSQL()

    fmt.Println(storage.GetAll())
    fmt.Println(storage.Find(1))
}

Dengan menggunakan interface, permasalahan penggunaan berbagai data source dapat diatasi dengan menerapkan konsep modular dan fleksibel pada aplikasi.

Referensi: