Creational Design Pattern: Singleton – Part 3

Untuk menyelesaikan masalah yang terjadi pada modul sebelumnya, perlu dilakukan beberapa modifikasi.

Pertama kita perlu buat satu abstraction menggunakan interface. Jadi dapat digunakan oleh real database dan dummy database.

type Database interface {
	GetPopulation(name string) int
}

Kita buat fungsi untuk menghitung total populasi, dimana dilakukan dengan memodifikasi dari fungsi sebelumnya.

Fungsi akan menerima argument Database sebagai tambahan, dan digunakan method GetPopulation dari interface Database.

func GetTotalPopulationEx(db Database, cities []string) int {
	result := 0
	for _, city := range cities {
		result += db.GetPopulation(city)
	}
	return result
}

Berikutnya adalah kita tambahkan dummy database yang akan digunakan oleh unit test.

type DummyDatabase struct {
	dummyData map[string]int
}

func (d *DummyDatabase) GetPopulation(name string) int {
	if len(d.dummyData) == 0 {
		d.dummyData = map[string]int{
			"alpha": 1,
			"beta":  2,
			"gamma": 3}
	}
	return d.dummyData[name]
}

Berikutnya adalah kode testing

names := []string{"alpha", "gamma"} // expect 4
tp := GetTotalPopulationEx(&DummyDatabase{}, names)
ok := tp == 4
fmt.Println(ok)

Jadi, kode testing diatas berfungsi sebagai mestinya, yaitu melakukan testing apakah fungsi berjalan perhitungan total populasi dengan benar, tanpa melakukan database loading.

Berikut code lengkapnya

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"sync"
)

// interface sebagai abstraction
type Database interface {
	GetPopulation(name string) int
}

type singletonDatabase struct {
	capitals map[string]int
}

func (db *singletonDatabase) GetPopulation(
	name string) int {
	return db.capitals[name]
}

var once sync.Once
var instance Database

func readData(path string) (map[string]int, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	scanner.Split(bufio.ScanLines)

	result := map[string]int{}

	for scanner.Scan() {
		k := scanner.Text()
		scanner.Scan()
		v, _ := strconv.Atoi(scanner.Text())
		result[k] = v
	}

	return result, nil
}

func GetSingletonDatabase() Database {
	once.Do(func() {
		db := singletonDatabase{}
		caps, err := readData(".\\capitals.txt")
		if err == nil {
			db.capitals = caps
		}
		instance = &db
	})
	return instance
}

func GetTotalPopulation(cities []string) int {
	result := 0
	for _, city := range cities {
		result += GetSingletonDatabase().GetPopulation(city)
	}
	return result
}

//fungsi dimodifikasi dari fungsi diatas.
//menerima argument interface Database
//fungsi akan flexible, dapat menggunakan dummy database atau real database.
func GetTotalPopulationEx(db Database, cities []string) int {
	result := 0
	for _, city := range cities {
		result += db.GetPopulation(city)
	}
	return result
}

//dummy database yang akan digunakan dalam fungsi testing
type DummyDatabase struct {
	dummyData map[string]int
}

func (d *DummyDatabase) GetPopulation(name string) int {
	if len(d.dummyData) == 0 {
		d.dummyData = map[string]int{
			"alpha": 1,
			"beta":  2,
			"gamma": 3}
	}
	return d.dummyData[name]
}

func main() {
	names := []string{"alpha", "gamma"} // expect 4
	tp := GetTotalPopulationEx(&DummyDatabase{}, names)
	ok := tp == 4
	fmt.Println(ok)
}

Kesimpulan

Jadi untuk singleton dapat digunakan sync.One, memungkinkan kita melakukan lazy instantiation dan thread safety.

Agar tidak melanggar prinsip DIP, code jangan tergantung langsung pada singleton instance, namun gunakan interface.

Sharing is caring:

Leave a Comment