在没有语言支持类型层次结构的情况下对相关事物的层次结构进行建模

Dar*_*tle 5 oop inheritance go go-interface

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

                                      Page
                                   /        \
                          HTML Page          Wiki Page
                         /         \
              HTML 5 Page           XHTML Page
Run Code Online (Sandbox Code Playgroud)

对于一个小项目,我可能会做这样的事情(翻译成我现在想要的 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
Run Code Online (Sandbox Code Playgroud)

对于较大的项目,比如一个名为“Egg Sample”的网站,我将现有的一种页面类型子类化,创建更深层次的层次结构:

                                 HTML 5 Page
                                      |
                               Egg Sample Page
                             /        |        \
               ES Store Page    ES Blog Page     ES Forum Page
Run Code Online (Sandbox Code Playgroud)

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

然而,Go 没有类型层次结构的概念:没有类,也没有类型继承。也没有方法的动态分派(在我看来,这是从上面得出的;Go 类型HtmlPage不是“某种”Go 类型Page)。

Go 确实提供:

  • 嵌入
  • 接口

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

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

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

我做过的事情:

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

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

    type page struct {
        Title     string
        content   bytes.Buffer
        openPage  func() string
        closePage func() string
        openBody  func() string
        closeBody func() string
    }
    
    Run Code Online (Sandbox Code Playgroud)

    这让我很接近,但并非完全如此。我现在的观点是,学习 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()
}
Run Code Online (Sandbox Code Playgroud)

这有效,但它有几个问题:

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

我强烈怀疑我做错了什么,并且有 Go 成语可以使这更好。

有一个String()是做正确的事的方法,但

func (p *page) String( string {
    return p.headerString() + p.contentStr() + p.footerString()
}
Run Code Online (Sandbox Code Playgroud)

page即使通过 an使用,也将始终使用这些方法HtmlPage,因为除了接口之外,在任何地方都缺乏动态调度。

使用我当前的基于界面的页面生成,我不仅不能只做fmt.Println(p)p某种页面在哪里),而且我必须在fmt.Println(dsts.PageString(p))和之间专门选择fmt.Println(dsts.HtmlPageString(p))。那感觉很不对劲。

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

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

Jes*_*sta 5

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

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

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

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


Dar*_*tle 0

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

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

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

    p := NewHtml5Page()
    
    Run Code Online (Sandbox Code Playgroud)
  2. (可选)设置属性并添加内容。例如:

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

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

    p  := NewEggSamplePage()
    p2 := NewEggSampleForumPage()
    
    Run Code Online (Sandbox Code Playgroud)

考虑如何在 Go 中对其进行建模,我认为客户端实际上不需要类型层次结构:他们永远不需要将 an 视为EggSampleForumPageanEggSamplePageEggSamplePagean 作为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
}
Run Code Online (Sandbox Code Playgroud)

虽然它可能需要一些清理工作,但一旦我有了这个想法,它就非常容易编写,它工作完美(据我所知),它不会让我感到畏缩或感觉我正在与语言结构作斗争,我什至可以按照fmt.Stringer我想要的方式实施。我已经成功生成了具有所需界面的 HTML5 和 XHTML 页面,以及Html5Page从客户端代码“子类化”并使用了新类型。

我认为这是成功的,即使它没有为 Go 中的层次结构建模问题提供清晰且通用的答案。