英文:
How to model this composite type hierarchy without generics
问题
我有一个解析日志文件的系统,其中包含mysql表的更改集,类似于binlog。可以进行更新和插入操作,暂时忽略删除操作。我的模块的功能是接收以下输入:
type Changeset struct {
Table string // 受影响的表
Type string // INSERT 或者 UPDATE
OldData map[string]string // 这两个字段包含表行的所有列
NewData map[string]string
}
当Changeset
是INSERT
更改集时,OldData
为空;当Changeset
是UPDATE
更改集时,OldData
和NewData
都有值(更新前后的数据)。
现在我不想在我的模块中使用这样的非类型化数据,因为我需要建模一些领域,使用类型安全会更好。然而,我仍然需要保留更改是插入还是更新的信息,以供领域逻辑使用(例如,如果是更新,我将验证某些字段是否未更改)。
假设我有两个表(假设它们只有一个名为Id的字段,但实际上它们有更多和不同的字段)。所以我将这些对象建模如下:
type Foo struct { // foo 表
Id string
// ... 想象更多字段 ...
}
type Bar struct { // bar 表
Id string
// ... 想象更多字段 ...
}
现在我可以将Changeset.OldData
和Changeset.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 {
// ... 几乎与上面相同,但返回 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
,我需要创建两个更多的类型XInsert
和XUpdate
。
有没有办法解决这个混乱的问题?在其他编程语言中,我可能会考虑类似于以下的解决方案:
type Update<T> {
T Old
T New
}
type Insert<T> {
T New
}
但不确定如何在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:
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 typesXInsert
andXUpdate
.
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
得分: 0
请看一下这个解决方案。这是一个可能的解决方案。
总的来说,你想在这里使用接口。在示例中,我使用接口DataRow
来存储任何表的一行数据。所有的表结构都必须实现我在示例中所看到的两个函数。(还请参阅我关于在基类中使用泛型的一般函数的注释)
以下是代码:
package main
import "fmt"
type Foo struct {
Id string
}
func (s *Foo) Fill(m map[string]string) {
// 如果你想构建一个通用的 Fill 函数,你可以构建一个基础结构体,用 reflect 来处理 Foo、Bar 等。请问你是否需要我最近构建的一个这样的结构体,但请注意,它的速度会比在这里实现的函数慢!
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("它实际上不是Foo类型")
}
希望这对你在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:
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!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论