如何在没有泛型的情况下对此复合类型层次结构进行建模



我有一个系统来解析包含mysql表变更集的日志文件,想想二进制日志之类的东西。可以有更新和插入,我们现在忽略的删除。我的模块的函数得到这样的输入:

type Changeset struct {
Table string // which table was affected
Type string // INSERT or UPDATE
OldData map[string]string // these 2 fields contain all columns of a table row
NewData map[string]string
}

OldDataINSERT变更集时,当它是UPDATE变更集时,填充OldDataNewData(更新前后的数据(时,为空。

现在我不想在我的模块中使用这样的非类型化数据,因为我需要对一些域进行建模,并且有一些类型安全性会更好。但是,如果更改是该域逻辑的插入或更新,我仍然需要保留知识(例如,如果是更新,我将验证某些字段是否未更改,例如(。

假设我有两个表(假设它们只有一个名为 Id 的字段,但实际上它们有更多不同的字段(。所以我对这些对象进行了建模,如下所示:

type Foo struct { // foo table
Id string
// ... imagine more fields  here ...
}
type Bar struct { // bar table
Id string
// ... imagine more fields  here ...
}

现在我可以从Changeset.OldDataChangeset.NewData映射map[string][string],但是我不再知道更改是插入还是更新。我来回想了一下,但我想出的最好的是:

type FooInsert struct {
New Foo
}
type FooUpdate struct {
New Foo
Old Foo
}
type BarInsert struct {
New Bar
}
type BarUpdate struct {
New Bar
Old Bar
}

映射代码如下所示:

func doMap(c Changeset) interface{} {
if c.Table == "foo" {
switch c.Type {
case "UPDATE":
return FooUpdate{Old: Foo{Id: c.OldData["id"]}, New: Foo{Id: c.NewData["id"]}}
case "INSERT":
return FooInsert{New: Foo{Id: c.NewData["id"]}}
}
}
if c.Table == "bar" {
switch c.Type {
// ... almost same as above, but return BarUpdate/BarInsert ...
}
}
return nil
}

好处是,它使我能够编写对这个映射函数的结果做一个类型开关,如下所示:

insertChangeset := Changeset{
Table: "foo",
Type: "INSERT",
NewData: map[string]string{"id": "1"},
}
o := doMap(insertChangeset)
switch o.(type) {
case BarUpdate:
println("Got an update of table bar")
case FooUpdate:
println("Got an update of table foo")
case BarInsert:
println("Got an insert to table bar")
case FooInsert:
println("Got an insert to table foo")           
}

类型开关是我最终需要的(每个更改变更集类型和每个实体的不同类型。但:

  • doMap中看到的映射代码非常丑陋和重复。
  • 对于我介绍的每个新实体X,我需要再创建两种类型XInsertXUpdate

有没有办法解决这个烂摊子?在其他编程语言中,我可能会想到这样的东西:

type Update<T> {
T Old
T New
}
type Insert<T> {
T New
}

但不确定如何在 Go 中对此进行建模。我还创建了一个游乐场示例,在一个程序中显示整个代码:https://play.golang.org/p/ZMnB5K7RaI

看看这个解决方案。这是一种可能的解决方案。

通常:您希望在此处使用接口。在示例中,我使用接口DataRow来存储任何表的行的数据。所有表结构都必须实现 2 个函数,如我的示例所示。(另请参阅我关于带有泛型的基类中的通用函数的说明(

这里再次显示代码:

package main
import "fmt"
type Foo struct {
Id string
}
func (s *Foo) Fill(m map[string]string) {
// If you want to build a general Fill you can build a base struct for Foo, Bar, etc. that works with reflect. 
// Note that it will be slower than implementing the function here! Ask me if you want one I built recently.
s.Id = m["id"]
}
func (s *Foo) GetRow() interface{} {
return nil
}
type Bar struct {
Id string
}
func (s *Bar) Fill(m map[string]string) {
s.Id = m["id"]
}
func (s *Bar) GetRow() interface{} {
return nil
}
type DataRow interface {
Fill(m map[string]string)
GetRow() interface{}
}
type Changeset struct {
Table   string
Type    string
OldData map[string]string
NewData map[string]string
}
type ChangesetTyped struct {
Table   string
Type    string
OldData DataRow
NewData DataRow
}
func doMap(c Changeset) ChangesetTyped {
ct := ChangesetTyped{
Table:   c.Table,
Type:    c.Type,
OldData: parseRow(c.Table, c.OldData),
}
if c.Type == "UPDATE" {
ct.NewData = parseRow(c.Table, c.NewData)
}
return ct
}
func parseRow(table string, data map[string]string) (row DataRow) {
if table == "foo" {
row = &Foo{}
} else if table == "bar" {
row = &Bar{}
}
row.Fill(data)
return
}
func main() {
i := Changeset{
Table:   "foo",
Type:    "INSERT",
NewData: map[string]string{"id": "1"},
}
u1 := Changeset{
Table:   "foo",
Type:    "UPDATE",
OldData: map[string]string{"id": "20"},
NewData: map[string]string{"id": "21"},
}
u2 := Changeset{
Table:   "bar",
Type:    "UPDATE",
OldData: map[string]string{"id": "30"},
NewData: map[string]string{"id": "31"},
}
m1 := doMap(i)
m2 := doMap(u1)
m3 := doMap(u2)
fmt.Println(m1, m1.OldData)
fmt.Println(m2, m2.OldData, m2.NewData)
fmt.Println(m3, m3.OldData, m3.NewData)
}

如果要将实际行从DataRow转换为正确的类型,请使用(在本例中为 Foo 类型(:

foo, ok := dt.GetRow().(Foo)
if !ok {
fmt.Println("it wasn't of type Foo after all")
}

希望这对你的戈朗任务有所帮助!

相关内容

最新更新