依赖注入(dependency injection)最通俗的讲解

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

服务和依赖

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

让咱们看一个小的例子github

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

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

一般状况下咱们会怎么实现呢?设计模式

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

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 ...
}

依赖注入和解耦

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

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

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)。

参考

How to write a REST API in Go with DI
关于设计模式:使用依赖注入有什么缺点?

相关文章
相关标签/搜索