英文:
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...))
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论