建模一组相关事物的层次结构,但没有语言支持类型层次结构。

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

Modeling a hierarchy of related things without language support for type hierarchies

问题

我是Go的新手,我想做的第一件事之一就是将我的小型标记页面生成库移植到Go。主要实现是用Ruby编写的,它在设计上非常“经典的面向对象”(至少从一个业余程序员的角度来理解面向对象)。它模拟了我对标记文档类型之间关系的看法:

                                      Page
                                   /        \
                          HTML Page          Wiki Page
                         /         \
              HTML 5 Page           XHTML Page

对于一个小项目,我可能会这样做(现在要转换为Go):

p := dsts.NewHtml5Page()
p.Title = "A Great Title"
p.AddStyle("default.css")
p.AddScript("site_wide.js")
p.Add("<p>A paragraph</p>")
fmt.Println(p) // 输出与上述相对应的有效的HTML 5页面

对于较大的项目,比如一个名为“Egg Sample”的网站,我会创建一个更深层次的继承关系:

                                 HTML 5 Page
                                      |
                               Egg Sample Page
                             /        |        \
               ES Store Page    ES Blog Page     ES Forum Page

这很适合经典的面向对象设计:子类可以免费获得很多东西,它们只需要关注与父类不同的几个部分。例如,EggSamplePage可以添加一些在所有Egg Sample页面中都通用的菜单和页脚。

然而,Go没有类型层次结构的概念:没有类,也没有类型继承。它也没有方法的动态分派(这似乎是从上面推导出来的;Go类型HtmlPage不是Go类型Page的“一种”)。

Go提供了以下功能:

  • 嵌入
  • 接口

这两个工具似乎足以实现我想要的功能,但是经过几次失败的尝试后,我感到困惑和沮丧。我猜我可能是在错误的思考方式,希望有人能指点我正确的方向,告诉我如何以“Go方式”解决这个问题。

这是一个我正在遇到的具体实际问题,因此,欢迎任何关于解决我特定问题的建议,而不涉及更广泛的问题。但我希望答案是以“通过结构、嵌入和接口的某种方式组合,您可以轻松地获得所需的行为”为形式,而不是绕过这一点的内容。我认为许多从经典面向对象语言转向Go的新手可能会经历类似的困惑时期。

通常我会在这里展示我的错误代码,但是我有几个版本,每个版本都有自己的问题,我不认为包含它们实际上会增加对我的问题的理解,而我的问题已经变得相当长了。如果代码被证明有用,我当然会添加代码。

我已经做过的事情:

  • 阅读了大部分Go FAQ(尤其是与问题相关的部分)
  • 阅读了大部分Effective Go(尤其是与问题相关的部分)
  • 使用许多搜索词在Google上搜索
  • 阅读了golang-nuts上的各种帖子
  • 编写了很多不完善的Go代码
  • 查看了Go标准库源代码中的一些类似的示例

为了更明确地说明我正在寻找什么:

  • 我想学习处理这种层次结构的Go惯用方式。我最有效的尝试之一似乎最不像Go:

type page struct {
    Title     string
    content   bytes.Buffer
    openPage  func() string
    closePage func() string
    openBody  func() string
    closeBody func() string
}

这让我接近目标,但还不够。我现在的观点是,这似乎是一个学习Go程序员在这种情况下使用的惯用法的失败机会。
  • 我希望尽可能地DRY(“不要重复自己”);当每个模板的大部分内容与其他模板相同时,我不希望为每种类型的页面使用单独的text/template。我废弃的一个实现就是这样工作的,但是一旦我有一个更复杂的页面类型层次结构(如上所述),它似乎会变得难以管理。

  • 我希望能够有一个核心库包,可以直接用于支持的类型(例如html5PagexhtmlPage),并且可以根据上述进行扩展,而不必直接复制和编辑库。(在经典的面向对象编程中,我扩展/子类化Html5Page并进行一些调整,例如。)我的当前尝试似乎并不适合这一点。

我希望正确的答案不需要太多的代码来解释Go的思考方式。

**更新:**根据迄今为止的评论和答案,似乎我离正确答案并不远。我的问题可能没有那么普遍的设计导向,而是与我具体的做事方式有关。所以这是我正在处理的问题:

type page struct {
    Title    string

    content  bytes.Buffer
}

type HtmlPage struct {
    page

    Encoding   string
    HeaderMisc string

    styles   []string
    scripts  []string
}

type Html5Page struct {
    HtmlPage
}

type XhtmlPage struct {
    HtmlPage

    Doctype string
}

type pageStringer interface {
    openPage()   string
    openBody()   string
    contentStr() string
    closeBody()  string
    closePage()  string
}

type htmlStringer interface {
    pageStringer

    openHead()   string
    titleStr()   string
    stylesStr()  string
    scriptsStr() string
    contentTypeStr() string
}

func PageString(p pageStringer) string {
    return headerString(p) + p.contentStr() + footerString(p)
}

func headerString(p pageStringer) string {
    return p.openPage() + p.openBody()
}

func HtmlPageString(p htmlStringer) string {
    return htmlHeaderString(p) + p.contentStr() + footerString(p)
}

func htmlHeaderString(p htmlStringer) string {
    return p.openPage() +
        p.openHead() + p.titleStr() + p.stylesStr() + p.scriptsStr() + p.contentTypeStr() +
        p.openBody()
}

这个代码可以工作,但是有几个问题:

  1. 它感觉非常笨拙
  2. 我在重复自己
  3. 可能不可能,但理想情况下,我希望所有Page类型都有一个String()方法,可以做正确的事情,而不必使用函数。

我强烈怀疑我做错了什么,而且有Go惯用法可以使这个问题变得更好。

我希望有一个String()方法,可以做正确的事情,但是

func (p *page) String() string {
    return p.headerString() + p.contentStr() + p.footerString()
}

将始终使用page方法,即使通过HtmlPage使用,因为除了接口之外,没有动态分派的地方。

使用我当前的基于接口的页面生成,不仅不能只做fmt.Println(p)(其中p是某种类型的Page),而且我必须明确选择fmt.Println(dsts.PageString(p))fmt.Println(dsts.HtmlPageString(p))之间。这感觉非常不对。

而且我在PageString() / HtmlPageString()headerString() / htmlHeaderString()之间重复了代码。

所以我觉得我仍然在遭受设计问题,部分原因是仍然在以Ruby或Java而不是Go的方式思考。我希望有一种直接和惯用的Go方式来构建一个具有类似我描述的客户端接口的库。

英文:

I'm new to Go, and one of the first things I want to do is to port my little marked-up-page-generation library to Go. The primary implementation is in Ruby, and it is very much "classical object orientation" in its design (at least as I understand OO from an amateur programmer's perspective). It models how I see the relationship between marked-up document types:

<!-- language: none -->

                                      Page
                                   /        \
                          HTML Page          Wiki Page
                         /         \
              HTML 5 Page           XHTML Page

For a small project, I might do something like this (translated to the Go I now want):

<!-- language: lang-go -->

p := dsts.NewHtml5Page()
p.Title = &quot;A Great Title&quot;
p.AddStyle(&quot;default.css&quot;)
p.AddScript(&quot;site_wide.js&quot;)
p.Add(&quot;&lt;p&gt;A paragraph&lt;/p&gt;&quot;)
fmt.Println(p) // Output a valid HTML 5 page corresponding to the above

For larger projects, say for a website called "Egg Sample", I subclass one of the existing Page types, creating a deeper hierarchy:

<!-- language: none -->

                                 HTML 5 Page
                                      |
                               Egg Sample Page
                             /        |        \
               ES Store Page    ES Blog Page     ES Forum Page

This fits well into classical object-oriented design: subclasses get a lot for free, and they just focus on the few parts that are different from their parent class. EggSamplePage can add some menus and footers that are common across all Egg Sample pages, for example.

Go, however, does not have a concept of a hierarchy of types: there are no classes and there is no type inheritance. There's also no dynamic dispatch of methods (which seems to me to follow from the above; a Go type HtmlPage is not a "kind of" Go type Page).

Go does provide:

  • Embedding
  • Interfaces

It seems those two tools should be enough to get what I want, but after several false starts, I'm feeling stumped and frustrated. My guess is that I'm thinking about it wrong, and I'm hoping someone can point me in the right direction for how to do this the "Go way".

This is a specific, real problem I'm having, and as such, any suggestions about solving my particular problem without addressing the broader question are welcome. But I'm hoping the answer will be in the form of "by combining structures, embedding, and interfaces in such-and-such a manner, you can easily have the behavior you want" rather than something that sidesteps that. I think many newcomers to Go transitioning from classical-OO languages likely go through a similar period of confusion.

Normally I would show my broken code here, but I've got several versions, each with their own problems, and I don't imagine including them will actually add any clarity to my question, which is already getting quite long. I will of course add code if it turns out to seem useful.

Things I've done:

  • Read much of The Go FAQ (especially the parts that seemed relevant)
  • Read much of Effective Go (especially the parts that seemed relevant)
  • Searched Google with many combinations of search terms
  • Read various posts on golang-nuts
  • Written much inadequate Go code
  • Looked through the Go standard library source code for any examples that seemed similar

To be a little more explicit about what I'm looking for:

  • I want to learn the idiomatic Go way of dealing with hierarchies like this. One of my more effective attempts seems the least Go-like:

    <!-- language: lang-go -->

     type page struct {
         Title     string
         content   bytes.Buffer
         openPage  func() string
         closePage func() string
         openBody  func() string
         closeBody func() string
     }
    

    This got me close, but not all the way. My point right now is that it seems like a failed opportunity to learn the idioms Go programmers use in situations like this.

  • I want to be as DRY ("Don't Repeat Yourself") as is reasonable; I don't want a separate text/template for each type of page when so much of each template is identical to others. One of my discarded implementations works this way, but it seems it would become unmanageable once I get a more complex hierarchy of page types as outlined above.

  • I'd like to be able to have a core library package that is usable as-is for the types that it supports (e.g. html5Page and xhtmlPage), and is extensible as outlined above without resorting to copying and editing the library directly. (In classical OO, I extend/subclass Html5Page and make a few tweaks, for example.) My current attempts haven't seemed to lend themselves to this very well.

I expect the correct answer won't need much code to explain the Go way of thinking about this.

Update: Based on the comments and answers so far, it seems I wasn't so far off. My problems must be a little less generally design-oriented than I thought, and a little more about exactly how I'm doing things. So here's what I'm working with:

type page struct {
	Title    string

	content  bytes.Buffer
}

type HtmlPage struct {
	page

	Encoding   string
	HeaderMisc string

	styles   []string
	scripts  []string
}

type Html5Page struct {
	HtmlPage
}

type XhtmlPage struct {
	HtmlPage

	Doctype string
}

type pageStringer interface {
	openPage()   string
	openBody()   string
	contentStr() string
	closeBody()  string
	closePage()  string
}

type htmlStringer interface {
	pageStringer

	openHead()   string
	titleStr()   string
	stylesStr()  string
	scriptsStr() string
	contentTypeStr() string
}

func PageString(p pageStringer) string {
	return headerString(p) + p.contentStr() + footerString(p)
}

func headerString(p pageStringer) string {
	return p.openPage() + p.openBody()
}

func HtmlPageString(p htmlStringer) string {
	return htmlHeaderString(p) + p.contentStr() + footerString(p)
}

func htmlHeaderString(p htmlStringer) string {
	return p.openPage() +
		p.openHead() + p.titleStr() + p.stylesStr() + p.scriptsStr() + p.con    tentTypeStr() +
		p.openBody()
}

This works, but it has several problems:

  1. It feels really awkward
  2. I'm repeating myself
  3. It might not be possible, but ideally I'd like all Page types to have a String() method that does the right thing, rather than having to use a function.

I strongly suspect that I'm doing something wrong and that there are Go idioms that could make this better.

I'd like to have a String() method that does the right thing, but

func (p *page) String( string {
	return p.headerString() + p.contentStr() + p.footerString()
}

will always use the page methods even when used through an HtmlPage, due to lack of dynamic dispatch anywhere but with interfaces.

With my current interface-based page generation, not only do I not get to just do fmt.Println(p) (where p is some kind of Page), but I have to specifically choose between fmt.Println(dsts.PageString(p)) and fmt.Println(dsts.HtmlPageString(p)). That feels very wrong.

And I'm awkwardly duplicating code between PageString() / HtmlPageString() and between headerString() / htmlHeaderString().

So I feel like I'm still suffering design issues as a result of to some extent still thinking in Ruby or Java rather than in Go. I'm hoping there's a straightforward and idiomatic Go way to build a library that has something like the client interface I've described.

答案1

得分: 5

继承结合了两个概念:多态性和代码共享。
Go语言将这些概念分开。

  • 在Go中,通过使用接口来实现多态性('is a')。
  • 在Go中,通过嵌入和对接口进行操作的函数来实现代码共享。

很多从面向对象编程语言过来的人会忘记函数,并且在只使用方法时会迷失方向。

因为Go将这些概念分开,你必须单独思考它们。'Page'和'Egg Sample Page'之间的关系是“is a”关系还是代码共享关系?

英文:

Inheritance combines two concepts. Polymorphism and code sharing.
Go separates these concepts.

  • Polymorphism('is a') in Go is achieved by using interfaces.
  • Code sharing in Go is achieved by embedding and functions that act on interfaces

A lot of people coming from OOP languages forget about functions and get lost using just methods.

Because Go separates these concepts you have to think about them individually. What is the relationship between 'Page' and 'Egg Sample Page'. It is an "is a" relationship or is it a code sharing relationship?

答案2

得分: 3

首先,一个警告:在所有语言中,深层次的层次结构都很难适应。深层次的结构建模通常是一个陷阱:一开始在智力上很令人满意,但最终变成了一场噩梦。

然后,Go语言有嵌入,它实际上是一种组合,但提供了通常需要的大部分功能(可能是多重)继承。

例如,让我们看看这个:

type ConnexionMysql struct {
	*sql.DB
}

type BaseMysql struct {
	user     string
	password string
	database string
}

func (store *BaseMysql) DB() (ConnexionMysql, error) {
	db, err := sql.Open("mymysql", store.database+"/"+store.user+"/"+store.password)
	return ConnexionMysql{db}, err
}

func (con ConnexionMysql) EtatBraldun(idBraldun uint) (*EtatBraldun, error) {
	row := con.QueryRow("select pv, pvmax, pa, tour, dla, faim from compte where id=?", idBraldun)
	// stuff
	return nil, err
}

// somewhere else:
con, err := ms.bd.DB()
defer con.Close()
// ...
somethings, err = con.EtatBraldun(id)

正如你所看到的,只需使用嵌入,我可以:

  • 轻松生成“子类”ConnexionMysql的实例
  • 定义和使用自己的函数,比如EtatBraldun
  • 仍然使用在*sql.DB上定义的函数,比如CloseQueryRow
  • 如果需要(这里没有),添加字段到我的子类并使用它们

我还可以嵌入多个类型。或者将我的ConnexionMysql类型“子类型化”。

在我看来,这是一个很好的折衷方案,它有助于避免深层次继承层次结构的陷阱和僵化。

我说这是一个折衷方案,因为很明显Go不是一种面向对象的语言。缺乏函数重写,正如你所看到的,阻止了通常解决方案中在“超类”中拥有方法来组合“子类”方法的调用。

我理解这可能会让人困惑,但我不确定我是否真的想念通常基于层次结构的解决方案的复杂性和冗长性。正如我所说,一开始很好,但当变得复杂时就会变得痛苦。这就是为什么我建议你尝试Go的方式:

Go接口可以用于减少对继承的需求。实际上,你的页面可以是一个接口(或更符合惯用法的几个接口)和一个结构体:

type pageOpenerCloser interface {
    openPage  func() string
    closePage func() string
    openPage  func() string
    closePage func() string
}

type page struct {
    Title     string
    content   bytes.Buffer
}

由于你不能依赖在pageOpenerCloser的实现上定义的String()方法来简单地调用同一实现上定义的closeBody方法,你必须使用函数而不是方法来完成部分工作,这是我所看到的组合:你必须将你的pageOpenerCloser实例传递给一个组合函数,它将调用正确的实现。

这意味着:

  • 鼓励你拥有原子和正交的接口定义
  • 接口由它们(简单的)动作定义
  • 接口函数不应该调用同一实例上的其他函数
  • 不鼓励你拥有由实现/算法定义的中间级别的深层次层次结构
  • 大型的“组合”算法不应该被重写或通常写多次

我觉得这减少了混乱,并有助于使Go程序小而易懂。

英文:

First, a warning : deep hierarchies are painful to adapt in all languages. Deep hierarchical structural modeling is often a trap : it's intellectually satisfying at first but ends as a nightmare.

Then, Go has embedding, which is really a composition but provides most of what is generally needed with (potentially multiple) inheritance.

For example, let's look at this :

type ConnexionMysql struct {
	*sql.DB
}

type BaseMysql struct {
	user     string
	password string
	database string
}

func (store *BaseMysql) DB() (ConnexionMysql, error) {
	db, err := sql.Open(&quot;mymysql&quot;, store.database+&quot;/&quot;+store.user+&quot;/&quot;+store.password)
	return ConnexionMysql{db}, err
}

func (con ConnexionMysql) EtatBraldun(idBraldun uint) (*EtatBraldun, error) {
	row := con.QueryRow(&quot;select pv, pvmax, pa, tour, dla, faim from compte where id=?&quot;, idBraldun)
	// stuff
	return nil, err
}

// somewhere else:
con, err := ms.bd.DB()
defer con.Close()
// ...
somethings, err = con.EtatBraldun(id)

As you can see, with just embedding I could :

  • easily generate an instance of the "subclass" ConnexionMysql
  • define and use my own functions, like EtatBraldun
  • still use functions defined on *sql.DB, like Close of QueryRow
  • if needed (not present here) add fields to my subclass and use them

And I could embedd more than one type. Or "subtype" my ConnexionMysql type.

In my opinion it's a good compromise and it helps avoiding the traps and rigidity of deep inheritance hierarchies.

I say it's a compromise, because it's clear Go isn't an OOP language. The lack of function overriding, as you saw, prevents the usual solution of having methods in a "superclass" composing the calls of "subclasses" methods.

I understand this may be disturbing, but I'm not sure I really miss the weight and verbosity of the usual hierarchy based solutions. As I said, there are fine at first but painful when it gets complex. That's why I suggest you try the Go way :

Go interfaces can be used to reduce the need for inheritance. In fact your page could be an interface (or more idiomatically a few interfaces) and a struct :

type pageOpenerCloser interface {
    openPage  func() string
    closePage func() string
    openPage  func() string
    closePage func() string
}

type page struct {
    Title     string
    content   bytes.Buffer
}

As you can't rely on a String() method defined on an implementation of pageOpenerCloser to simply call a closeBody method defined on the same implementation, you must use functions, not methods to do part of the work, what I see as composition : you must pass your instance of pageOpenerCloser to a Composing functions that will call the right implementations.

This means

  • you're encouraged to have atomic and orthogonal interface definitions
  • interfaces are defined by their (simple) actions
  • interface functions aren't supposed to call other functions on the same instance
  • you're not encouraged to have a deep hierarchy with intermediate levels defined by implementation/algorithms
  • big "composing" algorithms shouldn't be overrided or generally written more than once

I feel this reduces the clutter and helps make a Go program small and understandable.

答案3

得分: 1

我认为无法令您满意地回答您的问题。但是,让我们从一个不同的背景中进行一个简短的类比,以避免关于编程的任何普遍接受的观念(例如,许多程序员认为面向对象编程是“正确的”编程方式,因为他们多年来一直这样做)。

假设您正在玩一个叫做桥梁建造者的经典游戏。这个游戏的目标是在柱子上建造一座桥梁,以便火车可以从一边通过到另一边。有一天,在掌握了这个游戏多年后,您决定尝试一些新的东西。比如说《传送门2》 建模一组相关事物的层次结构,但没有语言支持类型层次结构。

您轻松地通过了第一关,但是您无法弄清楚如何在第二关到达另一边的平台。所以您问了一个朋友:“嘿,我怎么在《传送门2》中放置柱子?”您的朋友可能会有点困惑,但他可能会告诉您可以拿起那些箱子并将它们叠放在一起。于是,您立即开始收集您能找到的所有箱子,以便建造您通往房间另一边的桥梁。干得好!

无论如何,几个小时后,您发现《传送门2》真的很令人沮丧(收集方块需要很长时间,关卡也非常困难)。所以您停止了游戏。

那么,这里出了什么问题呢?首先,您假设一个游戏中的一种技巧在另一个游戏中可能效果很好。其次,您没有问对问题。与其告诉您的朋友您的问题(“我怎么才能到达那边的平台?”),您问他如何在其他游戏中实现您习惯的那些事情。如果您问了另一个问题,您的朋友可能能够告诉您可以使用传送门枪创建红色和蓝色传送门并穿过它们。

尝试将一个编写良好的Ruby / Java /等程序移植到Go语言中确实令人沮丧。在一种语言中有效的一种方法,在另一种语言中可能效果不佳。您甚至没有告诉我们您试图解决的问题。您只是发布了一些无用的样板代码,显示了一些类层次结构。在Go语言中,您不需要这些,因为Go的接口更加灵活。(类似的类比可以在JavaScript的原型和那些试图以面向对象方式编程的人之间进行绘制)。

在开始时,在Go语言中提出良好的设计是困难的,特别是如果您习惯了面向对象编程。但是,Go语言中的解决方案通常更小、更灵活且更容易理解。仔细研究Go标准库和其他外部包中的所有那些包。例如,我认为leveldb-go比leveldb更容易理解和直观,即使您对这两种语言都很熟悉。

英文:

I think it is not possible to answer your question satisfyingly. But let's start with a short analogy from a different context, to avoid any generally accepted ideas about programming (for example, many programmers believe that OOP is the "right way" to program, because that is what they have done for years now).

Let's suppose you are playing a classical game called Bridge Builder. The goal of this game is to build a bridge on top of pillars, so that a train can pass from one side to the other. One day, after mastering the game for years, you decide that you want to try something new. Let's say Portal 2 建模一组相关事物的层次结构,但没有语言支持类型层次结构。

You easily manage the first level, but you can't figure out how you can get to platform on the other side at the 2nd one. So you ask a friend: "Hey, how can I place pillars in Portal 2"? Your friend might look sightly confused, but he might tell you that you can pick up those boxes and place them on top of each other. So, you immediately start collecting all boxes you can find to build your bridge to your other side of the room. Well done!

Anyway, after a couple of hours, you find Portal 2 really frustrating (it takes ages to collect the blocks and the levels are really difficult). So you stop playing.

So, what went wrong here? First, you assumed that one technique from one game, might work well in another. Second, you haven't asked the right question. Instead of telling your friend about your problem ("how can I get to that platform over there?") you asked him how you can archive those things you are used to in other games. If you have asked the other question, your friend might have been able to tell you that you could use your portal gun to create a red and blue portal and walk through.

It is really frustrating to try to port a well-written Ruby / Java / etc. program to Go. One thing that works well in one language, might not work that well in another. You haven't even asked us, what problem you are trying to solve. You have just posted some useless boilerplate code that shows some class hierarchies. You won't need that in Go because Go's interfaces are more flexible. (A similar analogy could be drawn between Javascript's prototyping and people who try to program in a OOP-way in Javascript).

At the beginning, it's hard to came up with good designs in Go, especially if you are used to OOP. But the solutions in Go are usually smaller, more flexibly and much easier to understand. Take a close look at all those packages in the Go standard library and other external packages. For example, I think that leveldb-go is much easier and more straight-forward to understand than leveldb, even if you know both languages well.

答案4

得分: 1

也许可以尝试这样解决你的问题:

  • 创建一个函数,接受一个最小的接口来描述一个页面,并输出HTML。
  • 通过考虑“具有”关系来对所有页面进行建模。例如,你可以有一个Title结构体,将其嵌入到某个页面中,并在其上定义一个GetTitle()方法,然后进行类型断言和检查(并非所有内容都会有标题,所以你不希望它成为函数接受的接口的“必需”功能的一部分)。与其考虑页面的层次结构,不如考虑页面如何组合在一起。

我通常发现有一个有用的方法是将接口视为捕捉功能,将结构体视为捕捉数据。

英文:

Maybe try to solve your problem like this:

  • Create a function that accepts a minimal interface to describe a page and outputs html.
  • Model all of your pages by thinking about "has a" relationships. For example, you might have a Title struct that you embed in some page with a GetTitle() method on it that you type assert and check for (not everything would have a title so you don't want that to be part of the "required" functionality in the interface the function accepts.) Rather than think about the hierarchy of pages, think about how the pages are composed together.

One thing I usually find helpful is to think of interfaces as capturing functionality and structs as capturing data.

答案5

得分: 0

我似乎找到了一个可行的解决方案,至少对于我当前的任务来说是这样。在阅读了这里的所有建议并与一个朋友交谈之后(他不懂Go语言,但有其他经验,尝试在没有类型继承语言支持的情况下建模明显的层次关系),他说:“我问自己‘还有什么?是的,它是一个层次结构,但还有什么,我如何建模它?’”,我坐下来重新编写了我的需求:

我想要一个带有客户端接口的库,流程大致如下:

  1. 实例化一个页面创建对象,可能指定它将生成的格式。例如:

    p := NewHtml5Page()

  2. 可选地设置属性并添加内容。例如:

    p.Title = "FAQ"
    p.AddScript("default.css")
    p.Add("

    FAQ

    \n")

  3. 生成页面。例如:

    p.String()

  4. 而棘手的部分是:使其可扩展,以便名为Egg Sample的网站可以轻松利用该库基于现有格式创建新格式,这些格式本身可以成为进一步子格式的基础。例如:

    p := NewEggSamplePage()
    p2 := NewEggSampleForumPage()

在考虑如何在Go中建模这一点时,我决定客户端实际上不需要类型层次结构:它们从不需要将EggSampleForumPage视为EggSamplePage或将EggSamplePage视为Html5Page。相反,它似乎归结为希望我的“子类”在页面的某些位置添加内容或偶尔具有与其“超类”不同的内容。因此,这不是一个行为问题,而是一个数据问题。

那时,我恍然大悟:Go语言没有方法的动态分派,但是如果“子类型”(嵌入“超类型”的类型)更改了数据字段,则“超类型”的方法会看到该更改。(这是我在问题中展示的非常不像Go的尝试中使用函数指针而不是方法的方式。)以下是我最终得到的一部分摘录,展示了新的设计:

type Page struct {
    preContent  string
    content     bytes.Buffer
    postContent string
}

type HtmlPage struct {
    Page

    Title      string
    Encoding   string
    HeadExtras string

    // Exported, but meant as "protected" fields, to be optionally modified by
    // "subclasses" outside of this package
    DocTop     string
    HeadTop    string
    HeadBottom string
    BodyTop    string
    BodyAttrs  string
    BodyBottom string
    DocBottom  string

    styles  []string
    scripts []string
}

type Html5Page struct {
    *HtmlPage
}

type XhtmlPage struct {
    *HtmlPage

    Doctype string
}

func (p *Page) String() string {
    return p.preContent + p.content.String() + p.postContent
}

func (p *HtmlPage) String() string {
    p.preContent = p.DocTop + p.HeadTop +
        p.titleStr() + p.stylesStr() + p.scriptsStr() + p.contentTypeStr() +
        p.HeadExtras + p.HeadBottom + p.BodyTop
    p.postContent = p.BodyBottom + p.DocBottom

    return p.Page.String()
}

func NewHtmlPage() *HtmlPage {
    p := new(HtmlPage)

    p.DocTop     = "<html>\n"
    p.HeadTop    = "  <head>\n"
    p.HeadBottom = "  </head>\n"
    p.BodyTop    = "<body>\n"
    p.BodyBottom = "</body>\n"
    p.DocBottom  = "</html>\n"

    p.Encoding = "utf-8"

    return p
}

func NewHtml5Page() *Html5Page {
    p := new(Html5Page)

    p.HtmlPage = NewHtmlPage()

    p.DocTop = "<!DOCTYPE html>\n<html>\n"

    return p
}

虽然它可能需要一些整理,但一旦我有了这个想法,编写起来非常容易,它完美地工作(据我所知),它不会让我感到不舒服或感觉像是在与语言结构作斗争,而且我甚至可以像我想要的那样实现fmt.Stringer。我成功地使用我想要的接口生成了HTML5和XHTML页面,以及从客户端代码“子类化”了Html5Page并使用了新类型。

我认为这是一个成功,即使它并没有提供一个明确和普遍的答案来建模Go中的层次结构的问题。

英文:

I seem to have come up with a workable solution, at least for my current task. After reading all of the advice here and talking to a friend (who doesn't know Go, but has other experience trying to model apparently hierarchical relationships without language support for type inheritance) who said "I ask myself 'What else is it? Yes, it's a hierarchy, but what else is it, and how can I model that?'", I sat down and rewrote my requirements:

I want a library with a client interface with a flow something like this:

  1. Instantiate a page creation object, likely specifiying the format it will generate. E.g.:

    p := NewHtml5Page()

  2. Optionally set properties and add content. E.g.:

    p.Title = "FAQ"
    p.AddScript("default.css")
    p.Add("<h1>FAQ</h1>\n")

  3. Generate the page. E.g.:

    p.String()

  4. And the tricky part: Make it extensible, such that a website named Egg Sample could easily leverage the library to make new formats based on existing ones, which themselves can form the basis of further sub-formats. E.g.:

    p := NewEggSamplePage()
    p2 := NewEggSampleForumPage()

Thinking about how to model that in Go, I decided that the clients really don't need a type hierarchy: they never need to treat an EggSampleForumPage as an EggSamplePage or an EggSamplePage as an Html5Page. Rather, it seemed to boil down to wanting my "subclasses" to each have certain points in the page where they add content or occasionally have different content from their "superclass". So it's not a question of behavior, but one of data.

That's when something clicked for me: Go doesn't have dynamic dispatch of methods, but if a "subtype" (a type that embeds a "supertype") changes a data field, methods on the "supertype" do see that change. (This is what I was working with in the very un-Go-like attempt shown in my question, using function pointers rather than methods.) Here's an excerpt of what I ended up with, demonstrating the new design:

type Page struct {
	preContent  string
	content     bytes.Buffer
	postContent string
}

type HtmlPage struct {
	Page

	Title      string
	Encoding   string
	HeadExtras string

    // Exported, but meant as &quot;protected&quot; fields, to be optionally modified by
    //  &quot;subclasses&quot; outside of this package
	DocTop     string
	HeadTop    string
	HeadBottom string
	BodyTop    string
	BodyAttrs  string
	BodyBottom string
	DocBottom  string

	styles  []string
	scripts []string
}

type Html5Page struct {
	*HtmlPage
}

type XhtmlPage struct {
	*HtmlPage

	Doctype string
}

func (p *Page) String() string {
	return p.preContent + p.content.String() + p.postContent
}

func (p *HtmlPage) String() string {
	p.preContent = p.DocTop + p.HeadTop +
		p.titleStr() + p.stylesStr() + p.scriptsStr() + p.contentTypeStr() +
		p.HeadExtras + p.HeadBottom + p.BodyTop
	p.postContent = p.BodyBottom + p.DocBottom

	return p.Page.String()
}

func NewHtmlPage() *HtmlPage {
	p := new(HtmlPage)

	p.DocTop     = &quot;&lt;html&gt;\n&quot;
	p.HeadTop    = &quot;  &lt;head&gt;\n&quot;
	p.HeadBottom = &quot;  &lt;/head&gt;\n&quot;
	p.BodyTop    = &quot;&lt;body&gt;\n&quot;
	p.BodyBottom = &quot;&lt;/body&gt;\n&quot;
	p.DocBottom  = &quot;&lt;/html&gt;\n&quot;

	p.Encoding = &quot;utf-8&quot;

	return p
}

func NewHtml5Page() *Html5Page {
	p := new(Html5Page)

	p.HtmlPage = NewHtmlPage()

	p.DocTop = &quot;&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n&quot;

	return p
}

While it could perhaps use some cleaning up, it was extremely easy to write once I had the idea, it works perfectly (as far as I can tell), it doesn't make me cringe or feel like I'm fighting the language constructs, and I even get to implement fmt.Stringer like I wanted to. I've successfully generated both HTML5 and XHTML pages with my desired interface, as well as "subclassed" Html5Page from client code and used the new type.

I consider this a success, even if it doesn't provide a clear and universal answer to the question of modeling hierarchies in Go.

huangapple
  • 本文由 发表于 2012年7月13日 16:49:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/11467112.html
匿名

发表评论

匿名网友

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

确定