From f866f18cfd674a165f8efaf6eb2310e0601b427d Mon Sep 17 00:00:00 2001 From: Muyao CHEN Date: Sat, 28 Sep 2024 22:18:32 +0200 Subject: [PATCH] Container: create service container and service provider --- framework/container.go | 195 +++++++++++++++++++++++++++++++++++ framework/containter_test.go | 88 ++++++++++++++++ framework/provider.go | 23 +++++ 3 files changed, 306 insertions(+) create mode 100644 framework/container.go create mode 100644 framework/containter_test.go create mode 100644 framework/provider.go diff --git a/framework/container.go b/framework/container.go new file mode 100644 index 0000000..38684f3 --- /dev/null +++ b/framework/container.go @@ -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 +} diff --git a/framework/containter_test.go b/framework/containter_test.go new file mode 100644 index 0000000..d15ce53 --- /dev/null +++ b/framework/containter_test.go @@ -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) + } +} diff --git a/framework/provider.go b/framework/provider.go new file mode 100644 index 0000000..c834916 --- /dev/null +++ b/framework/provider.go @@ -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 +}