【问题标题】:How to model this composite type hierarchy without generics如何在没有泛型的情况下对这种复合类型层次结构建模
【发布时间】:2018-01-08 02:15:36
【问题描述】:

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

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")           
}

typeswitch 是我最终需要的(每个更改集类型和每个实体都有不同的类型。)但是:

  • doMap 中的映射代码非常丑陋且重复。
  • 对于我引入的每个新实体X,我需要再创建两个类型XInsertXUpdate

有什么办法可以解决这个烂摊子吗?在其他编程语言中,我可能会想到类似的东西:

type Update<T> {
    T Old
    T New
}

type Insert<T> {
    T New
}

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

【问题讨论】:

    标签: go types


    【解决方案1】:

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

    通常:您希望在此处使用接口。在示例中,我使用接口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")
    }
    

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

    【讨论】:

    • 谢谢!这并不完全是我所采用的(我的实际应用程序也比我的示例更复杂),但它让我走上了正确的轨道。特别是创建Fill 方法和诸如ChangesetTyped 之类的东西有助于大大降低复杂性。我仍然觉得(只是我个人的看法)Go 在这里没有提供最佳解决方案,但在语言的范围内,我实现了一些可维护的东西。 :-)
    • 我知道这种感觉。来自 python,我习惯于概括很多,这似乎是良好编码的目标。 Go 不是这样,我花了一些时间来调整我的想法——而且还在调整......
    猜你喜欢
    • 2012-07-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-14
    • 2013-03-20
    • 1970-01-01
    • 2020-12-02
    • 2021-03-28
    相关资源
    最近更新 更多