在Go语言中,习惯上使用DRY原则来消除重复的代码。

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

Idiomatically DRYing up common fields in Go

问题

我正在为一个API编写客户端。其中一个方法posts返回一个用户帖子的数组。

  • 每个帖子都属于八种不同的类型,明显是一种"is-a"关系。
  • 帖子的许多字段,包括(但不限于)ID、URL和时间戳,对于每种类型的帖子都是共同的。
  • 每种类型的帖子都有其特定类型的字段。例如,照片帖子将具有分辨率和标题。

在具有继承的语言中,我会创建一个抽象基类Post,然后从该基类派生出每种类型的具体类。我会在基类Post中定义一个构造函数或工厂方法,例如fromJson(),该方法接受一个JSON对象并提取所有共同字段。然后,我会在每个子类中重写该方法以提取特定的字段,并确保调用基类实现以避免重复提取共同字段。

Go语言没有继承,但它有组合。我定义了一个Post结构体,其中包含所有共同的字段,然后我为每种类型创建了一个结构体,该结构体具有一个匿名的Post字段,以便包含所有Post字段。例如:

type PhotoPost struct {
    Post // 包含:ID int; URL string; 等等
    Caption string
    Height int
    Width int
    // 等等
}

我的一个目标是让我客户端的用户能够轻松访问Post的共同字段。所以我绝对不希望Posts()方法只返回interface{},因为这样每当有人想要获取所有帖子的ID时,他们都必须进行可怕的类型切换,这是一种会反复使用的模式,让我感到不舒服:

func GetIDs(posts []interface{}) []int {
    var ids []int
    for _, p := range posts {
        switch p.(type) {
            case PhotoPost:
                ids = append(ids, p.(PhotoPost).ID)
            //... 重复处理其他七种类型的帖子,并不要忘记添加默认情况!
        }
    }
}

这太糟糕了。但是我也不能让Posts方法返回[]Post,因为当需要更专门的数据时(例如“给我这个用户的所有照片标题”),它将不会存在(因为在Go中,PhotoPost不是Post,它一个Post和它的字段)。

目前,我正在考虑让Posts()方法返回一个PostCollection,它是一个结构体,看起来像这样,这样至少可以避免上面那种可怕的类型切换:

type PostCollection struct {
    PhotoPosts []PhotoPost
    // ... 为其他类型重复
}

但是,对于“将所有帖子的ID放入一个切片中”之类的用例仍然非常麻烦。有人能否提供一种处理这个问题的惯用方法?最好不需要使用反射。

我一直在考虑让每种类型的帖子在TypedPost接口中实现一个PostData()方法,该方法返回其自己的Post,但是除非我同时拥有一个命名的一个匿名类型,否则似乎不存在这样的方法(匿名类型使我可以在我知道自己有一个PhotoPost并且想要时说somePhotoPost.ID,而在我只知道自己正在处理某种类型的TypedPost时说somTypedPost.PostData().ID)。然后,我将让Posts()方法返回[]TypedPost。有更好的方法吗?

英文:

I'm writing a client for an API. One method, posts, returns an array of users' posts.

  • Each post is one of eight different types. Clearly, an "is-a" relationship.
  • Many of the fields of the post, including (among others) the ID, URL and time stamp are common to every type of post.
  • Each type of post has fields unique to its type. For example, a photo post will have a resolution and a caption.

In a language with inheritance, I would make an abstract base class Post and then subclass that to make one concrete class for each type of post. I would have a constructor or factory method in the base Post, maybe fromJson(), that takes a JSON object and extracts all the common fields. Then I would override that in each subclass to extract the specialized fields, making sure to call the base implementation to DRY up the extraction of the common fields.

Go does not have inheritance, but it has composition. I defined a Post struct having all the common fields, then I made a struct for each type which has an anonymous Post field so that it includes all the Post fields. For example,

type PhotoPost struct {
    Post // which contains: ID int; URL string; etc
    Caption string
    Height int
    Width int
    /// etc
}

One of my goals is that I want to make it easy for users of my client to access the common fields of the Post. So I definitely don't want to just have the Posts() method that I am writing return interface{}, because then anytime someone wants to get the IDs of all the posts, for example, they would have to make a horrible type switch which would be a pattern used over and over and makes me cringe:

func GetIDs(posts []interface{}) []int {
    var ids []int
    for _, p := range posts {
        switch p.(type) {
            case PhotoPost:
                ids = append(ids, p.(PhotoPost).ID)
            //... repeat for the 7 other kinds of posts, and don't forget a default!
        }
    }
}

This is just awful. But I can't have Posts return []Post, because then when the more specialized data is needed (for use cases like "give me all the photo captions from this user's posts"), it won't be there (because in Go, a PhotoPost is not a Post, it has a Post and its fields.

At the moment, I'm contemplating having Posts() return a PostCollection, which would be a struct that would look like this, so that at least I would avoid the type switch monstrosity above:

type PostCollection struct {                                                                                                                           
        PhotoPosts   []PhotoPost
        // ...repeat for the others
}

but the use case for "get all IDs of all the posts into a slice" or something similar is still very cumbersome. Can someone suggest an idiomatic way to deal with this problem? Preferably one that doesn't require reflection?

I've been pondering having each type of Post implement a PostData() method in a TypedPost interface that returns its own Post, but it doesn't look like that exists unless I have both a named and an anonymous type which seems strange (anonymous so that I can say somePhotoPost.ID when I know I have a PhotoPost want to, and someTypedPost.PostData().ID when I just know that I'm dealing with a TypedPost of some kind. Then I'd have Posts() return []TypedPost. Is there a better way?

答案1

得分: 6

为一个帖子定义一个interface-除了通过接口访问公共数据元素外,不要访问其他数据。

以下是一个示例。请注意Post接口,它定义了所有帖子可以做什么(但不包括它们拥有的数据)。playground

// 关于帖子的基本信息
type PostInfo struct {
    ID  int
    URL string
}

// 满足帖子接口的方法
func (p *PostInfo) Info() *PostInfo {
    return p
}

// 定义帖子可以做什么的接口
type Post interface {
    Info() *PostInfo
}

type PhotoPost struct {
    PostInfo // 包含:ID int; URL string; 等等
    Caption  string
    Height   int
    Width    int
    // 等等
}

func GetIDs(posts []Post) []int {
    var ids []int
    for _, p := range posts {
        ids = append(ids, p.Info().ID)
    }
    return ids
}

func main() {
    p0 := &PostInfo{1, "url0"}
    p1 := &PhotoPost{PostInfo{2, "url1"}, "img", 16, 32}
    posts := []Post{p0, p1}
    fmt.Printf("Post IDs %v\n", GetIDs(posts))
}

如果你的代码中有一个类型开关来切换你自己的对象,那么你在定义接口时就出错了。

请注意,你可以定义一些帖子满足的接口,并使用类型转换来判断它们是否实现了该接口。

英文:

Define an interface for a Post - don't access common data elements except through an interface.

Here is an example. Note the Post interface which defines what all posts can do (but not what data they have in). playground

// Basic information about a post
type PostInfo struct {
	ID  int
	URL string
}

// To satisfy the post interface
func (p *PostInfo) Info() *PostInfo {
	return p
}

// Interface that defines what a Post can do
type Post interface {
	Info() *PostInfo
}

type PhotoPost struct {
	PostInfo // which contains: ID int; URL string; etc
	Caption  string
	Height   int
	Width    int
	/// etc
}

func GetIDs(posts []Post) []int {
	var ids []int
	for _, p := range posts {
		ids = append(ids, p.Info().ID)
	}
	return ids
}

func main() {
	p0 := &PostInfo{1, "url0"}
	p1 := &PhotoPost{PostInfo{2, "url1"}, "img", 16, 32}
	posts := []Post{p0, p1}
	fmt.Printf("Post IDs %v\n", GetIDs(posts))
}

If your code has a type switch to switch over your own objects then you've gone wrong with defining interfaces.

Note that you can define interfaces which a subset of your posts satisfy and use a type cast to see if they implement it.

答案2

得分: 1

一个更简单的方法是使用接口playground

type PostInterface interface {
    Id() int
}
type Post struct {
    ID int
}

func (p Post) Id() int {
    return p.ID
}

type PhotoPost struct {
    Post
}

func GetIDs(posts ...PostInterface) (ids []int) {
    ids = make([]int, len(posts))
    for i := range posts {
        p := posts[i]
        ids[i] = p.Id()
        switch pt := p.(type) {
        case PhotoPost:
            fmt.Println("PhotoPost, width =", pt.Width)
        }
    }
    return
}

func main() {
    pp := []PostInterface{
        PhotoPost{Post: Post{10}, Width: 20},
        PhotoPost{Post: Post{20}},
        PhotoPost{Post: Post{30}},
        PhotoPost{Post: Post{40}},
    }
    fmt.Println(GetIDs(pp...))
}
英文:

A much simpler approach is using interfaces playground:

type PostInterface interface {
	Id() int
}
type Post struct {
	ID int
}

func (p Post) Id() int {
	return p.ID
}

type PhotoPost struct {
	Post
}

func GetIDs(posts ...PostInterface) (ids []int) {
	ids = make([]int, len(posts))
	for i := range posts {
		p := posts[i]
		ids[i] = p.Id()
		switch pt := p.(type) {
		case PhotoPost:
			fmt.Println("PhotoPost, width =", pt.Width)
		}
	}
	return
}

func main() {
	pp := []PostInterface{
		PhotoPost{Post: Post{10}, Width: 20},
		PhotoPost{Post: Post{20}},
		PhotoPost{Post: Post{30}},
		PhotoPost{Post: Post{40}},
	}
	fmt.Println(GetIDs(pp...))
}

huangapple
  • 本文由 发表于 2014年6月16日 01:44:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/24232385.html
匿名

发表评论

匿名网友

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

确定