preface
The architecture of the business system needs reasonable layering. At present, the mainstream is three-layer architecture and onion architecture. The existence of layering involves the dependency between layers. Instances of each layer depend on instances of this layer or other layers. Only by cooperating with each other can the system work. How can instances of the system be assembled in relation to IOC principles
IOC
IOC (Inversion of Control) is an object-oriented design principle
The implication is that the way objects acquire their dependencies has been reversed. Prior to IOC, each instance of an object was constructed, and the acquisition of its dependencies was done actively by the object itself. With IOC, the construction of object instances and the acquisition of dependent objects is done by the IOC framework, which has to be done by someone else, but not by the business object capability. Business objects are only responsible for declaring which objects to rely on and their subsequent use
This has the following benefits:
- Decouple the construction and use of business objects, business objects only care about business logic, as for how to construct dependencies, how to inject into the object is business independent general logic, by the framework, this is also a reflection of the decomposition of the problem
- The singleton pattern is usually used to create objects in IOC containers, which can save some memory
IOC has two implementation methods: dependency search and dependency injection
- Dependency lookup: In a dependency lookup IOC, the IOC framework is responsible for creating objects, but its dependencies need to be looked up from the container itself. This method fits the non-IOC object assembly mode and can be used to dynamically obtain the instance in the container during the program running
- Dependency injection: The difference between this and dependency lookup is that the IOC framework is not only responsible for creating an object, but also for injecting dependencies into that object, which really implements dependency inversion. Dependency injection is also the most popular method currently used
Here’s a handy dependency injection framework for go
Dig Basic use
Dig is an open-source dependency injection framework of Uber based on go language, which helps developers to manage the creation and maintenance of objects in the system. The general process of using dig is as follows:
- Create a container: dig.new
- Register constructors: Create constructors for instances that you want dig to manage. Constructors can have multiple parameters and multiple return values that are dependent on the return values that are managed by the container
- Use these instances: Write a function, take the instance you want to use as an argument, and then call Invoke to execute the function we wrote. The framework finds an instance of the function’s argument type in the container and calls the function
Here is a simple example:
Defines two types, A and B, and their constructors, that return instances of A and B, respectively
Where the constructor B has no other dependencies, A’s constructor has an argument B, representing its dependency on the instance B
type A struct {
Name string
Pb *B
}
type B struct {
Name string
}
func NewA(b *B) *A {
return &A{
Pb: b,
Name: "a",}}func NewB(a) *B {
return &B{
Name: "b",}}Copy the code
Next create the container, register the constructors of A and B, respectively, and get the instance from the container and use:
func main(a) {
// Create a container
c := dig.New()
// Register the constructors of A and B, respectively
err := c.Provide(NewA)
iferr ! =nil {
fmt.Println(err)
return
}
err = c.Provide(NewB)
iferr ! =nil {
fmt.Println(err)
return
}
// Get the instance from the container and use it
err = c.Invoke(func(a *A) {
fmt.Println(a.Name)
fmt.Println(a.Pb.Name)
})
iferr ! =nil {
fmt.Println(err)
}
}
Copy the code
The output is as follows:
a
b
Copy the code
See that the container correctly injects an instance of B into A and passes the constructed instance of A to the user-defined function
By default, only a single instance of a type is constructed in the container. It is possible to store multiple instances of the type in the container
The constructor specifies the instance Name with dig.Name. To use it, you define a receive object with an embedded dig.In type, and add fields of that type. The Name tag specifies the instance Name to receive
Define the parameter object param
type param struct {
dig.In
B1 *B `name:"b1"`
B2 *B `name:"b2"`
}
Copy the code
Register two constructors separately:
err = c.Provide(NewB("b1"),dig.Name("b1"))
err = c.Provide(NewB("b2"),dig.Name("b2"))
Copy the code
Execute using frame functions:
err = c.Invoke(func(p param) {
fmt.Println(p.B1.Name)
fmt.Println(p.B2.Name)
})
Copy the code
Results: Examples B1 and B2 were obtained correctly
b1
b2
Copy the code
Implementation principle of DIG
Next, how does the framework implement dependency injection
Initialize the container
First, what structure is the container returned by dig.new ()
type Container struct {
// Save each type, which node provides it, usually only one
providers map[key][]*node
// Save all nodes and generate one node each time the provide method is called
nodes []*node
// Save instance values for each type
values map[key]reflect.Value
// Save a list of values for each group type
groups map[key][]reflect.Value
}
Copy the code
The structure of a key is as follows: Represents a type where the name field holds the name of the instance
type key struct {
t reflect.Type
name string
group string
}
Copy the code
Register constructor
Now look at the method of registering the constructor: provide
First, the corresponding node is constructed according to the constructor. Its main structure is as follows:
type node struct {
// The constructor itself
ctor interface{}
// Type of the constructor
ctype reflect.Type
// A list of the constructor's argument types
paramList paramList
// A list of return value types for the constructor
resultList resultList
}
Copy the code
The construction process is relatively simple
func newNode(ctor interface{}, opts nodeOptions) (*node, error) {
cval := reflect.ValueOf(ctor)
ctype := cval.Type()
cptr := cval.Pointer()
// Extract a list of parameter types by type
params, err := newParamList(ctype)
iferr ! =nil {
return nil, err
}
// Extract the list of return value types
results, err := newResultList(
ctype,
resultOptions{
Name: opts.ResultName,
Group: opts.ResultGroup,
},
)
iferr ! =nil {
return nil, err
}
// Generate an instance
return &node{
ctor: ctor,
ctype: ctype,
location: digreflect.InspectFunc(ctor),
id: dot.CtorID(cptr),
paramList: params,
resultList: results,
}, err
}
Copy the code
As can be seen when privode, does not generate any instance is constructed, and no way to generate examples, because constructors depend on instance may not have other constructor can provide, here can only save the generated instances of metadata, the metadata describes what types of instance, it can produce and its need what kind of
Use instances in containers
Now let’s see how, when invoking Invoke, DIG finds the type instance needed by the custom method from the container and calls back the method
As in the following example, the framework needs to find an instance of *A from the container and call back the user’s func
err = c.Invoke(func(a *A) {
fmt.Println(a.Name)
})
Copy the code
The first is to get the types of the method’s parameters
ftype := reflect.TypeOf(function)
// ...
pl, err := newParamList(ftype)
iferr ! =nil {
return err
}
Copy the code
The next step is to get instances of these types from the container
func (pl paramList) BuildList(c containerStore) ([]reflect.Value, error) {
args := make([]reflect.Value, len(pl.Params))
for i, p := range pl.Params {
var err error
// Build parameters in sequence
args[i], err = p.Build(c)
iferr ! =nil {
return nil, err
}
}
return args, nil
}
Copy the code
The first step is to check if it has been generated before, and the generated instance will be cached in values
if v, ok := c.getValue(ps.Name, ps.Type); ok {
return v, nil
}
Copy the code
If there are none in the cache, you need to generate new ones, and then get constructors that can generate instances of these types, which are already registered with providers
providers := c.getValueProviders(ps.Name, ps.Type)
Copy the code
These constructors are then called
for _, n := range providers {
// Call the constructor
err := n.Call(c)
if err == nil {
continue
}
// ...
}
Copy the code
If a constructor has a dependency, it calls the constructor of the dependent type, layer by layer, until it reaches a constructor that doesn’t have any dependencies
func (n *node) Call(c containerStore) error {
if n.called {
return nil
}
iferr := shallowCheckDependencies(c, n.paramList); err ! =nil {
return errMissingDependencies{
Func: n.location,
Reason: err,
}
}
// Construct the argument list for the constructor
args, err := n.paramList.BuildList(c)
iferr ! =nil {
return errArgumentsFailed{
Func: n.location,
Reason: err,
}
}
// When the node is constructed, place the returned value into the container
receiver := newStagingContainerWriter()
results := c.invoker()(reflect.ValueOf(n.ctor), args)
iferr := n.resultList.ExtractList(receiver, results); err ! =nil {
return errConstructorFailed{Func: n.location, Reason: err}
}
receiver.Commit(c)
n.called = true
return nil
}
Copy the code
The user’s method is called when all the parameters of the method are available
returned := c.invokerFn(reflect.ValueOf(function), args)
Copy the code
This is the end of the process
Circular dependencies
In the example of using a container, there is a sentence that says “until a constructor without any dependencies is called”. That is, all the dependencies of the objects in the container form a directed acyclic graph. What if all constructors have dependencies?
Let’s modify the definitions and constructors of A and B as follows:
type A struct {
Name string
Pb *B
}
type B struct {
Name string
Pa *A
}
func NewA(b *B) *A {
return &A{
Pb: b,
}
}
func NewB(a *A) *B {
return &B{
Pa: a,
}
}
Copy the code
It is then injected separately into the container
func main(a) {
c := dig.New()
c.Provide(NewA)
err := c.Provide(NewB)
iferr ! =nil {
fmt.Println(err)
}
}
Copy the code
The following error message is displayed:
cannot provide function "main".NewB: this function introduces a cycle:
Copy the code
As you can see, the DIG framework does not support circular dependencies, and detection is done by default when constructors are registered, or the configuration can be modified to defer detection until instances in the container are used
The principle of detecting cyclic dependencies is that if the return type of the newly added constructor funcA is the parameter type of one of funcA’s dependent constructors, it is considered to be in conflict
To construct C, construct A, B, and then construct B depending on C, resulting in cyclic dependence
Cyclic dependencies should be avoided when designing software architectures.
At the vertical level of architecture, whether it is a hierarchical architecture or an onion architecture, the top should depend on the bottom, the implementation should depend on the abstract, the variable part depends on the stable part, it can depend on the ladder, but it is better not to rely on the reverse.
At the horizontal level, there may be situations where two services depend on each other. This does not make good business sense, usually because module boundaries are not fully demarcated. In this case, common dependencies can be identified, abstracted into a single service, or placed at a lower level to avoid circular dependencies
Compared with the Spring
Dig and Spring are both frameworks that can implement dependency injection. Developers simply declare which objects an object needs to depend on, and the framework creates the dependent objects and does the injection
Spring supports constructor injection, field injection, and dig supports constructor injection
By default, Spring generates instances of all types and completes property assembly at program startup. Dig is deferred until the first use to regenerate an instance, but subsequent uses are fetched directly from the cache
conclusion
In this paper, IOC design principle, basic application and realization principle of DIG framework are briefly introduced