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

huangapple go评论119阅读模式
英文:

How to model this composite type hierarchy without generics

问题

我有一个解析日志文件的系统,其中包含mysql表的更改集,类似于binlog。可以进行更新和插入操作,暂时忽略删除操作。我的模块的功能是接收以下输入:

  1. type Changeset struct {
  2. Table string // 受影响的表
  3. Type string // INSERT 或者 UPDATE
  4. OldData map[string]string // 这两个字段包含表行的所有列
  5. NewData map[string]string
  6. }

ChangesetINSERT更改集时,OldData为空;当ChangesetUPDATE更改集时,OldDataNewData都有值(更新前后的数据)。

现在我不想在我的模块中使用这样的非类型化数据,因为我需要建模一些领域,使用类型安全会更好。然而,我仍然需要保留更改是插入还是更新的信息,以供领域逻辑使用(例如,如果是更新,我将验证某些字段是否未更改)。

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

  1. type Foo struct { // foo 表
  2. Id string
  3. // ... 想象更多字段 ...
  4. }
  5. type Bar struct { // bar 表
  6. Id string
  7. // ... 想象更多字段 ...
  8. }

现在我可以将Changeset.OldDataChangeset.NewData中的map[string][string]映射到相应的结构体,但是我不再知道更改是插入还是更新。我思考了一下,但我能想到的最好的解决方案是:

  1. type FooInsert struct {
  2. New Foo
  3. }
  4. type FooUpdate struct {
  5. New Foo
  6. Old Foo
  7. }
  8. type BarInsert struct {
  9. New Bar
  10. }
  11. type BarUpdate struct {
  12. New Bar
  13. Old Bar
  14. }

映射代码如下:

  1. func doMap(c Changeset) interface{} {
  2. if c.Table == "foo" {
  3. switch c.Type {
  4. case "UPDATE":
  5. return FooUpdate{Old: Foo{Id: c.OldData["id"]}, New: Foo{Id: c.NewData["id"]}}
  6. case "INSERT":
  7. return FooInsert{New: Foo{Id: c.NewData["id"]}}
  8. }
  9. }
  10. if c.Table == "bar" {
  11. switch c.Type {
  12. // ... 几乎与上面相同,但返回 BarUpdate/BarInsert ...
  13. }
  14. }
  15. return nil
  16. }

优点是,它使我能够在此映射函数的结果上进行类型切换,例如:

  1. insertChangeset := Changeset{
  2. Table: "foo",
  3. Type: "INSERT",
  4. NewData: map[string]string{"id": "1"},
  5. }
  6. o := doMap(insertChangeset)
  7. switch o.(type) {
  8. case BarUpdate:
  9. println("Got an update of table bar")
  10. case FooUpdate:
  11. println("Got an update of table foo")
  12. case BarInsert:
  13. println("Got an insert to table bar")
  14. case FooInsert:
  15. println("Got an insert to table foo")
  16. }

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

  • doMap中所示的映射代码非常丑陋和重复。
  • 对于每个新实体X,我需要创建两个更多的类型XInsertXUpdate

有没有办法解决这个混乱的问题?在其他编程语言中,我可能会考虑类似于以下的解决方案:

  1. type Update<T> {
  2. T Old
  3. T New
  4. }
  5. type Insert<T> {
  6. T New
  7. }

但不确定如何在Go中建模这个。我还创建了一个演示示例,其中包含完整的代码:https://play.golang.org/p/ZMnB5K7RaI

英文:

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:

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

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:

  1. type Foo struct { // foo table
  2. Id string
  3. // ... imagine more fields here ...
  4. }
  5. type Bar struct { // bar table
  6. Id string
  7. // ... imagine more fields here ...
  8. }

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:

  1. type FooInsert struct {
  2. New Foo
  3. }
  4. type FooUpdate struct {
  5. New Foo
  6. Old Foo
  7. }
  8. type BarInsert struct {
  9. New Bar
  10. }
  11. type BarUpdate struct {
  12. New Bar
  13. Old Bar
  14. }

And the mapping code looks like this:

  1. func doMap(c Changeset) interface{} {
  2. if c.Table == &quot;foo&quot; {
  3. switch c.Type {
  4. case &quot;UPDATE&quot;:
  5. return FooUpdate{Old: Foo{Id: c.OldData[&quot;id&quot;]}, New: Foo{Id: c.NewData[&quot;id&quot;]}}
  6. case &quot;INSERT&quot;:
  7. return FooInsert{New: Foo{Id: c.NewData[&quot;id&quot;]}}
  8. }
  9. }
  10. if c.Table == &quot;bar&quot; {
  11. switch c.Type {
  12. // ... almost same as above, but return BarUpdate/BarInsert ...
  13. }
  14. }
  15. return nil
  16. }

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

  1. insertChangeset := Changeset{
  2. Table: &quot;foo&quot;,
  3. Type: &quot;INSERT&quot;,
  4. NewData: map[string]string{&quot;id&quot;: &quot;1&quot;},
  5. }
  6. o := doMap(insertChangeset)
  7. switch o.(type) {
  8. case BarUpdate:
  9. println(&quot;Got an update of table bar&quot;)
  10. case FooUpdate:
  11. println(&quot;Got an update of table foo&quot;)
  12. case BarInsert:
  13. println(&quot;Got an insert to table bar&quot;)
  14. case FooInsert:
  15. println(&quot;Got an insert to table foo&quot;)
  16. }

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:

  1. type Update&lt;T&gt; {
  2. T Old
  3. T New
  4. }
  5. type Insert&lt;T&gt; {
  6. T New
  7. }

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

得分: 0

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

总的来说,你想在这里使用接口。在示例中,我使用接口DataRow来存储任何表的一行数据。所有的表结构都必须实现我在示例中所看到的两个函数。(还请参阅我关于在基类中使用泛型的一般函数的注释)

以下是代码:

  1. package main
  2. import "fmt"
  3. type Foo struct {
  4. Id string
  5. }
  6. func (s *Foo) Fill(m map[string]string) {
  7. // 如果你想构建一个通用的 Fill 函数,你可以构建一个基础结构体,用 reflect 来处理 Foo、Bar 等。请问你是否需要我最近构建的一个这样的结构体,但请注意,它的速度会比在这里实现的函数慢!
  8. s.Id = m["id"]
  9. }
  10. func (s *Foo) GetRow() interface{} {
  11. return nil
  12. }
  13. type Bar struct {
  14. Id string
  15. }
  16. func (s *Bar) Fill(m map[string]string) {
  17. s.Id = m["id"]
  18. }
  19. func (s *Bar) GetRow() interface{} {
  20. return nil
  21. }
  22. type DataRow interface {
  23. Fill(m map[string]string)
  24. GetRow() interface{}
  25. }
  26. type Changeset struct {
  27. Table string
  28. Type string
  29. OldData map[string]string
  30. NewData map[string]string
  31. }
  32. type ChangesetTyped struct {
  33. Table string
  34. Type string
  35. OldData DataRow
  36. NewData DataRow
  37. }
  38. func doMap(c Changeset) ChangesetTyped {
  39. ct := ChangesetTyped{
  40. Table: c.Table,
  41. Type: c.Type,
  42. OldData: parseRow(c.Table, c.OldData),
  43. }
  44. if c.Type == "UPDATE" {
  45. ct.NewData = parseRow(c.Table, c.NewData)
  46. }
  47. return ct
  48. }
  49. func parseRow(table string, data map[string]string) (row DataRow) {
  50. if table == "foo" {
  51. row = &Foo{}
  52. } else if table == "bar" {
  53. row = &Bar{}
  54. }
  55. row.Fill(data)
  56. return
  57. }
  58. func main() {
  59. i := Changeset{
  60. Table: "foo",
  61. Type: "INSERT",
  62. NewData: map[string]string{"id": "1"},
  63. }
  64. u1 := Changeset{
  65. Table: "foo",
  66. Type: "UPDATE",
  67. OldData: map[string]string{"id": "20"},
  68. NewData: map[string]string{"id": "21"},
  69. }
  70. u2 := Changeset{
  71. Table: "bar",
  72. Type: "UPDATE",
  73. OldData: map[string]string{"id": "30"},
  74. NewData: map[string]string{"id": "31"},
  75. }
  76. m1 := doMap(i)
  77. m2 := doMap(u1)
  78. m3 := doMap(u2)
  79. fmt.Println(m1, m1.OldData)
  80. fmt.Println(m2, m2.OldData, m2.NewData)
  81. fmt.Println(m3, m3.OldData, m3.NewData)
  82. }

如果你想从DataRow获取实际的行,并将其转换为正确的类型(在此示例中为Foo类型),可以使用以下代码:

  1. foo, ok := dt.GetRow().(Foo)
  2. if !ok {
  3. fmt.Println("它实际上不是Foo类型")
  4. }

希望这对你在Go语言的探索中有所帮助!

英文:

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:

  1. package main
  2. import &quot;fmt&quot;
  3. type Foo struct {
  4. Id string
  5. }
  6. func (s *Foo) Fill(m map[string]string) {
  7. // If you want to build a general Fill you can build a base struct for Foo, Bar, etc. that works with reflect.
  8. // Note that it will be slower than implementing the function here! Ask me if you want one I built recently.
  9. s.Id = m[&quot;id&quot;]
  10. }
  11. func (s *Foo) GetRow() interface{} {
  12. return nil
  13. }
  14. type Bar struct {
  15. Id string
  16. }
  17. func (s *Bar) Fill(m map[string]string) {
  18. s.Id = m[&quot;id&quot;]
  19. }
  20. func (s *Bar) GetRow() interface{} {
  21. return nil
  22. }
  23. type DataRow interface {
  24. Fill(m map[string]string)
  25. GetRow() interface{}
  26. }
  27. type Changeset struct {
  28. Table string
  29. Type string
  30. OldData map[string]string
  31. NewData map[string]string
  32. }
  33. type ChangesetTyped struct {
  34. Table string
  35. Type string
  36. OldData DataRow
  37. NewData DataRow
  38. }
  39. func doMap(c Changeset) ChangesetTyped {
  40. ct := ChangesetTyped{
  41. Table: c.Table,
  42. Type: c.Type,
  43. OldData: parseRow(c.Table, c.OldData),
  44. }
  45. if c.Type == &quot;UPDATE&quot; {
  46. ct.NewData = parseRow(c.Table, c.NewData)
  47. }
  48. return ct
  49. }
  50. func parseRow(table string, data map[string]string) (row DataRow) {
  51. if table == &quot;foo&quot; {
  52. row = &amp;Foo{}
  53. } else if table == &quot;bar&quot; {
  54. row = &amp;Bar{}
  55. }
  56. row.Fill(data)
  57. return
  58. }
  59. func main() {
  60. i := Changeset{
  61. Table: &quot;foo&quot;,
  62. Type: &quot;INSERT&quot;,
  63. NewData: map[string]string{&quot;id&quot;: &quot;1&quot;},
  64. }
  65. u1 := Changeset{
  66. Table: &quot;foo&quot;,
  67. Type: &quot;UPDATE&quot;,
  68. OldData: map[string]string{&quot;id&quot;: &quot;20&quot;},
  69. NewData: map[string]string{&quot;id&quot;: &quot;21&quot;},
  70. }
  71. u2 := Changeset{
  72. Table: &quot;bar&quot;,
  73. Type: &quot;UPDATE&quot;,
  74. OldData: map[string]string{&quot;id&quot;: &quot;30&quot;},
  75. NewData: map[string]string{&quot;id&quot;: &quot;31&quot;},
  76. }
  77. m1 := doMap(i)
  78. m2 := doMap(u1)
  79. m3 := doMap(u2)
  80. fmt.Println(m1, m1.OldData)
  81. fmt.Println(m2, m2.OldData, m2.NewData)
  82. fmt.Println(m3, m3.OldData, m3.NewData)
  83. }

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

  1. foo, ok := dt.GetRow().(Foo)
  2. if !ok {
  3. fmt.Println(&quot;it wasn&#39;t of type Foo after all&quot;)
  4. }

Hope this helps you in you golang quest!

huangapple
  • 本文由 发表于 2017年8月1日 17:27:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/45433986.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定