Dependency Injection in Go


Dependency Injection (DI) merupakan salah satu teknik yang cukup sederhana namun sangat powerful dalam pengembangan perangkat lunak. Teknik ini bertujuan untuk membuat unit atau komponen yang independent dan testable.

Goals

Dengan menulis artikel ini, diharapkan bisa menjawab beberapa pertanyaan berikut.

Case Study

Kasus yang digunakan pada artikel ini adalah menambahkan daftar following pada jejaring sosial, seperti Instagram atau Twitter. Berikut struktur data model yang digunakan untuk menggambarkan masalah daftar following pada jejaring sosial.

type Following struct {
    ID int
    Username string
    FullName string
}
func NewFollowing(username string, fullName string) Following {
    return Following{0, username, fullName}
}

Phase 1, Put everything in main()

Solusi paling straightforward ketika membuat Go program, adalah menaruh semuanya di main function.

package main

import (
    "database/sql"
    "fmt"
    "log"   _ "github.com/go-sql-driver/mysql"
)

var (
    mysqlHost     = "localhost"
    mysqlUser     = "root"
    mysqlPassword = "root_pass"
)

func main() {
    connString := fmt.Sprintf("%s:%s@tcp(%s:3306)/following?parseTime=true", mysqlUser, mysqlPassword, mysqlHost)
    sqlDB, err := sql.Open("mysql", connString)
    if err != nil {
        log.Fatalf("Error %v", err)
    }
    if err := sqlDB.Ping(); err != nil {
        log.Fatalf("Error %v", err)
    }
    stmt, err := sqlDB.Prepare("INSERT INTO following (username, full_name) VALUES (?, ?)")
    if err != nil {
        log.Fatalf("Error %v", err)
    }
    defer stmt.Close()
    _, err = stmt.Exec("root", "I'm Root")
    if err != nil {
        log.Fatalf("Error %v", err)
    }
    log.Println("Done!")
}

Pase 2, Put specific task in specific function

Selanjutnya, kita bisa menaruh scope tertentu di specific function, semisal scope program yang bertugas untuk membuat koneksi database, mengambil spesifik data, menyimpan data di database dan lain sebagainya.

package main

import (
    "database/sql"
    "fmt"
    "log"    _ "github.com/go-sql-driver/mysql"
)

func newSql(mysqlHost, mysqlUser, mysqlPassword string) (*sql.DB, error) {
    connString := fmt.Sprintf("%s:%s@tcp(%s:3306)/following", mysqlUser, mysqlPassword, mysqlHost)
    sqlDB, err := sql.Open("mysql", connString)
    if err != nil {
        return nil, err
    }
    if err := sqlDB.Ping(); err != nil {
        return nil, err
    }
    return sqlDB, nil
}

func storeNewEntry(sqlDB *sql.DB, entity Following) error {
    stmt, err := sqlDB.Prepare("INSERT INTO following (username, full_name) VALUES (?, ?)")
    if err != nil {
        return err
    }
    defer stmt.Close()
    _, err = stmt.Exec(entity.Username, entity.FullName)
    if err != nil {
        return err
    }
    return nil
}

func main() {
    sqlDB, err := newSql("localhost", "root", "root-is-not-used")
    if err != nil {
        log.Fatalf("Error %v", err)
    }
    newFollowing := NewFollowing("root", "I'm Root!")
    if err := storeNewEntry(sqlDB, newFollowing); err != nil {
        log.Fatalf("Error %v", err)
    }
    log.Println("Done!")
}

Contoh diatas, saya membuat 2 fungsi baru, yaitu fungsi untuk menyimpan data di database dan fungsi untuk membuat koneksi database.

Phase 3, Use dependency injection

Contoh selanjutnya, ialah memisahkan scope tersebut sesuai concern masing-masing, semisal scope yang berhubungan dengan interaksi ke database saya taruh di FollowingRepository. Selanjutnya, hal yang berhubungan dengan business logic saya taruh di FollowingService.

Nantinya FollowingRepository akan di-inject ke FollowingService. Sehingga FollowingService dapat menggunakan FollowingRepository apabila dibutuhkan proses yang berhubungan dengan database, semisal menyimpan dan mengambil data.

package main

import (
    "database/sql"
    "fmt"
    "log"    _ "github.com/go-sql-driver/mysql"
)

type Repository interface {
    Store(following Following) error
}

func NewMysqlRepository(sqlDB *sql.DB) Repository {
    return MysqlRepository{sqlDB: sqlDB}
}

type MysqlRepository struct {
    sqlDB *sql.DB
}

func (sql MysqlRepository) Store(following Following) error {
    stmt, err := sql.sqlDB.Prepare("INSERT INTO following (username, full_name) VALUES (?, ?)")
    if err != nil {
        return err
    }
    defer stmt.Close()
    if _, err := stmt.Exec(following.Username, following.FullName); err != nil {
        return err
    }
    return nil
}

func newSql(mysqlHost, mysqlUser, mysqlPassword string) (*sql.DB, error) {
    connString := fmt.Sprintf("%s:%s@tcp(%s:3306)/following", mysqlUser, mysqlPassword, mysqlHost)
    sqlDB, err := sql.Open("mysql", connString)
    if err != nil {
        return nil, err
    }
    if err := sqlDB.Ping(); err != nil {
        return nil, err
    }
    return sqlDB, nil
}

type Service struct {
    followingRepository Repository
}

func NewService(followingRepository Repository) Service {
    return Service{followingRepository}
}

func (service Service) Insert(username, fullName string) error {
    err := service.followingRepository.Store(NewFollowing(username, fullName))
    if err != nil {
        return err
    }
    return nil
}

var (
    mysqlHost     = "localhost"
    mysqlUser     = "root"
    mysqlPassword = "root_pass"
)

func main() {
    sqlDB, err := newSql(mysqlHost, mysqlUser, mysqlPassword)
    if err != nil {
        log.Fatalf("Error %v", err)
    }
    mysqlRepository := NewMysqlRepository(sqlDB)
    followingService := NewService(mysqlRepository)
    if err := followingService.Insert("root", "I'm Root!"); err != nil {
        log.Fatalf("Error %v", err)
    }
    log.Println("Done!")
}

Unit Testing

Salah satu keunggulan dependency injection ialah kita bisa membuat komponen yang mandiri dan testable. Berikut contoh bagaimana kita membuat mock repository yang akan digunakan oleh layer service.

package main_test

import (
    "testing"    following "github.com/rakateja/di-go/manual-di"
)

type MockRepository struct{}func NewMockRepository() following.Repository {
    return MockRepository{}
}

func (mock MockRepository) Store(entity following.Following) error {
    return nil
}

func TestInsertFollowing(t *testing.T) {
    followingRepository := NewMockRepository()
    followingService := following.NewService(followingRepository)
    if err := followingService.Insert("root", "I'm Root"); err != nil {
        t.Errorf("Got %v, expect nil  when inserting new following", err.Error())
    }
}

Conclusion

Sekian artikel tentang Dependency Injection di Go. Contoh kode di artikel ini, bisa lihat disini. Terimakasih dan semoga bermanfaat.

References

comments powered by Disqus