dongtieshang5429 2017-08-01 09:27
浏览 25
已采纳

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

I have a system that parses a logfile which contains changesets of mysql tables, think of something like a binlog. There can be updates and inserts, deletes we ignore for now. The function of my module gets an input like this:

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
}

OldData is empty when it's an INSERT changeset, when it's an UPDATE changeset, OldData and NewData are filled (the data before and after the update).

Now I don't want to work with untyped data like this in my module, as I need to model some domain and it would be nicer to have some type safety. However, I need to still retain the knowledge if a change was an insert or an update for that domain logic (like, if it's an update, I will validate that some fields didn't change, as an example).

Assume I have two tables (let's say they only have one field named Id, but in reality they have more and different ones). So I modeled these objects like so:

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

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

Now I can map the map[string][string] from Changeset.OldData and Changeset.NewData, but then I don't know anymore if the change was an insert or an update. I was thinking a bit back and forth, but the best I came up with was:

type FooInsert struct {
    New Foo
}

type FooUpdate struct {
    New Foo
    Old Foo
}

type BarInsert struct {
    New Bar
}

type BarUpdate struct {
    New Bar
    Old Bar
}

And the mapping code looks like this:

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
}

The upside is, it enables me to write do do a typeswitch on the result of this mapping function like so:

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

The typeswitch is what I would need to have in the end (different types per change changeset type and per entity.) But:

  • the mapping code as seen in doMap is very ugly and repetitive.
  • for every new entity X I introduce, I need to create two more types XInsert and XUpdate.

Is there any way around this mess? In other programming languages I might have thought of something like:

type Update<T> {
    T Old
    T New
}

type Insert<T> {
    T New
}

But not sure how to model this in Go. I created also a playground sample that shows the whole code in one program: https://play.golang.org/p/ZMnB5K7RaI

  • 写回答

1条回答 默认 最新

  • doutangshuan6473 2017-08-01 10:07
    关注

    have a look at this solution. It is one possible solution.

    Generally: you want to work with interfaces here. In the sample I use the interface DataRow to store data of a row of any table. All table structs have to implement 2 functions as you can see in my example. (Also see my note about a general function in a base class with generics)

    Here the code again:

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

    If you want to get the actual row from DataRow cast to the correct type use (of type Foo in this example):

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

    Hope this helps you in you golang quest!

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥15 drone 推送镜像时候 purge: true 推送完毕后没有删除对应的镜像,手动拷贝到服务器执行结果正确在样才能让指令自动执行成功删除对应镜像,如何解决?
  • ¥15 求daily translation(DT)偏差订正方法的代码
  • ¥15 js调用html页面需要隐藏某个按钮
  • ¥15 ads仿真结果在圆图上是怎么读数的
  • ¥20 Cotex M3的调试和程序执行方式是什么样的?
  • ¥20 java项目连接sqlserver时报ssl相关错误
  • ¥15 一道python难题3
  • ¥15 牛顿斯科特系数表表示
  • ¥15 arduino 步进电机
  • ¥20 程序进入HardFault_Handler