这篇文章解释了什么是依赖注入(又称控制反转),以及它如何改善定义业务逻辑的代码。

服务和依赖

服务可以是您编写的类,也可以是来自导入库的类。例如,它可以是一个 logger 或一个 database connection。因此,您可以编写一个无需任何外部帮助即可单独运行的服务,但也可能您会很快您会达到一个点,即其中一个服务将不得不使用另一个服务的代码的地步。

让我们看一个小的例子

我们将创建一个EmailSender。此类将用于发送电子邮件。它必须在数据库中写入已发送电子邮件的信息,并记录可能发生的错误。

EmailSender 将依赖于其他三项服务:用于发送电子邮件的 SmtpClient,用于与数据库交互的 EmailRepository 以及用于记录错误的 Logger。

通常情况下我们会怎么实现呢?

1.EmailSender 的三个依赖关系被声明为属性。依赖项注入的思想是,EmailSender不应该负责创建其依赖项,它们应该从外部注入。对EmailSender来说,其配置的详细信息应该是未知的。

interface SmtpClientInterface {
    send(toName: string, toEmail: string, subject: string, message: string)
}

interface EmailRepositoryInterface {
    insertEmail(address: string, email: Email, status: string)
    updateEmailStatus(id: number, status: string)
}

interface LoggerInterface {
    error(message: string)
}

class EmailSender {
    client: SmtpClientInterface
    repo: EmailRepositoryInterface
    logger: LoggerInterface
    
    send(user: User, email: Email) {
        try {
            this.repo.insertEmail(user.email, email, "sending")
            this.client.send(user.email, user.name, email.subject, email.message)
            this.repo.updateEmailStatus(email.id, "sent")
        } catch(e) {
            this.logger.error(e.toString())
        }
    }
}

2.使用 setter,您可以在EmailSender上添加setSmtpClient(),setEmailRepository()和setLogger()方法。

// inject dependencies with setters
sender = new EmailSender()
sender.setSmtpClient(client)
sender.setEmailRepository(repo)
sender.setLogger(logger)

3.在构造函数中设置依赖项。它确保一旦创建对象,它就会按预期工作并且不会遗忘任何依赖关系。

class EmailSender {
    // ... dependencies and other attributes ...
    
    constructor(client: SmtpClientInterface, repo: EmailRepositoryInterface, logger: LoggerInterface) {
        this.client = client
        this.repo = repo
        this.logger = logger
    }
    
    // ... methods ...
}

依赖注入和解耦

依赖项注入的关键概念是解耦,它们应该从外部注入。服务不应将其依赖项绑定到特定的实现,他们将失去可重用性。因此,这将使它们更难维护和测试。

例如,如果将依赖像这样写入构造函数。

constructor() {
    this.client = new SmtpClient()
}

这么做很不好,你的服务将只能使用特定的Smtp客户端,它的配置不能被更改(一旦要更换一个Smtp客户端你需要修改业务代码,这样不好)。

应该怎么做:

constructor(client: SmtpClientInterface) {
    this.client = client
}

这样,您就可以自由使用你想要的实现。

话虽如此,构造函数的参数不一定必须是接口:

constructor(client: SmtpClient) {
    this.smtp = smtp
}

一般情况下这种方式足够了,接口很棒,但是它们会使您的代码难以阅读。如果要避免过度设计的代码,那么从类(classes)开始可能是一个很好的方法。然后在必要时将其替换为接口(interfaces)。

总结

依赖项注入的主要优点是解耦。它可以极大地提高代码的可重用性和可测试性。缺点是,服务的创建将会远离您的业务逻辑,这会使您的代码更难理解。

如何用DI编写Go中的REST API

为了进一步说明DI的优点,我们将使用DI编写Go中的REST API。

现在假设我们要开发一个汽车管理的项目,需要提供几个API,对cars进行增删改查(CRUD)操作。

API description

api的作用是管理汽车列表。该api实现以下基本的CRUD操作:

clipboard.png

请求和响应主体用json编码。api处理以下错误代码:

  • 400 - Bad Request : the parameters of the request are not valid
  • 404 - Not Found : the car does not exist
  • 500 - Internal Error : an unexpected error occurred (eg: the database connection failed)

Project structure

项目结构非常简单:

├─ app
│   ├─ handlers
│   ├─ middlewares
│   └─ models
│       ├─ garage
│       └─ helpers
│
├─ config
│   ├─ logging
│   └─ services
│
└─ main.go 
  • main.go文件是应用程序的入口点。它的作用是创建一个可以处理api路由的Web服务器。
  • app/handler 和 app/middlewares 就像他们的名字所说的,是定义应用程序的处理程序和中间件的位置。它们代表了MVC应用程序的控制器部分,仅此而已。
  • app/models/garage 包含业务逻辑。换句话说,它定义了什么是汽车以及如何管理它们。
  • app/models/helpers由可以协助处理程序的功能组成。 ReadJSONBody函数可以解码http请求的正文,而JSONResponse函数可以编写json响应。该软件包还包括两个错误类型:ErrValidation和ErrNotFound。它们用于促进http处理程序中的错误处理。
  • 在config/logging目录中,logger 定义为全局变量。记录器是一个特殊的对象。那是因为您需要尽快在应用程序中安装一个记录器。而且您还希望保留它直到应用程序停止。
  • 在config/services中,您可以找到依赖注入容器的服务定义。它们描述了服务的创建方式以及应如何关闭服务。

Model

我们先在model中定义好car的数据结构。

// Car is the structure representing a car.
type Car struct {
    ID    string `json:"id" bson:"_id"`
    Brand string `json:"brand" bson:"brand"`
    Color string `json:"color" bson:"color"`
}

它代表一辆非常简单的汽车,只有两个字段,一个品牌和一个颜色。Car 是保存在数据库中的结构。该结构也用于请求和响应中。

CarManager 的结构体及业务逻辑层的CRUD操作。

type CarManager struct {
    Repo   *CarRepository
    Logger *zap.Logger
}

// GetAll returns the list of cars.
func (m *CarManager) GetAll() ([]*Car, error) {
    cars, err := m.Repo.FindAll()

    if cars == nil {
        cars = []*Car{}
    }

    if err != nil {
        m.Logger.Error(err.Error())
    }

    return cars, err
}

// Get returns the car with the given id.
// If the car does not exist an helpers.ErrNotFound is returned.
func (m *CarManager) Get(id string) (*Car, error) {
    car, err := m.Repo.FindByID(id)

    if m.Repo.IsNotFoundErr(err) {
        return nil, helpers.NewErrNotFound("Car `" + id + "` does not exist.")
    }

    if err != nil {
        m.Logger.Error(err.Error())
    }

    return car, err
}

// Create inserts the given car in the database.
// It returns the inserted car.
func (m *CarManager) Create(car *Car) (*Car, error) {
    if err := ValidateCar(car); err != nil {
        return nil, err
    }

    car.ID = bson.NewObjectId().Hex()

    err := m.Repo.Insert(car)

    if m.Repo.IsAlreadyExistErr(err) {
        return m.Create(car)
    }

    if err != nil {
        m.Logger.Error(err.Error())
        return nil, err
    }

    return car, nil
}

// Update updates the car with the given id.
// It uses the values contained in the given car fields.
// It returns the updated car.
func (m *CarManager) Update(id string, car *Car) (*Car, error) {
    if err := ValidateCar(car); err != nil {
        return nil, err
    }

    car.ID = id

    err := m.Repo.Update(car)

    if m.Repo.IsNotFoundErr(err) {
        return nil, helpers.NewErrNotFound("Car `" + id + "` does not exist.")
    }

    if err != nil {
        m.Logger.Error(err.Error())
        return nil, err
    }

    return car, err
}

// Delete removes the car with the given id.
func (m *CarManager) Delete(id string) error {
    err := m.Repo.Delete(id)

    if m.Repo.IsNotFoundErr(err) {
        return nil
    }

    if err != nil {
        m.Logger.Error(err.Error())
    }

    return err
}

CarManager 是一个数据结构体,被handlers用来处理执行CRUD操作。每个方法对应一个http handle 。 CarManager需要一个CarRepository来执行mongo查询。

CarRepository 的结构体及具体DB层的操作

package garage

import mgo "gopkg.in/mgo.v2"

// CarRepository contains all the interactions
// with the car collection stored in mongo.
type CarRepository struct {
    Session *mgo.Session
}

// collection returns the car collection.
func (repo *CarRepository) collection() *mgo.Collection {
    return repo.Session.DB("dingo_car_api").C("cars")
}

// FindAll returns all the cars stored in the database.
func (repo *CarRepository) FindAll() ([]*Car, error) {
    var cars []*Car
    err := repo.collection().Find(nil).All(&cars)
    return cars, err
}

// FindByID retrieves the car with the given id from the database.
func (repo *CarRepository) FindByID(id string) (*Car, error) {
    var car *Car
    err := repo.collection().FindId(id).One(&car)
    return car, err
}

// Insert inserts a car in the database.
func (repo *CarRepository) Insert(car *Car) error {
    return repo.collection().Insert(car)
}

// Update updates all the caracteristics of a car.
func (repo *CarRepository) Update(car *Car) error {
    return repo.collection().UpdateId(car.ID, car)
}

// Delete removes the car with the given id.
func (repo *CarRepository) Delete(id string) error {
    return repo.collection().RemoveId(id)
}

// IsNotFoundErr returns true if the error concerns a not found document.
func (repo *CarRepository) IsNotFoundErr(err error) bool {
    return err == mgo.ErrNotFound
}

// IsAlreadyExistErr returns true if the error is related
// to the insertion of an already existing document.
func (repo *CarRepository) IsAlreadyExistErr(err error) bool {
    return mgo.IsDup(err)
}

CarRepository只是mongo查询的包装器。这里的CarRepository可以是具体的CarMongoRepository或CarPsgRepository等。

在Repository中分离数据库查询可以轻松列出与数据库的所有交互。在这种情况下,很容易替换数据库。例如,您可以使用postgres代替mongo创建另一个存储库。它还为您提供了为测试创建模拟存储库的机会。

服务依赖配置

以下配置了每个服务的依赖,比如car-manager依赖car-repository和logger

The logger is in the App scope. It means it is only created once for the whole application. The Build function is called the first time to retrieve the service. After that, the same object is returned when the service is requested again.

logger是在App范围内。这意味着它只为整个应用程序创建一次。第一次调用Build函数来检索服务。之后,当再次请求服务时,将返回相同的对象。

声明如下:

var Services = []di.Def{
    {
        Name:  "logger",
        Scope: di.App,
        Build: func(ctn di.Container) (interface{}, error) {
            return logging.Logger, nil
        },
    },
    // other services
}

现在我们需要一个mongo连接。我们首先要的是连接池。然后,每个http请求将使用该池来检索其自己的连接。

因此,我们将创建两个服务。在App范围内为mongo-pool,在Request范围内为mongo:

 {
        Name:  "mongo-pool",
        Scope: di.App,
        Build: func(ctn di.Container) (interface{}, error) {
            // create a *mgo.Session
            return mgo.DialWithTimeout(os.Getenv("MONGO_URL"), 5*time.Second)
        },
        Close: func(obj interface{}) error {
            // close the *mgo.Session, it should be cast first
            obj.(*mgo.Session).Close()
            return nil
        },
    },
    {
        Name:  "mongo",
        Scope: di.Request,
        Build: func(ctn di.Container) (interface{}, error) {
            // get the pool of connections (*mgo.Session) from the container
            // and retrieve a connection thanks to the Copy method
            return ctn.Get("mongo-pool").(*mgo.Session).Copy(), nil
        },
        Close: func(obj interface{}) error {
            // close the *mgo.Session, it should be cast first
            obj.(*mgo.Session).Close()
            return nil
        },
    },

mongo服务在每个请求中被创建,它使用mongo-pool服务取得数据库连接。mongo服务可以在Build函数中使用mongo-pool服务,多亏了容器的Get方法。

请注意,在两种情况下关闭mongo连接也很重要。这可以使用定义的“关闭”字段来完成。删除容器时将调用Close函数。它发生在针对请求容器的每个http请求的末尾,以及针对App容器的程序停止时。

接下来是CarRepository。这依赖于mongo服务。由于mongo连接在Request范围内,因此CarRepository不能在App范围内。它也应该在Request范围内。

 {
        Name:  "car-repository",
        Scope: di.Request,
        Build: func(ctn di.Container) (interface{}, error) {
            return &garage.CarRepository{
                Session: ctn.Get("mongo").(*mgo.Session),
            }, nil
        },
    },

最后,我们可以编写CarManager定义。与CarRepository相同,由于其依赖性,CarManager应该位于Request范围内。

{
        Name:  "car-manager",
        Scope: di.Request,
        Build: func(ctn di.Container) (interface{}, error) {
            return &garage.CarManager{
                Repo:   ctn.Get("car-repository").(*garage.CarRepository),
                Logger: ctn.Get("logger").(*zap.Logger),
            }, nil
        },
    },

基于这些定义,可以在main.go文件中创建依赖项注入容器。

Handlers

http处理程序的作用很简单。它必须解析传入的请求,检索并调用适当的服务并编写格式化的响应。所有处理程序大致相同。例如,GetCarHandler看起来像这样:

func GetCarHandler(w http.ResponseWriter, r *http.Request) {
    id := mux.Vars(r)["carId"]

    car, err := di.Get(r, "car-manager").(*garage.CarManager).Get(id)

    if err == nil {
        helpers.JSONResponse(w, 200, car)
        return
    }

    switch e := err.(type) {
    case *helpers.ErrNotFound:
        helpers.JSONResponse(w, 404, map[string]interface{}{
            "error": e.Error(),
        })
    default:
        helpers.JSONResponse(w, 500, map[string]interface{}{
            "error": "Internal Error",
        })
    }
}

mux.Vars只是使用gorilla/mux路由库,从URL中检索carId参数的方法。

mux.Vars只是使用大猩猩/ mux(用于该项目的路由库)从URL中检索carId参数的方法。

处理程序有趣的部分是如何从依赖项注入容器中检索CarManager。这是通过di.Get(r,“car-manager”)完成的。为此,容器应包含在http.Request中。您必须使用中间件来实现。

Middlewares

该api使用两个中间件。

第一个是PanicRecoveryMiddleware。它用于从处理程序中可能发生的紧急情况中恢复并记录错误。这一点非常重要,因为如果无法从容器中检索CarManager,di.Get(r,“ car-manager”)可能会慌乱。

// PanicRecoveryMiddleware handles the panic in the handlers.
func PanicRecoveryMiddleware(h http.HandlerFunc, logger *zap.Logger) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                // log the error
                logger.Error(fmt.Sprint(rec))

                // write the error response
                helpers.JSONResponse(w, 500, map[string]interface{}{
                    "error": "Internal Error",
                })
            }
        }()

        h(w, r)
    }
}

第二个中间件通过将di.Container注入http.Request来允许di.Get(r, "car-manager").(*garage.CarManager)工作。

package di

import (
    "context"
    "net/http"
)

// ContainerKey is a type that can be used to store a container
// in the context.Context of an http.Request.
// By default, it is used in the C function and the HTTPMiddleware.
type ContainerKey string

// HTTPMiddleware adds a container in the request context.
//
// The container injected in each request, is a new sub-container
// of the app container given as parameter.
//
// It can panic, so it should be used with another middleware
// to recover from the panic, and to log the error.
//
// It uses logFunc, a function that can log an error.
// logFunc is used to log the errors during the container deletion.
func HTTPMiddleware(h http.HandlerFunc, app Container, logFunc func(msg string)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // create a request container from tha app container
        ctn, err := app.SubContainer()
        if err != nil {
            panic(err)
        }
        defer func() {
            if err := ctn.Delete(); err != nil && logFunc != nil {
                logFunc(err.Error())
            }
        }()

        // call the handler with a new request
        // containing the container in its context
        h(w, r.WithContext(
            context.WithValue(r.Context(), ContainerKey("di"), ctn),
        ))
    }
}

// C retrieves a Container from an interface.
// The function panics if the Container can not be retrieved.
//
// The interface can be :
// - a Container
// - an *http.Request containing a Container in its context.Context
//   for the ContainerKey("di") key.
//
// The function can be changed to match the needs of your application.
var C = func(i interface{}) Container {
    if c, ok := i.(Container); ok {
        return c
    }

    r, ok := i.(*http.Request)
    if !ok {
        panic("could not get the container with C()")
    }

    c, ok := r.Context().Value(ContainerKey("di")).(Container)
    if !ok {
        panic("could not get the container from the given *http.Request")
    }

    return c
}

// Get is a shortcut for C(i).Get(name).
func Get(i interface{}, name string) interface{} {
    return C(i).Get(name)
}

对于每个http请求。将创建给定应用程序容器的子容器。它被注入到http.Request的context.Context中,因此可以使用di.Get进行检索。在每个请求结束时,将删除子容器。 logFunc函数用于记录删除子容器期间可能发生的错误。

Main

main.go文件是应用程序的入口点。

首先确保 logger 在程序结束之前能够写入任何内容。

defer logging.Logger.Sync()

然后依赖注入容器可以被创建:

// create a builder
builder, err := di.NewBuilder()
if err != nil {
    logging.Logger.Fatal(err.Error())
}

// add the service definitions
err = builder.Add(services.Services...)
if err != nil {
    logging.Logger.Fatal(err.Error())
}

// create the app container, delete it just before the program stops
app := builder.Build()
defer app.Delete()

最后一件有趣的事情是这部分:

m := func(h http.HandlerFunc) http.HandlerFunc {
    return middlewares.PanicRecoveryMiddleware(
        di.HTTPMiddleware(h, app, func(msg string) {
            logging.Logger.Error(msg)
        }),
        logging.Logger,
    )
}

m 函数结合了两个中间件。它可以用于将中间件应用于处理程序。

主文件的其余部分只是 gorilla mux router(多路复用器路由器)的配置和Web服务器的创建。

下面给出完成的Main.go的全部代码:

package main

import (
    "context"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/gorilla/mux"
    "github.com/sarulabs/di"
    "github.com/sarulabs/di-example/app/handlers"
    "github.com/sarulabs/di-example/app/middlewares"
    "github.com/sarulabs/di-example/config/logging"
    "github.com/sarulabs/di-example/config/services"
)

func main() {
    // Use a single logger in the whole application.
    // Need to close it at the end.
    defer logging.Logger.Sync()

    // Create the app container.
    // Do not forget to delete it at the end.
    builder, err := di.NewBuilder()
    if err != nil {
        logging.Logger.Fatal(err.Error())
    }

    err = builder.Add(services.Services...)
    if err != nil {
        logging.Logger.Fatal(err.Error())
    }

    app := builder.Build()
    defer app.Delete()

    // Create the http server.
    r := mux.NewRouter()

    // Function to apply the middlewares:
    // - recover from panic
    // - add the container in the http requests
    m := func(h http.HandlerFunc) http.HandlerFunc {
        return middlewares.PanicRecoveryMiddleware(
            di.HTTPMiddleware(h, app, func(msg string) {
                logging.Logger.Error(msg)
            }),
            logging.Logger,
        )
    }

    r.HandleFunc("/cars", m(handlers.GetCarListHandler)).Methods("GET")
    r.HandleFunc("/cars", m(handlers.PostCarHandler)).Methods("POST")
    r.HandleFunc("/cars/{carId}", m(handlers.GetCarHandler)).Methods("GET")
    r.HandleFunc("/cars/{carId}", m(handlers.PutCarHandler)).Methods("PUT")
    r.HandleFunc("/cars/{carId}", m(handlers.DeleteCarHandler)).Methods("DELETE")

    srv := &http.Server{
        Handler:      r,
        Addr:         "0.0.0.0:" + os.Getenv("SERVER_PORT"),
        WriteTimeout: 15 * time.Second,
        ReadTimeout:  15 * time.Second,
    }

    logging.Logger.Info("Listening on port " + os.Getenv("SERVER_PORT"))

    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            logging.Logger.Error(err.Error())
        }
    }()

    // graceful shutdown
    stop := make(chan os.Signal, 1)

    signal.Notify(stop, os.Interrupt, syscall.SIGTERM)

    <-stop

    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()

    logging.Logger.Info("Stopping the http server")

    if err := srv.Shutdown(ctx); err != nil {
        logging.Logger.Error(err.Error())
    }
}

Conclusion

通过上面的例子可以看到,业务层代码和其依赖是解耦的,如果要更换依赖不需要更改业务层的代码,而只需要修改服务的依赖配置文件就可以了。

依赖注入将有助于使这个项目变得更容易维护。使用sarulabs/di框架可让您将服务的定义与业务逻辑分开。声明发生在单一的地方,这是应用程序配置的一部分。这些服务可以被获取在handles中通过使用在请求中的容器存储(container stored)。

参考