5

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

                                      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) // Output a valid HTML 5 page corresponding to the above

对于较大的项目,例如一个名为“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 确实提供:

  • 嵌入
  • 接口

似乎这两个工具应该足以得到我想要的东西,但是在几次错误的开始之后,我感到很难过和沮丧。我的猜测是我想错了,我希望有人可以为我指出正确的方向,以了解如何做到这一点。

这是我遇到的一个具体的、真实的问题,因此,欢迎任何关于在不解决更广泛问题的情况下解决我的特定问题的建议。但我希望答案的形式是“通过以某种方式组合结构、嵌入和接口,您可以轻松获得所需的行为”,而不是回避它。我认为许多从经典 OO 语言过渡到 Go 的新手可能会经历类似的困惑时期。

通常我会在这里展示我损坏的代码,但我有几个版本,每个版本都有自己的问题,我不认为包括它们实际上会增加我的问题的清晰度,这个问题已经很长了。如果结果看起来有用,我当然会添加代码。

我做过的事情:

要更明确地说明我在寻找什么:

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

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

    这让我很接近,但不是一直。我现在的观点是,学习 Go 程序员在这种情况下使用的习语似乎是一个失败的机会。

  • 我想尽可能地干(“不要重复自己”);text/template当每个模板的大部分内容与其他模板相同时,我不希望每种类型的页面都有单独的页面。我废弃的一个实现就是这样工作的,但是一旦我得到一个更复杂的页面类型层次结构,它似乎就会变得难以管理,如上所述。

  • 我希望能够拥有一个核心库包,它可以按原样用于它支持的类型(例如html5PagexhtmlPage),并且可以如上所述进行扩展,而无需直接复制和编辑库。(例如,在经典的 OO 中,我扩展/子类化 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.con    tentTypeStr() +
        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某种页面在哪里),而且我必须专门在 和 之间进行fmt.Println(dsts.PageString(p))选择fmt.Println(dsts.HtmlPageString(p))。这感觉很不对劲。

而且我在PageString()/HtmlPageString()之间和headerString()/之间笨拙地复制代码htmlHeaderString()

所以我觉得我仍然在遭受设计问题,因为在某种程度上仍然在思考 Ruby 或 Java 而不是 Go。我希望有一种直接且惯用的 Go 方法来构建一个库,该库具有我所描述的客户端界面。

4

5 回答 5

5

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

  • Go 中的多态('is a')是通过使用接口来实现的。
  • Go 中的代码共享是通过嵌入和作用于接口的函数来实现的

许多来自 OOP 语言的人忘记了函数,只使用方法就迷失了方向。

因为 Go 将这些概念分开,所以您必须单独考虑它们。“页面”和“鸡蛋样本页面”之间有什么关系。是“是”关系还是代码共享关系?

于 2012-07-13T14:21:23.097 回答
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 不是 OOP 语言。如您所见,缺少函数覆盖阻止了通常的解决方案,即在“超类”中使用方法来组成“子类”方法的调用。

我知道这可能会令人不安,但我不确定我是否真的错过了通常基于层次结构的解决方案的重要性和冗长性。正如我所说,起初很好,但当它变得复杂时会很痛苦。这就是为什么我建议你尝试 Go 方式:

Go 接口可用于减少对继承的需求。实际上,您的页面可能是一个接口(或者更惯用的几个接口)和一个 struct :

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

type page struct {
    Title     string
    content   bytes.Buffer
}

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

这表示

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

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

于 2012-07-13T09:47:41.567 回答
1

我认为不可能令人满意地回答您的问题。但是,让我们从一个不同背景的简短类比开始,以避免任何普遍接受的关于编程的想法(例如,许多程序员认为 OOP 是编程的“正确方法”,因为这是他们多年来一直在做的事情)。

假设您正在玩一款名为 Bridge Builder 的经典游戏。这个游戏的目标是在柱子上建造一座桥,这样火车就可以从一侧通行到另一侧。有一天,在掌握了这款游戏多年后,您决定尝试一些新的东西。假设传送门 2 :)

您可以轻松管理第一级,但您无法弄清楚如何在第二级到达另一侧的平台。所以你问朋友:“嘿,如何在传送门 2 中放置柱子”?您的朋友可能看起来很困惑,但他可能会告诉您,您可以拿起这些盒子并将它们叠放在一起。因此,您立即开始收集所有可以找到的盒子,以建立通往房间另一侧的桥梁。做得好!

无论如何,几个小时后,你会发现 Portal 2 真的很令人沮丧(收集积木需要很长时间,而且关卡真的很困难)。所以你别玩了。

那么,这里出了什么问题?首先,您假设一种游戏中的一种技术可能在另一种游戏中效果很好。其次,你没有问对问题。而不是告诉你的朋友你的问题(“我怎样才能到达那里的那个平台?”)你问他如何存档你在其他游戏中习惯的那些东西。如果您问了其他问题,您的朋友可能会告诉您,您可以使用传送枪创建一个红色和蓝色的传送门并穿过。

尝试将编写良好的 Ruby/Java/etc. 程序移植到 Go 中确实令人沮丧。一件事在一种语言中效果很好,在另一种语言中可能效果不佳。你甚至没有问我们,你要解决什么问题。您刚刚发布了一些显示一些类层次结构的无用样板代码。在 Go 中你不需要它,因为 Go 的接口更灵活。(可以在 Javascript 的原型设计和尝试用 Javascript 以 OOP 方式编程的人之间进行类似的类比)。

一开始,很难在 Go 中想出好的设计,尤其是如果你习惯了 OOP。但 Go 中的解决方案通常更小、更灵活且更易于理解。仔细查看 Go 标准库和其他外部包中的所有这些包。例如,我认为 leveldb-go 比 leveldb 更容易理解,更直接,即使你对这两种语言都很熟悉。

于 2012-07-13T12:09:32.283 回答
1

也许尝试像这样解决您的问题:

  • 创建一个接受最小接口来描述页面并输出 html 的函数。
  • 通过考虑“具有”关系对所有页面进行建模。例如,您可能有一个 Title 结构,它嵌入在某个页面中,上面有一个 GetTitle() 方法,您可以键入 assert 并检查它(并非所有内容都有标题,因此您不希望它成为“需要”函数接受的接口中的功能。)与其考虑页面的层次结构,不如考虑页面是如何组合在一起的。

我通常觉得有帮助的一件事是将接口视为捕获功能,将结构视为捕获数据。

于 2012-07-13T15:28:51.997 回答
0

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

我想要一个带有客户端界面的库,其流程如下:

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

    p := NewHtml5Page()
    
  2. 可以选择设置属性并添加内容。例如:

    p.Title = "FAQ"
    p.AddScript("default.css")
    p.Add("<h1>FAQ</h1>\n")
    
  3. 生成页面。例如:

    p.String()
    
  4. 棘手的部分是:使其可扩展,这样一个名为 Egg Sample 的网站就可以轻松地利用该库在现有格式的基础上制作新格式,这些格式本身可以构成进一步子格式的基础。例如:

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

考虑到如何在 Go 中对其进行建模,我决定客户端真的不需要类型层次结构:他们永远不需要将 anEggSampleForumPage视为 anEggSamplePage或 an EggSamplePageas an 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 中的层次结构建模问题提供明确和普遍的答案。

于 2012-07-14T11:38:01.470 回答