【问题标题】:Dependency Injection loses type safety with struct arguments依赖注入失去了结构参数的类型安全性
【发布时间】:2021-01-20 00:39:28
【问题描述】:

Go 中的一个常见模式是使用依赖注入作为绕过循环导入的手段。

例如,here is a very short code snippet 定义了两个对象,HouseGarage。使用了Dispatcher 抽象,以便他们可以随意调用彼此的方法。 (Dispatcher注入到每个对象中。)

到目前为止,一切都很好。

在这段代码 sn-p 的基础上,假设我编写了一个 House 的新方法,它需要 10 个参数:

type HouseDispatcher interface {
    // This matches all of the methods of house.House
    SetThermostat(temp int)
    PrepareBedroom(arg1 int, arg2 int64, arg3 int32, arg4 uint64, arg5 uint32, arg6 string, arg7 rune, arg8 int, arg9 int, arg10 int)
}

这段代码有异味,所以我们需要将它重构为一个对象:

type BedroomData struct {
    arg1 int
    arg2 int64
    arg3 int32
    arg4 uint64
    arg5 uint32
    arg6 string
    arg7 rune
    arg8 int
    arg9 int
    arg10 int
}

type HouseDispatcher interface {
    // This matches all of the methods of house.House
    SetThermostat(temp int)
    PrepareBedroom(data BedroomData)
}

好多了!但是 BedroomData 结构体存在于 House.go 文件中是有意义的,紧挨着与房屋相关的所有其他代码。

所以当我们在 Dispatcher 中搭建方法时,我们不得不这样编码:

type HouseDispatcher interface {
    // This matches all of the methods of house.House
    SetThermostat(temp int)
    PrepareBedroom(data interface{})
}

现在我们已经失去了类型安全性。

我们可以通过在Dispatcher 中复制BedroomData 结构定义来恢复类型安全性,但现在我们违反了 DRY - 这两个结构可能不同步并导致灾难性的运行时错误。

那么解决办法是什么?

【问题讨论】:

  • 将结构体放入model 包中,并使用它定义接口和实现。
  • 这很聪明,应该可以解决问题。虽然,房子包中没有所有与房子相关的数据很烦人。有更好的解决方案吗?
  • 显而易见的是:将HouseGarage 放入同一个包中。但是,将接口和模型放入单独的包而不是实现的想法是一种常见的模式。
  • 虽然我可以效仿你的做法,但这似乎是强迫的,不切实际的。我从未见过这样的代码。你有这个问题困扰你的真实例子吗?

标签: go dependency-injection dry circular-dependency


【解决方案1】:

为什么车库告诉房子准备卧室? 如果 Garage 从不调用该函数,则该函数不应该出现在提供给 Garage 的接口中。在 Garage 包中定义 House 接口,仅包含 Garage 包中所需的功能,并具有另一个接口(如果需要,并且您不能只使用结构本身)用于 House 类型的一般用途。

// This matches all of the methods of house.House

让我举个例子说明如何构建它:

包车库:

type House interface {
    // Put only function here that the Garage package needs
    SetThermostat(temp int)
}

// imagining things from here on:

func NewGarage(house House) *Garage {
    return &Garage{
        house: House,
    }
}

type Garage struct {
    house House
}

func (s *Garage) HeatHouse(temp int) {
    // use house here
    s.house.SetThermostat(temp)
}

包房:

type Garage interface {
    // Put only functions here the House package needs from Garage
}

type House struct {
    // ...
}

type BedroomData struct {
    arg1 int
    arg2 int64
    arg3 int32
    arg4 uint64
    arg5 uint32
    arg6 string
    arg7 rune
    arg8 int
    arg9 int
    arg10 int
}

func (s *House) PrepareBedroom(data BedroomData) {
    // ...
}

这不是一个绝对规则,但在许多情况下会有所帮助:为用例定义一个接口。 (与为整个类型定义接口相反)

【讨论】:

    【解决方案2】:

    这个例子可能太做作了,无法证明你在说什么。一般来说,我更喜欢有一个定义共享数据模型和服务的根包。包提供服务并作用于模型。这些包还可以在内部定义和使用它们需要的任何类型,这是其他包不需要的。如果您要注入依赖项,则该依赖项应该是共享服务接口,如果您要传递数据,则它应该来自共享模型。包应该只使用共享模型相互通信。

    在您的示例中,BedroomData 将是共享模型的一部分。不是依赖,是数据,所以不需要用接口传入;从来没有理由需要单独实现只保存数据的东西。如果它不是真正的共享模型的一部分,那么它不应该从外部传入。

    【讨论】:

      猜你喜欢
      • 2022-09-28
      • 2015-08-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-06-01
      • 2015-10-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多