依賴注入(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
關於設計模式:使用依賴注入有什麼缺點?

相關文章
相關標籤/搜索