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