惯用地干燥Go中的常见字段

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?

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
", 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.

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