mtojek/dependency-injection-in-go

语言: Go

git: https://github.com/mtojek/dependency-injection-in-go

如何解决Go中的依赖注入问题
How to solve a a dependency injection problem in Go
README.md (中文)

Go中的依赖注入

Build Status

该项目展示了如何在Go中解决(DIY)依赖注入问题。

状态:完成

关键词:依赖注入,注入,注入器,DIY

介绍

在编写DRY代码时,依赖注入似乎是一个关键因素,特别是当开发人员不想关心通过几层传递服务时,总是伴随着一长串构造函数参数。此样板通常使代码不可读,并且不必要地比关键业务逻辑更长。

我想出这个的方式,在一个“概念证明”简单项目中呈现。该应用程序基于facebookgo / inject提供的图表,该图表负责解决服务之间的依赖关系。

用户故事

  • 有一个用户可以借书的图书馆。
  • 用户有名字。
  • 这本书有一个标题。
  • 用户可以借书。

解决方案的设计

该应用程序有一个单独的“服务”包,它只包含两个包 - “实现”和“接口”。第一个包含几个应用程序服务(也在包中组织),如BookService,BorrowService,UserService,LoggerService和BorrowingFormatter。

我假设所有提到的服务都应该是无状态的,因此每个服务只能表示一个实例。接口和实现的分离可防止有问题的依赖循环,并使服务模拟更容易。

BookService - 示例服务

可以注意到记录器服务将由图填充(下面的描述)注入。没有使用构造函数,特殊的初始化过程。

package bookservice

import ...

// BookService allows to create books.
type BookService struct {
    LoggerService interfaces.LoggerService `inject:""`
}

// CreateBook method is responsible for creating new book.
func (b *BookService) CreateBook(title string) shared.Book {
    b.LoggerService.Info("New book created: %v", title)
    return newBook(title)
}

如果我们使用接口进行注入,则可以编写一些测试,包括在外部模拟它们(导出的字段):

func TestCreateBook(t *testing.T) {
    assert := assert.New(t)

    // given
    logger := new(mockedLogger)
    sut := &BookService{logger}

    // when
    sut.CreateBook("book")

    // then
    assert.True(logger.invoked, "LoggerService's Info method should be invoked")
}

借用格式 - 新实例

在某些情况下,始终注入新的服务实例很有用。 facebookgo / inject支持通过提供特殊注释来创建新的私有注入实例:

`inject:"private"`

在其他情况下,当不需要注射时,建议创建新实例的典型方法:

// Borrow method is responsible for borrowing book by user.
func (b *BorrowService) Borrow(user shared.User, book shared.Book) {
    formatter := new(borrowingformatter.BorrowingFormatter)
    formatted := formatter.Format(user, book)

    b.LoggerService.Info(formatted)
}

提示:出于测试目的,我建议准备一个“新实例提供程序”,它可以生成我们想要的任何模拟实例。提供者模式不适用于此项目。

依赖图

准备依赖图是facebookgo / inject的一块蛋糕。开发人员应该根据目标结构中选择的接口来定义将在图填充中使用哪些服务。人口程序如下:

func main() {
    application := new(libraryApplication)

    if error := inject.Populate(
        application,
        new(bookservice.BookService),
        new(borrowservice.BorrowService),
        new(loggerservice.LoggerService),
        new(userservice.UserService),
    ); nil != error {
        log.Fatalf("Error occured while populating graph: %v", error)
    }

    application.run()
}

本文使用googletrans自动翻译,仅供参考, 原文来自github.com

en_README.md

Dependency Injection in Go

Build Status

This project shows how to solve (DIY) a dependency injection problem in Go.

Status: Done

Keywords: dependency injection, inject, injector, DIY

Introduction

Dependency injection seems to be a key factor while writing a DRY code, especially when a developer does not want to care about passing a service through few layers, always accompanied by a long list of constructor arguments. This boilerplate usually makes the code unreadable and unnecessarily longer than the crucial business logic.

The way I figured this out, is presented in a "proof of concept" simple project. The application bases on a graph provided by facebookgo/inject, which is responsible for resolving dependencies between services.

User stories

  • There is a library in which a user can borrow a book.
  • The user has name.
  • The book has a title.
  • The user can borrow a book.

Design of the solution

The application has a separated "services" package, which contains only two packages - "implementation" and "interfaces". The first one contains several application services (also organized in packages) like a BookService, BorrowService, UserService, LoggerService and BorrowingFormatter.

I assume that all mentioned services should be stateless, thus they can be represented by only one instance per service. The separation of interfaces and implementation prevents from problematic dependency cycles and makes easier services mocking.

BookService - Sample Service

There can be noticed that logger service will be injected by the graph population (description below). No constructors, special initialization procedures have been used.

package bookservice

import ...

// BookService allows to create books.
type BookService struct {
    LoggerService interfaces.LoggerService `inject:""`
}

// CreateBook method is responsible for creating new book.
func (b *BookService) CreateBook(title string) shared.Book {
    b.LoggerService.Info("New book created: %v", title)
    return newBook(title)
}

If we use interfaces for injections, there will be a possibility to write some tests, including mocking them externally (exported fields):

func TestCreateBook(t *testing.T) {
    assert := assert.New(t)

    // given
    logger := new(mockedLogger)
    sut := &BookService{logger}

    // when
    sut.CreateBook("book")

    // then
    assert.True(logger.invoked, "LoggerService's Info method should be invoked")
}

BorrowingFormatter - New Instance

In some situations it is useful to always inject a new instance of service. The facebookgo/inject supports creating new private own injected instances by providing special annotation:

`inject:"private"`

In other cases, when no injections are needed, a typical way of creating new instances is advised:

// Borrow method is responsible for borrowing book by user.
func (b *BorrowService) Borrow(user shared.User, book shared.Book) {
    formatter := new(borrowingformatter.BorrowingFormatter)
    formatted := formatter.Format(user, book)

    b.LoggerService.Info(formatted)
}

Hint: for testing purposes I recommend to prepare a "new instance provider", which can produce any mocked instance we want. The provider pattern is not applied to this project.

Dependency Graph

Preparing a dependency graph is a piece of cake with facebookgo/inject. The developer should only define which services will be used in graph population according to chosen intefaces in target structs. The population procedure is presented below:

func main() {
    application := new(libraryApplication)

    if error := inject.Populate(
        application,
        new(bookservice.BookService),
        new(borrowservice.BorrowService),
        new(loggerservice.LoggerService),
        new(userservice.UserService),
    ); nil != error {
        log.Fatalf("Error occured while populating graph: %v", error)
    }

    application.run()
}