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

8次阅读

共计 14061 个字符,预计需要花费 36 分钟才能阅读完成。

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

服务和依赖

服务可以是您编写的类,也可以是来自导入库的类。例如,它可以是一个 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 操作:

请求和响应主体用 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
关于设计模式:使用依赖注入有什么缺点?

正文完
 0