Skip to content

Latest commit

 

History

History
792 lines (597 loc) · 31.9 KB

File metadata and controls

792 lines (597 loc) · 31.9 KB

十一、抑制热情

在本章中,我们将研究依赖注入DI)可能出错的一些方式。

作为程序员,我们对新工具或新技术的热情有时会胜过我们。希望这一章能帮助我们站稳脚跟,避免麻烦。

重要的是要记住,DI 是一种工具,因此,当它方便时,当它是工作的正确工具时,应该有选择地应用它。

本章将介绍以下主题:

  • 去离子损伤
  • 未成熟的未来证明
  • Mocking HTTP requests
  • Unnecessary injection?

技术要求

You might also find it useful to read and run the full versions of the code for this chapter, which are available at https://github.com/PacktPublishing/Hands-On-Dependency-Injection-in-Go/tree/master/ch11.

去离子损伤

DI 导致的损坏源于使用 DI 使代码更难理解、维护或以其他方式使用的情况。

一个长的构造器参数列表

长的构造器参数列表可能是最常见的,也是最常抱怨 DI 造成的代码损坏。虽然 DI 不是代码损坏的根本原因,但它肯定没有帮助

考虑下面的示例,它使用构造器注入:

func NewMyHandler(logger Logger, stats Instrumentation,
   parser Parser, formatter Formatter,
   limiter RateLimiter,
   cache Cache, db Datastore) *MyHandler {

   return &MyHandler{
      // code removed
   }
}

// MyHandler does something fantastic
type MyHandler struct {
   // code removed
}

func (m *MyHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
   // code removed
}

构造器的参数太多。这使得它难以使用、测试和维护。那么,问题的原因是什么?实际上有三个不同的问题。

当第一次采用 DI 时,第一个,也许最常见的是不正确的抽象。假设cachedatastore前面使用,而不是缓存MyHandler的输出,那么这些应该组合成不同的抽象。MyHandler代码不需要非常清楚数据存储的位置和方式;它只需要说明它需要什么。我们应该用更通用的抽象来替换这两个输入值,如下代码所示:

// Loader is responsible for loading the data
type Loader interface {
   Load(ID int) ([]byte, error)
}

顺便说一句,这也是另一个包/层的好地方。

第二个问题类似于第一个问题,即违反单一责任原则。我们的MyHandler承担了太多的责任。它当前正在解码请求,从数据存储和/或缓存加载数据,然后呈现响应。解决这个问题的最好方法是考虑软件的层。是顶层,我们的 HTTP 处理程序;需要理解和说 HTTP。因此,我们应该寻找办法,将其作为其主要(也许是唯一)责任。

第三个问题是交叉关注。我们的参数包括日志和插装依赖项,这些依赖项可能会被我们的大多数代码使用,并且在少数测试之外很少更改。我们有几个选择来解决这个问题;我们可以应用配置注入,从而将它们折叠成一个依赖项,并将它们与我们可能拥有的任何配置合并。或者我们可以使用即时JIT注入)访问全球单身人士。

在本例中,我们决定使用配置注入。应用它之后,我们只剩下以下代码:

func NewMyHandler(config Config,
   parser Parser, formatter Formatter,
   limiter RateLimiter,
   loader Loader) *MyHandler {

   return &MyHandler{
      // code removed
   }
}

我们仍然有五个参数,这比我们开始的要好得多,但仍然很多。

We can reduce this even further using composition. Firstly, let's look at our previous example's constructor, which is demonstrated in the following code:

func NewMyHandler(config Config,
   parser Parser, formatter Formatter,
   limiter RateLimiter,
   loader Loader) *MyHandler {

   return &MyHandler{
      config:    config,
      parser:    parser,
      formatter: formatter,
      limiter:   limiter,
      loader:    loader,
   }
}

MyHandler作为基本处理程序开始,我们可以定义一个新的处理程序来包装我们的基本处理程序,如下代码所示:

type FancyFormatHandler struct {
   *MyHandler
}

现在我们可以通过以下方式为我们的FancyFormatHandler定义一个新的构造器:

func NewFancyFormatHandler(config Config,
   parser Parser,
   limiter RateLimiter,
   loader Loader) *FancyFormatHandler {

   return &FancyFormatHandler{
      &MyHandler{
         config:    config,
         formatter: &FancyFormatter{},
         parser:    parser,
         limiter:   limiter,
         loader:    loader,
      },
   }
}

就像那样,我们少了一个参数。这里魔法的真正来源是匿名合成;因此,任何对FancyFormatHandler.ServeHTTP()的调用实际上都会调用MyHandler.ServeHTTP()。在本例中,我们添加了一些代码,以改进用户处理程序的用户体验。

当 config 可以执行时注入对象

通常情况下,您的第一反应是注入依赖项,以便可以单独测试代码。然而,要做到这一点,您必须引入如此多的抽象和间接,以致于代码量和复杂性呈指数级增长。

这种情况的一个普遍现象是使用公共库访问外部资源,如网络资源、文件或数据库。例如,让我们使用示例服务的data包。如果我们想抽象我们对sql包的使用,我们可能会从定义一个接口开始,如下代码所示:

type Connection interface {
   QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
   QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
   ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
}

然后我们意识到QueryRowContext()QueryContext()分别返回*sql.Row*sql.Rows。深入研究这些结构,我们发现我们无法从sql包外部填充它们的内部状态。为了解决这个问题,我们必须定义自己的RowRows接口,如下代码所示:

type Row interface {
   Scan(dest ...interface{}) error
}

type Rows interface {
   Scan(dest ...interface{}) error
   Close() error
   Next() bool
}

type Result interface {
   LastInsertId() (int64, error)
   RowsAffected() (int64, error)
}

我们现在与sql包完全解耦,可以在测试中模拟它。但是让我们停下来考虑一下我们在哪里:

  • 我们已经引入了大约 60 行代码,我们还没有为这些代码编写任何测试
  • We cannot test the new code without using an actual database,  which means we'll never be fully decoupled from the database
  • 我们添加了另一层抽象和少量复杂性

现在,将其与在本地安装数据库并确保其处于良好状态进行比较。这里也有复杂性,但可以说,一次性成本微不足道,尤其是在我们所从事的所有项目中。我们还必须在数据库中创建和维护表。最简单的选择是一个SQL脚本,这个脚本也可以用来支持实时系统。

对于我们的示例服务,我们决定维护一个SQL文件和一个本地安装的数据库。由于这个决定,我们不需要模拟对数据库的调用,而是只需要将数据库配置传递给本地数据库。

这种情况经常出现,尤其是对于来自可信源(如标准库)的低级包。解决这一问题的关键是务实。问问你自己,我真的需要嘲笑这个吗?是否有一些配置我可以通过,这将导致较少的工作?

最后,我们必须确保从额外的工作、代码和复杂性中获得足够的回报,以证明我们的努力是合理的。

不必要的间接

DI 被误用的另一种方式是引入目的有限(或没有)的抽象。与我们前面讨论的注入配置而不是对象类似,这种额外的间接级别会导致额外的工作、代码和复杂性。

让我们看一个例子,在这个例子中,您可以引入一个抽象来帮助测试,但实际上没有必要。

在标准 HTTP 库中,有一个名为http.ServeMux的结构。ServeMux用于构建 HTTP 路由器,它是 URL 和 HTTP 处理程序之间的映射。一旦ServeMux被配置,它就会被传递到 HTTP 服务器,如下代码所示:

func TestExample(t *testing.T) {
   router := http.NewServeMux()
   router.HandleFunc("/health", func(resp http.ResponseWriter, req *http.Request) {
      _, _ = resp.Write([]byte(`OK`))
   })

   // start a server
   address := ":8080"
   go func() {
      _ = http.ListenAndServe(address, router)
   }()

   // call the server
   resp, err := http.Get("http://:8080/health")
   require.NoError(t, err)

   // validate the response
   responseBody, err := ioutil.ReadAll(resp.Body)
   assert.Equal(t, []byte(`OK`), responseBody)
}

随着服务的扩展,我们需要确保添加更多端点。为了防止 API 回归,我们决定添加一些测试以确保路由器配置正确。由于我们熟悉 DI,我们可以直接切入并引入ServerMux的抽象,以便添加模拟实现。如下例所示:

type MyMux interface {
   Handle(pattern string, handler http.Handler)
   Handler(req *http.Request) (handler http.Handler, pattern string)
   ServeHTTP(resp http.ResponseWriter, req *http.Request)
}

// build HTTP handler routing
func buildRouter(mux MyMux) {
   mux.Handle("/get", &getEndpoint{})
   mux.Handle("/list", &listEndpoint{})
   mux.Handle("/save", &saveEndpoint{})
}

有了我们的抽象,我们可以定义一个模拟实现MyMux,并为自己编写一个测试,如以下示例所示:

func TestBuildRouter(t *testing.T) {
   // build mock
   mockRouter := &MockMyMux{}
   mockRouter.On("Handle", "/get", &getEndpoint{}).Once()
   mockRouter.On("Handle", "/list", &listEndpoint{}).Once()
   mockRouter.On("Handle", "/save", &saveEndpoint{}).Once()

   // call function
   buildRouter(mockRouter)

   // assert expectations
   assert.True(t, mockRouter.AssertExpectations(t))
}

这一切看起来都很好。然而,问题是这不是必要的。我们的目标是通过测试端点和 URL 之间的映射来防止 API 意外回归。

我们的目标可以在不嘲弄ServeMux的情况下实现。首先,让我们回到在引入MyMux接口之前的原始函数,如下例所示:

// build HTTP handler routing
func buildRouter(mux *http.ServeMux) {
   mux.Handle("/get", &getEndpoint{})
   mux.Handle("/list", &listEndpoint{})
   mux.Handle("/save", &saveEndpoint{})
}

再深入研究一下ServeMux,我们可以看到,如果我们调用Handler(req *http.Request)方法,它将返回配置到该 URL 的http.Handler

因为我们知道我们将对每个端点执行一次,所以我们应该定义一个函数来执行此操作,如以下示例所示:

func extractHandler(router *http.ServeMux, path string) http.Handler {
   req, _ := http.NewRequest("GET", path, nil)
   handler, _ := router.Handler(req)
   return handler
}

有了我们的函数,我们现在可以构建一个测试来验证每个 URL 返回的预期处理程序,如下例所示:

func TestBuildRouter(t *testing.T) {
   router := http.NewServeMux()

   // call function
   buildRouter(router)

   // assertions
   assert.IsType(t, &getEndpoint{}, extractHandler(router, "/get"))
   assert.IsType(t, &listEndpoint{}, extractHandler(router, "/list"))
   assert.IsType(t, &saveEndpoint{}, extractHandler(router, "/save"))
}

在前面的示例中,您还将注意到我们的buildRouter()函数和我们的测试非常相似。这让我们怀疑这些测试的有效性。

在这种情况下,更有效的方法是确保我们进行 API 回归测试,不仅验证路由器的配置,而且验证输入和输出格式,就像我们在第 10 章现成注入结尾所做的那样。

服务定位器

First, a definition—Service locator is a software design pattern that revolves around an object that acts as a central repository of all dependencies and is able to return them by name. You'll find this pattern in use in many languages and at the heart of some DI frameworks and containers.

在我们深入研究为什么这是 DI 引起的损坏之前,让我们看一个过度简化的服务定位器的示例:

func NewServiceLocator() *ServiceLocator {
   return &ServiceLocator{
      deps: map[string]interface{}{},
   }
}

type ServiceLocator struct {
   deps map[string]interface{}
}

// Store or map a dependency to a key
func (s *ServiceLocator) Store(key string, dep interface{}) {
   s.deps[key] = dep
}

// Retrieve a dependency by key
func (s *ServiceLocator) Get(key string) interface{} {
   return s.deps[key]
}

为了使用我们的服务定位器,我们首先必须创建它并用它们的名称映射依赖项,如以下示例所示:

// build a service locator
locator := NewServiceLocator()

// load the dependency mappings
locator.Store("logger", &myLogger{})
locator.Store("converter", &myConverter{})

通过构建服务定位器并设置依赖项,我们现在可以根据需要传递它并提取依赖项,如以下代码所示:

func useServiceLocator(locator *ServiceLocator) {
   // use the locators to get the logger
   logger := locator.Get("logger").(Logger)

   // use the logger
   logger.Info("Hello World!")
}

现在,如果我们想在测试期间记录器换成模拟记录器,那么我们只需要用模拟记录器构造一个新的服务定位器,并将其传递到我们的函数中。

那么这有什么错呢?首先,我们的服务定位器现在是一个神的物体(如第 1 章中提到的,永远不会停止瞄准更好的),我们可能会到处走动。只需将一个对象传递给每个函数听起来可能是件好事,但这会导致第二个问题。

对象和它使用的依赖项之间的关系现在对外部完全隐藏。我们不再能够查看函数或结构定义,并立即知道需要哪些依赖项。

最后,我们的操作没有 Go 的类型系统和编译器的保护。在上一个示例中,以下行可能引起了您的注意:

logger := locator.Get("logger").(Logger)

因为服务定位器接受并返回interface{},所以每次我们需要访问依赖项时,我们都需要转换为适当的类型。这种强制转换不仅使代码更加混乱,而且如果值丢失或类型错误,还会导致运行时崩溃。我们可以用更多的代码来解释这些问题,如下例所示:

// use the locators to get the logger
loggerRetrieved := locator.Get("logger")
if loggerRetrieved == nil {
   return
}
logger, ok := loggerRetrieved.(Logger)
if !ok {
   return
}

// use the logger
logger.Info("Hello World!")

使用前面的方法,我们的应用程序将不再崩溃,但它变得相当混乱。

未成熟的未来证明

Sometimes, the application of DI is not wrong, but just unnecessary. A common manifestation of this is premature future-proofing. Premature future-proofing occurs when we add features to software that we don't yet need, based on the assumption that we might need it one day. As you might expect, this results in unnecessary work and complexity.

让我们通过借用我们的示例服务来看看一个示例。目前,我们有一个 Get 端点,如下代码所示:

// GetHandler is the HTTP handler for the "Get Person" endpoint
type GetHandler struct {
   cfg    GetConfig
   getter GetModel
}

// ServeHTTP implements http.Handler
func (h *GetHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
   // extract person id from request
   id, err := h.extractID(request)
   if err != nil {
      // output error
      response.WriteHeader(http.StatusBadRequest)
      return
   }

   // attempt get
   person, err := h.getter.Do(id)
   if err != nil {
      // not need to log here as we can expect other layers to do so
      response.WriteHeader(http.StatusNotFound)
      return
   }

   // happy path
   err = h.writeJSON(response, person)
   if err != nil {
      response.WriteHeader(http.StatusInternalServerError)
   }
}

// output the supplied person as JSON
func (h *GetHandler) writeJSON(writer io.Writer, person *get.Person) error {
   output := &getResponseFormat{
      ID:       person.ID,
      FullName: person.FullName,
      Phone:    person.Phone,
      Currency: person.Currency,
      Price:    person.Price,
   }

   return json.NewEncoder(writer).Encode(output)
}

它是一个返回 JSON 的简单 REST 端点。如果有一天我们决定以不同的格式输出,我们可以将编码移动到依赖项,如以下示例所示:

// GetHandler is the HTTP handler for the "Get Person" endpoint
type GetHandler struct {
   cfg       GetConfig
   getter    GetModel
   formatter Formatter
}

// ServeHTTP implements http.Handler
func (h *GetHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
   // no changes to this method
}

// output the supplied person
func (h *GetHandler) buildOutput(writer io.Writer, person *Person) error {
   output := &getResponseFormat{
      ID:       person.ID,
      FullName: person.FullName,
      Phone:    person.Phone,
      Currency: person.Currency,
      Price:    person.Price,
   }

   // build output payload
   payload, err := h.formatter.Marshal(output)
   if err != nil {
      return err
   }

   // write payload to response and return
   _, err = writer.Write(payload)
   return err
}

这个代码看起来很合理。那么,问题在哪里?简单地说,这是我们不需要做的工作。

通过扩展,我们不需要编写或维护这些代码。在这个简单的示例中,我们的更改只增加了少量额外的复杂性,这是相对常见的。这一小部分额外的复杂性在整个系统中成倍增加,将使我们的速度减慢。

如果这应该成为一个实际的需求,那么这绝对是交付该特性的正确方式,但在这一点上,它是一个特性,因此是我们必须承担的负担。

模拟 HTTP 请求

在本章前面,我们讨论了注入如何不是所有问题的答案,并且在某些情况下,传入配置要高效得多,代码也要少得多。当我们处理外部服务,特别是 HTTP 服务(如示例服务中的上游货币转换服务)时,经常会出现这种情况。

可以模拟对外部服务的 HTTP 请求,并使用模拟来彻底测试对外部服务的调用,但这不是必需的。让我们看看通过使用示例服务中的代码,模拟与配置的并行比较。

The following is the code from our sample service, which calls to the external currency conversion service:

// Converter will convert the base price to the currency supplied
type Converter struct {
   cfg Config
}

// Exchange will perform the conversion
func (c *Converter) Exchange(ctx context.Context, basePrice float64, currency string) (float64, error) {
   // load rate from the external API
   response, err := c.loadRateFromServer(ctx, currency)
   if err != nil {
      return defaultPrice, err
   }

   // extract rate from response
   rate, err := c.extractRate(response, currency)
   if err != nil {
      return defaultPrice, err
   }

   // apply rate and round to 2 decimal places
   return math.Floor((basePrice/rate)*100) / 100, nil
}

// load rate from the external API
func (c *Converter) loadRateFromServer(ctx context.Context, currency string) (*http.Response, error) {
   // build the request
   url := fmt.Sprintf(urlFormat,
      c.cfg.ExchangeBaseURL(),
      c.cfg.ExchangeAPIKey(),
      currency)

   // perform request
   req, err := http.NewRequest("GET", url, nil)
   if err != nil {
      c.logger().Warn("[exchange] failed to create request. err: %s", err)
      return nil, err
   }

   // set latency budget for the upstream call
   subCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
   defer cancel()

   // replace the default context with our custom one
   req = req.WithContext(subCtx)

   // perform the HTTP request
   response, err := http.DefaultClient.Do(req)
   if err != nil {
      c.logger().Warn("[exchange] failed to load. err: %s", err)
      return nil, err
   }

   if response.StatusCode != http.StatusOK {
      err = fmt.Errorf("request failed with code %d", response.StatusCode)
      c.logger().Warn("[exchange] %s", err)
      return nil, err
   }

   return response, nil
}

func (c *Converter) extractRate(response *http.Response, currency string) (float64, error) {
   defer func() {
      _ = response.Body.Close()
   }()

   // extract data from response
   data, err := c.extractResponse(response)
   if err != nil {
      return defaultPrice, err
   }

   // pull rate from response data
   rate, found := data.Quotes["USD"+currency]
   if !found {
      err = fmt.Errorf("response did not include expected currency '%s'", currency)
      c.logger().Error("[exchange] %s", err)
      return defaultPrice, err
   }

   // happy path
   return rate, nil
}

在开始编写测试之前,我们应该首先问自己,我们想要测试什么?以下是典型的测试场景:

  • 快乐路径:外部服务器返回数据,我们提取成功
  • 请求失败/慢:外部服务器返回错误或未及时应答
  • 错误响应:外部服务器返回无效的 HTTP 响应代码,表示存在问题
  • 无效响应:外部服务器以我们不期望的格式返回有效负载

我们将通过模拟 HTTP 请求开始比较。

用 DI 模拟 HTTP 请求

如果我们打算使用 DI 和 mock,那么最干净的选择就是模拟 HTTP 请求,这样我们就可以让它返回我们需要的任何响应。

要实现这一点,我们需要做的第一件事是抽象构建和发送 HTTP 请求,如以下代码所示:

// Requester builds and sending HTTP requests
//go:generate mockery -name=Requester -case underscore -testonly -inpkg -note @generated
type Requester interface {
   doRequest(ctx context.Context, url string) (*http.Response, error)
}

您可以看到,我们还包含了一个go generate注释,该注释将为我们创建模拟实现。

然后我们可以更新我们的Converter以使用Requester抽象,如下例所示:

// NewConverter creates and initializes the converter
func NewConverter(cfg Config, requester Requester) *Converter {
   return &Converter{
      cfg:       cfg,
      requester: requester,
   }
}

// Converter will convert the base price to the currency supplied
type Converter struct {
   cfg       Config
   requester Requester
}

// load rate from the external API
func (c *Converter) loadRateFromServer(ctx context.Context, currency string) (*http.Response, error) {
   // build the request
   url := fmt.Sprintf(urlFormat,
      c.cfg.ExchangeBaseURL(),
      c.cfg.ExchangeAPIKey(),
      currency)

   // perform request
   response, err := c.requester.doRequest(ctx, url)
   if err != nil {
      c.logger().Warn("[exchange] failed to load. err: %s", err)
      return nil, err
   }

   if response.StatusCode != http.StatusOK {
      err = fmt.Errorf("request failed with code %d", response.StatusCode)
      c.logger().Warn("[exchange] %s", err)
      return nil, err
   }

   return response, nil
}

有了requester抽象,我们可以使用模拟实现进行测试,如下代码所示:

func TestExchange_invalidResponse(t *testing.T) {
   // build response
   response := httptest.NewRecorder()
   _, err := response.WriteString(`invalid payload`)
   require.NoError(t, err)

   // configure mock
   mockRequester := &mockRequester{}
   mockRequester.On("doRequest", mock.Anything, mock.Anything).Return(response.Result(), nil).Once()

   // inputs
   ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
   defer cancel()

   basePrice := 12.34
   currency := "AUD"

   // perform call
   converter := &Converter{
      requester: mockRequester,
      cfg:       &testConfig{},
   }
   result, resultErr := converter.Exchange(ctx, basePrice, currency)

   // validate response
   assert.Equal(t, float64(0), result)
   assert.Error(t, resultErr)
   assert.True(t, mockRequester.AssertExpectations(t))
}

在前面的示例中,我们的模拟请求程序返回无效响应,而不是调用外部服务。有了这一点,我们可以确保我们的代码在发生这种情况时能够正常运行。

为了涵盖其他典型的测试场景,我们只需要复制此测试并更改来自模拟和预期的响应。

现在,让我们将基于模拟的测试与基于配置的等效测试进行比较。

使用 config 模拟 HTTP 请求

我们可以测试Converter而无需任何代码更改。第一步是定义一个 HTTP 服务器,它返回我们需要的响应。在以下示例中,服务器将返回与上一节中的模拟相同的结果:

server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
   payload := []byte(`invalid payload`)
   response.Write(payload)
}))

然后我们从测试服务器获取 URL,并将其作为配置传递给Converter,如下例所示:

cfg := &testConfig{
   baseURL: server.URL,
   apiKey:  "",
}

converter := NewConverter(cfg)

现在,下面的示例演示了如何执行 HTTP 调用并验证响应,就像我们在模拟版本中所做的那样:

result, resultErr := converter.Exchange(ctx, basePrice, currency)

// validate response
assert.Equal(t, float64(0), result)
assert.Error(t, resultErr)

通过这种方法,我们可以实现与基于模拟的版本相同级别的测试场景覆盖率,但代码和复杂性要少得多。也许,更重要的是,我们不会招致额外构造器参数的测试导致的损害。

不必要的注射

By now, you are probably thinking, there are times when using DI is not the best option, but how do I know when? For this, I would like to offer you another self-survey.

当您不确定如何继续时,或者在开始一个潜在的大型重构之前,首先快速浏览一下我的 DI 调查:

  • 依赖性是否是环境问题(如伐木)? 环境依赖是必要的,但有可能污染函数的用户体验,尤其是构造器。注入它们是合适的,但您应该更喜欢一种不那么突兀的 DI 方法,如 JIT 注入或配置注入。
  • 在重构过程中,是否有适当的测试来保护我们? 当对测试覆盖率较低的现有代码应用 DI 时,添加一些 monkey 补丁将是您能做的最小更改,因此风险最小。一旦测试到位,将为将来的更改提供保护;即使这些改变意味着取消猴子补丁。
  • 依赖项的存在是否提供了信息? 依赖关系的存在告诉用户关于结构的什么?如果答案不是太多或什么都没有,那么依赖关系可以合并到任何配置注入中。类似地,如果依赖项不存在于这个结构的范围之外,那么您可以使用 JIT 注入来管理它。
  • 您将有多少依赖项的实现? 如果答案不止一个,那么注入依赖项是正确的选择。如果答案是一,那么你需要深入挖掘。这种依赖性会改变吗?如果它从未改变过,那么注入它就是一种浪费,而且很可能会增加不必要的复杂性。
  • 测试之外的依赖关系是否发生过变化? 如果只是在测试期间更改,那么这是 JIT 注入的一个很好的候选者,毕竟,我们希望避免测试导致的损坏。
  • 每次执行是否需要更改依赖关系? 如果答案是肯定的,则应使用方法注射。只要有可能,尽量避免向结构中添加任何确定要使用哪个依赖项的逻辑(例如,switch语句)。相反,请确保注入依赖项并使用它,或者注入包含确定依赖项的逻辑的工厂或定位器对象。这将确保您的结构没有任何与责任相关的问题。当我们添加一个新的依赖性实现时,它还可以帮助我们避免进行鸟枪手术类型的更改。
  • 依赖关系是否稳定? 稳定的依赖关系是已经存在的、不太可能改变(或以向后兼容的方式改变)并且不太可能被替换的东西。标准库和管理良好、很少更改的公共包就是一个很好的例子。如果依赖关系是稳定的,那么出于解耦的目的注入依赖关系的价值较小,因为代码没有更改,可以信任。 您可能希望注入一个稳定的依赖项,以便能够测试您是如何使用它的,正如我们在前面的 SQL 包和 HTTP 客户端示例中看到的那样。然而,为了避免测试引起的损害和不必要的复杂性,我们应该要么采用 JIT 注入,以避免污染 UX,要么完全避免注入。
  • 此结构将有一个或多个用途? 如果该结构只有一次使用,那么对该代码的灵活性和可扩展性的压力很小。因此,在我们的实现中,我们可以支持更少的注入和更多的特异性;至少在我们的情况改变之前。另一方面,在许多地方使用的代码将有更大的压力进行更改,并且,可以说,希望具有更大的灵活性,在更多情况下更有用。在这些情况下,您将希望更多地支持注入,以便为用户提供更大的灵活性。只是要小心不要有太多的注入,以致于函数的用户体验很糟糕。 对于共享代码,您还应该花更多的精力将代码与尽可能多的外部(非稳定)依赖项分离。当用户采用您的代码时,他们可能不希望采用您的所有依赖项。
  • Is this code wrapping the dependency? If we are wrapping a package to make its UX more convenient to insulate us from changes in that package, then injecting that package is unnecessary. The code we are writing is tightly coupled with the code it is wrapping, so introducing an abstraction does not achieve anything significant.
  • Does applying DI make the code better? This is, of course, extraordinarily subjective but perhaps also the most crucial question. Abstraction is useful, but it also adds indirection and complication. Decoupling is important but not always necessary. Decoupling between packages and layers is more important than decoupling between objects within a package.

通过经验和重复,您会发现,随着您对何时应用 DI 以及使用哪种方法形成直觉,这些问题中的许多将成为第二天性。

同时,下表可能会有所帮助:

| **  Method** | **  Ideal for:** | | 猴子修补术 |

  • 代码依赖于一个单例
  • 代码,该代码当前没有测试或现有依赖注入
  • 解耦包,而不对依赖包

进行任何更改 | | 构造器注入 |

  • 所需的依赖项
  • 在调用任何方法之前必须准备好的依赖项
  • 对象
  • 的大多数或所有方法所使用的依赖项
  • 在请求之间不会改变的依赖项具有多个实现的依赖项

| | 方法注入 |

  • 与函数、框架和共享库
  • 请求范围的依赖项
  • 无状态对象
  • 依赖项一起使用,这些依赖项在请求中提供上下文或数据,因此在调用

之间预计会有所不同 | | 配置注入 |

  • 更换构造器或方法注入,提升代码

的用户体验 | | 即时注射 |

  • 替换一个依赖项,否则该依赖项将被注入构造器,并且只有一个生产实现
  • 在对象和全局单例依赖项或环境依赖项之间提供一个间接层或抽象层。特别是当我们想在测试
  • 期间交换全局单例时,允许依赖项由用户

选择性提供 | | 现货注射 |

  • 降低采用构造器注入的成本
  • 降低维护依赖项创建顺序的复杂性

|

总结

在本章中,我们研究了不必要或不正确地应用 DI 的影响。我们还讨论了一些情况,其中使用 DI 并不是工作的最佳工具。

然后,我们用一个包含 10 个问题的列表来结束这一章,您可以问自己,以确定 DI 是否适合您当前的用例。

在下一章中,我们将通过回顾本书中讨论的所有内容来结束对 DI 的检查。特别是,我们现在将对比示例服务的状态与原始状态。我们还将快速了解如何使用 DI 启动新服务。

问题

  1. 您最常看到的 DI 引起的损害形式是什么?
  2. 为什么不总是盲目地应用 DI 很重要?
  3. 采用 Google Wire 等框架是否消除了所有形式的 DI 引起的损害?