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

时间:2012-07-13 08:49:41

标签: oop inheritance go go-interface

我是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

对于较大的项目,例如对于名为&#34; Egg Sample&#34;的网站,我将其中一个现有的页面类型子类化,从而创建更深层次的层次结构:

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

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

但是,Go没有hierarchy of types的概念:没有类,而且no type inheritance。还有no dynamic dispatch of methods(我觉得从上面可以看出; Go类型HtmlPage不是&#34; Go类型Page

Go确实提供:

  • 嵌入
  • 接口

似乎这两个工具应该足以得到我想要的东西,但经过几次错误的开始,我感到难过和沮丧。我的猜测是,我认为这是错误的,我希望有人可以指出我如何做到这一点的正确方向&#34; Go way&#34;。

这是我遇到的一个特定的,真实的问题,因此,欢迎任何有关解决我的特定问题而不解决更广泛问题的建议。但是,我希望答案将以&#34;通过以这种方式组合结构,嵌入和接口的形式,你可以轻松地获得你想要的行为&#34;而不是回避这一点的东西。我认为许多Go从古典OO语言过渡的新人可能会经历类似的混乱时期。

通常情况下,我会在这里显示我的代码,但是我有几个版本,每个版本都有自己的问题,而且我不能想象包含它们实际上会为我的问题添加任何清晰度,这已经是变得很长。如果事实证明看起来很有用,我当然会添加代码。

我已经完成的事情:

要更清楚地了解我正在寻找的内容:

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

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

    这让我很接近,但并非一路走来。我现在的观点是,似乎没有机会学习Go程序员在这种情况下使用的习语。

  • 我想成为干(&#34;不要重复自己&#34;),这是合理的;当每个模板的大部分与其他模板完全相同时,我不希望为每种类型的页面单独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. 这可能是不可能的,但理想情况下,我希望所有页面类型都有String()方法做正确的事情,而不是必须使用函数。
  4. 我强烈怀疑我做错了什么,而且有成语可以让这更好。

    喜欢使用String()方法做正确的事,但

    func (p *page) String( string {
        return p.headerString() + p.contentStr() + p.footerString()
    }
    
    即使在通过page使用时,

    仍会始终使用HtmlPage方法,原因是缺少动态调度,只能使用接口。

    使用我当前基于界面的页面生成,我不仅要做fmt.Println(p)(其中p是某种页面),而且我必须在{{1}之间进行选择。 }和fmt.Println(dsts.PageString(p))。这感觉非常错误。

    我在fmt.Println(dsts.HtmlPageString(p)) / PageString()HtmlPageString() / headerString()之间笨拙地复制代码。

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

5 个答案:

答案 0 :(得分:4)

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

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

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

因为Go将这些概念分开,所以你必须单独考虑它们。 'Page'和'Egg Sample Page'之间的关系是什么?它是一种“是一种”关系,还是一种代码共享关系?

答案 1 :(得分: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上定义的功能,例如Close的{​​{1}}
  • 如果需要(此处不存在)将字段添加到我的子类并使用它们

我可以嵌入多种类型。或“子类型”我的QueryRow类型。

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

我说这是妥协,因为很明显Go不是OOP语言。正如您所看到的那样,缺少函数覆盖会阻止在“超类”中使用方法组成“子类”方法调用的通常解决方案。

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

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

ConnexionMysql

由于您不能依赖在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实例传递给一个能够调用正确实现的Composing函数。

这意味着

  • 鼓励您使用原子和正交接口定义
  • 接口由其(简单)操作
  • 定义
  • 接口函数不应该调用同一实例上的其他函数
  • 不鼓励您使用由implementation / algorithms
  • 定义的中间级别的深层次结构
  • 大“组合”算法不应该被覆盖或一般不止一次写入

我觉得这可以减少混乱,并有助于使Go程序变得小巧且易于理解。

答案 2 :(得分:1)

我认为不可能满意地回答你的问题。但是,让我们从一个不同的上下文中进行简短的类比,以避免任何普遍接受的关于编程的想法(例如,许多程序员认为OOP是编程的“正确方法”,因为这是他们多年来所做的事情)。

假设您正在玩一款名为Bridge Builder的经典游戏。这场比赛的目标是在柱子顶部建造一座桥梁,以便火车可以从一侧传递到另一侧。有一天,在掌握游戏多年后,你决定要尝试新的东西。让我们说门户2:)

您可以轻松管理第一级,但无法弄清楚如何在第二级找到另一侧的平台。所以你问一个朋友:“嘿,我怎么能把支柱放在Portal 2中”?你的朋友可能看起来很混乱,但他可能会告诉你,你可以拿起那些盒子并将它们放在彼此之上。因此,您立即开始收集您可以找到的所有箱子,以便将您的桥梁建造到房间的另一侧。做得好!

无论如何,经过几个小时,你发现Portal 2真的很令人沮丧(收集块需要很长时间,而且关卡真的很难)。所以你停止了比赛。

那么,这里出了什么问题?首先,你假设一种游戏中的一种技术可能在另一种游戏中运行良好。其次,你没有问过正确的问题。而不是告诉你的朋友你的问题(“我怎么能在那里进入那个平台?”)你问他如何归档你在其他游戏中习惯的东西。如果你问过另一个问题,你的朋友可能会告诉你,你可以使用你的门户枪创建一个红色和蓝色的门户网站并走过去。

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

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

答案 3 :(得分:1)

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

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

我通常认为有用的一点是将接口视为捕获功能和结构捕获数据。

答案 4 :(得分:0)

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

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

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

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

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

    p.String()
    
  4. 棘手的部分:让它可扩展,这样一个名为Egg Sample的网站可以轻松利用这个库来制作基于现有格式的新格式,这些格式本身可以构成更多子格式的基础。 E.g:

    p  := NewEggSamplePage()
    p2 := NewEggSampleForumPage()
    
  5. 考虑如何在Go中对其进行建模,我认为客户真的不需要类型层次结构:他们永远不需要将EggSampleForumPage视为EggSamplePage或{{1} } EggSamplePage。相反,似乎可以归结为希望我的“子类”在页面中具有某些点,在这些点中它们添加内容或偶尔具有与其“超类”不同的内容。所以这不是行为问题,而是数据之一。

    当我点击某些内容时:Go没有动态调度方法,但如果“子类型”(嵌入“超类型”的类型)更改数据字段,则方法开启“超级型”确实看到了这种变化。 (这是我在使用函数指针而不是方法时在我的问题中显示的非类似Go的尝试中使用的。)以下是我最终得到的内容的摘录,展示了新的设计:

    Html5Page

    虽然它可能会使用一些清理,但是一旦我有了这个想法就很容易写,它完美地工作(据我所知),它不会让我感到畏缩或觉得我在战斗语言结构,我甚至可以像我想的那样实现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 } 。我已成功生成了带有所需界面的HTML5和XHTML页面,以及客户端代码中的“子类”fmt.Stringer并使用了新类型。

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