Container: create service container and service provider

This commit is contained in:
Muyao CHEN 2024-09-28 22:18:32 +02:00
parent 722e8191a9
commit f866f18cfd
3 changed files with 306 additions and 0 deletions

195
framework/container.go Normal file
View File

@ -0,0 +1,195 @@
package framework
import (
"fmt"
"io"
"sync"
)
type Container interface {
// Bind binds a service provider to the container.
//
// If the service exists, the old one will be replaced by the new one.
// No error is returned in this case.
//
// Return any Init() error.
Bind(provider ServiceProvider) error
// IsBound returns true the provider is bound to the container.
IsBound(name string) bool
// Get gets or creates a service by its name.
//
// Return error if the service cannot be initiated or doesn't exist.
Get(name string) (interface{}, error)
// JustGet gets a service, return nil if error.
JustGet(name string) interface{}
// GetExisted gets an initiated service by its name.
GetExisted(name string) interface{}
// MakeNew creates a new instance of the service with different parameters.
//
// Used for non singleton services.
MakeNew(name string, params []interface{}) (interface{}, error)
}
type GoWebContainer struct {
// Must implement Container interfaces.
Container
// Service providers by name.
providers map[string]ServiceProvider
// Instantiated services by name.
instances map[string]interface{}
// RWMutex to protect the changes in container.
mu sync.RWMutex
}
func errUnknownService(name string) error {
return fmt.Errorf("service %q is not registered in the container", name)
}
func NewGoWebContainer() *GoWebContainer {
return &GoWebContainer{
providers: map[string]ServiceProvider{},
instances: map[string]interface{}{},
mu: sync.RWMutex{},
}
}
func (c *GoWebContainer) PrintProviders(w io.Writer) {
ret := make([]string, len(c.providers))
for _, provider := range c.providers {
name := provider.Name()
ret = append(ret, name)
}
fmt.Fprintf(w, "%v", ret)
}
func (c *GoWebContainer) Bind(provider ServiceProvider) error {
c.bindProvider(provider)
// Instantiate Later ?
if provider.InstantiateLater() {
return nil
}
// Instantiate now !
_, err := c.instantiate(provider, nil)
if err != nil {
return err
}
return nil
}
func (c *GoWebContainer) bindProvider(provider ServiceProvider) {
c.mu.Lock()
defer c.mu.Unlock()
name := provider.Name()
c.providers[name] = provider
}
func (c *GoWebContainer) IsBound(name string) bool {
return c.getServiceProvider(name) != nil
}
func (c *GoWebContainer) getServiceProvider(name string) ServiceProvider {
c.mu.RLock()
defer c.mu.RUnlock()
provider, ok := c.providers[name]
if ok {
return provider
}
return nil
}
func (c *GoWebContainer) Get(name string) (interface{}, error) {
return c.makeServiceInstance(name, nil, false)
}
func (c *GoWebContainer) GetExisted(name string) interface{} {
return c.getServiceInstance(name)
}
func (c *GoWebContainer) JustGet(name string) interface{} {
ins, err := c.makeServiceInstance(name, nil, false)
if err != nil {
return nil
}
return ins
}
func (c *GoWebContainer) MakeNew(name string, params []interface{}) (interface{}, error) {
return c.makeServiceInstance(name, params, true)
}
func (c *GoWebContainer) getServiceInstance(name string) interface{} {
c.mu.RLock()
defer c.mu.RUnlock()
instance, ok := c.instances[name]
if ok {
return instance
}
return nil
}
func (c *GoWebContainer) makeServiceInstance(
name string,
params []interface{},
forceNew bool,
) (interface{}, error) {
provider := c.getServiceProvider(name)
if provider == nil {
return nil, errUnknownService(name)
}
if forceNew {
return c.instantiate(provider, params)
}
instance := c.getServiceInstance(name)
if instance != nil {
return instance, nil
}
// create a new instance
return c.instantiate(provider, params)
}
// instantiate instantiates a new instance.
// If no params are provided, then default params are used.
func (c *GoWebContainer) instantiate(
provider ServiceProvider,
params []interface{},
) (interface{}, error) {
c.mu.Lock()
defer c.mu.Unlock()
if err := provider.Init(c); err != nil {
// TODO: Error should be wrapped.
return nil, err
}
if params == nil {
params = provider.Params(c)
}
construct := provider.Register(c)
instance, err := construct(params...)
if err != nil {
// TODO: Error should be wrapped.
return nil, err
}
c.instances[provider.Name()] = instance
return instance, nil
}

View File

@ -0,0 +1,88 @@
package framework
import (
"log"
"reflect"
"testing"
)
// {{{ contract
const DummyProviderName = "container:test"
type IDummyService interface {
GetDummy() DummyStruct
}
type DummyStruct struct {
Name string
}
// }}}
// {{{ provider
type DummyServiceProvider struct{}
func (p *DummyServiceProvider) Name() string {
return DummyProviderName
}
func (p *DummyServiceProvider) Register(c Container) NewInstance {
return NewDummyService
}
func (p *DummyServiceProvider) Init(c Container) error {
log.Println("init")
return nil
}
func (p *DummyServiceProvider) InstantiateLater() bool {
return true
}
func (p *DummyServiceProvider) Params(c Container) []interface{} {
return []interface{}{c}
}
// }}}
// {{{ service
type DummyService struct {
IDummyService
// parameters
c Container
}
func (d *DummyService) GetDummy() DummyStruct {
return DummyStruct{
Name: "Dummy!",
}
}
func NewDummyService(params ...interface{}) (interface{}, error) {
c := params[0].(Container)
log.Println("new dummy service")
return &DummyService{c: c}, nil
}
// }}}
func TestBind(t *testing.T) {
container := NewGoWebContainer()
provider := &DummyServiceProvider{}
container.Bind(provider)
dummyService := container.JustGet(DummyProviderName).(IDummyService)
want := DummyStruct{
Name: "Dummy!",
}
got := dummyService.GetDummy()
if !reflect.DeepEqual(want, got) {
t.Errorf("want %v, got %v ", want, got)
}
}

23
framework/provider.go Normal file
View File

@ -0,0 +1,23 @@
package framework
// NewInstance defines the function to create a new instance for a service.
type NewInstance func(...interface{}) (interface{}, error)
// ServiceProvider is the interface for a service provider
type ServiceProvider interface {
// Register a service into the service container, return a NewInstance constructer
Register(Container) NewInstance
// Init is called when instantiating the service.
Init(Container) error
// InstantiateLater decides if the service is instantiated at register phase.
// If true, then this service shall be instantiated later.
InstantiateLater() bool
// Params defines the parameters needed to instantiate a service.
Params(Container) []interface{}
// Name is the name of the service provider
Name() string
}