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

5

5

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:

                                      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):

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

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

                                 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:

    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.

Bacchic answered 13/7, 2012 at 8:49 Comment(2)
my opinion, not an answer : 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. I'd suggest composition of embedding in such a case.Teddi
@dystroy Thanks. I guess I should read up on "composition over inheritance", something I've heard a lot recently but not understood. Since I don't have inheritance available in Go, and I'm dealing with something that is rigidly an "is a" relationship in my mind, I'm having trouble figuring out how to use "has a" to the same effect.Bacchic
B
0

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 "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
}

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.

Bacchic answered 14/7, 2012 at 11:38 Comment(0)
C
5

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?

Cataplasia answered 13/7, 2012 at 14:21 Comment(1)
Thanks, this helps clarify my thinking a bit. I think trying to make everything a method and get code sharing through inheritance is definitely part of where I've been having difficulty. I've updated my question to include my code; if you have any insights, they'd be much appreciated.Bacchic
A
3

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("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)

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.

Anabiosis answered 13/7, 2012 at 9:47 Comment(4)
Thanks for this. I actually feel like I understand the basics of embedding already; it's applying this to the full hierarchy outlined in my post that is giving me trouble. (Specifically regarding dispatching methods to the receiver I want, I think. I keep writing code thinking "is a" when Go doesn't work that way.) Now I'm regretting not posting my code and the specific problems I was having. I'll post some code tomorrow and hopefully that will make it more obvious where I'm going wrong. Upvoting because it's a good explanation of embedding, and my question should be clearer.Bacchic
If your problem is related to function overriding, there are chances you can solve it with interfaces.Teddi
I've now posted my code, where I do try to use interfaces properly but am still having some issues. Thanks again!Bacchic
I have no answer that will satisfy you : I don't think you can elegantly use methods calling methods as you usually do in java. I think you need to pass your interface implementations to a function (e.g. a composer) that will call the right interface methods. You can't do real OOP in Go. I tried to write my thoughts in the answer.Teddi
A
1

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.

Augur answered 13/7, 2012 at 12:9 Comment(2)
Thank you for your thoughtful response. Your analogy is a vivid and clear example of trying to carry one's old thinking into a new situation where it doesn't apply. That's precisely what I'm asking for help with: I want to learn the Go way of dealing with this situation rather than trying to force Go to work the Ruby way. Maybe what you're telling me is that I need to step back even further than I already have, but right now all I can see is a hierarchical relationship that I want to model, even if the code isn't hierarchical. I'll post my failed attempts after getting some sleep.Bacchic
I've updated my question with my current best code; hopefully this will make it clearer where I'm going wrong.Bacchic
S
1

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.

Suppression answered 13/7, 2012 at 15:28 Comment(1)
"Think of interfaces as capturing functionality and structs as capturing data." This seems like very good advice for those of us coming from classical OO languages -- thanks! If you have any advice based on the code I've just added to my question, I'd be grateful.Bacchic
B
0

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 "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
}

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.

Bacchic answered 14/7, 2012 at 11:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.