Моделирование иерархии связанных вещей без языковой поддержки для иерархий типов

Я новичок в 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) // Output a valid HTML 5 page corresponding to the above

Для более крупных проектов, например, для веб-сайта под названием "Образец яйца", я подклассирую один из существующих типов страниц, создавая более глубокую иерархию:

                                 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 (особенно части, которые казались соответствующими)
  • Прочитайте большую часть Effective Go (особенно части, которые казались актуальными)
  • Поиск в Google с множеством комбинаций поисковых терминов
  • Читайте различные посты о голангах
  • Написано много неадекватного кода 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 используют в подобных ситуациях.

  • Я хочу быть СУХОЙ ("Не повторяй себя"), насколько это разумно; Я не хочу отдельного text/template для каждого типа страницы, когда большая часть каждого шаблона идентична другим. Одна из моих отвергнутых реализаций работает таким образом, но кажется, что она станет неуправляемой, как только я получу более сложную иерархию типов страниц, как описано выше.

  • Я хотел бы иметь возможность иметь основной пакет библиотеки, который можно использовать как есть для типов, которые он поддерживает (например, html5Page а также xhtmlPage), и его можно расширять, как описано выше, не прибегая к копированию и редактированию библиотеки напрямую. (В классическом ОО я расширяю / подкласс 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() метод, который делает правильную вещь, вместо того, чтобы использовать функцию.

Я сильно подозреваю, что я делаю что-то не так и что существуют идиомы 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 для создания библиотеки, которая имеет что-то вроде клиентского интерфейса, который я описал.

5 ответов

Решение

Кажется, я нашел подходящее решение, по крайней мере, для моей текущей задачи. Прочитав все советы здесь и поговорив с другом (который не знает 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, я решил, что клиентам действительно не нужна иерархия типов: им никогда не нужно обрабатывать 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.

Наследование объединяет две концепции. Полиморфизм и совместное использование кода. Go разделяет эти понятия.

  • Полиморфизм ("это") в Go достигается с помощью интерфейсов.
  • Совместное использование кода в Go достигается за счет встраивания и функций, которые работают на интерфейсах

Многие люди из языков ООП забывают о функциях и теряются, используя только методы.

Поскольку Go разделяет эти понятия, вы должны думать о них индивидуально. Какая связь между "Page" и "Egg Sample Page". Это отношение "является" или это отношение обмена кодом?

Во-первых, предупреждение: глубокие иерархии болезненны для адаптации на всех языках. Глубокое иерархическое структурное моделирование часто является ловушкой: сначала оно приносит интеллектуальное удовлетворение, но заканчивается кошмаром.

Затем 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 из QueryRow
  • если необходимо (здесь нет) добавьте поля в мой подкласс и используйте их

И я мог бы вставлять более одного типа. Или "подтип" мой 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
}

Поскольку вы не можете положиться на String() метод, определенный для реализации pageOpenerCloser просто позвонить closeBody метод, определенный в той же реализации, вы должны использовать функции, а не методы для выполнения части работы, что я вижу как композицию: вы должны передать свой экземпляр pageOpenerCloser составные функции, которые будут вызывать правильные реализации.

Это означает

  • Вам рекомендуется иметь атомарные и ортогональные определения интерфейса
  • интерфейсы определяются своими (простыми) действиями
  • интерфейсные функции не должны вызывать другие функции в том же экземпляре
  • вам не рекомендуется иметь глубокую иерархию с промежуточными уровнями, определяемыми реализацией / алгоритмами
  • большие "составляющие" алгоритмы не должны быть переопределены или вообще написаны более одного раза

Я чувствую, что это уменьшает беспорядок и помогает сделать программу Go маленькой и понятной.

Я думаю, что невозможно ответить на ваш вопрос удовлетворительно. Но давайте начнем с короткой аналогии с другого контекста, чтобы избежать любых общепринятых идей о программировании (например, многие программисты считают, что ООП - это "правильный путь" для программирования, потому что это то, что они делали годами).

Предположим, вы играете в классическую игру под названием Bridge Builder. Цель этой игры - построить мост на столбах, чтобы поезд мог переходить с одной стороны на другую. Однажды, после долгих лет освоения игры, вы решаете попробовать что-то новое. Допустим, Портал 2:)

Вы легко управляете первым уровнем, но не можете понять, как попасть на платформу с другой стороны на втором уровне. Итак, вы спрашиваете друга: "Эй, как я могу разместить колонны в Portal 2"? Ваш друг может выглядеть смущенным, но он может сказать вам, что вы можете взять эти коробки и поставить их друг на друга. Таким образом, вы немедленно начинаете собирать все коробки, которые можете найти, чтобы построить свой мост на другой стороне комнаты. Отлично сработано!

Как бы то ни было, через пару часов вы обнаружите, что Portal 2 действительно разочаровывает (собирать блоки нужно целую вечность, а уровни действительно сложны). Так что перестань играть.

Итак, что здесь пошло не так? Во-первых, вы предполагали, что одна техника из одной игры может хорошо работать в другой. Во-вторых, вы не задали правильный вопрос. Вместо того, чтобы рассказывать другу о своей проблеме ("как я могу попасть на эту платформу?"), Вы спросили его, как можно архивировать те вещи, к которым вы привыкли в других играх. Если вы задали другой вопрос, ваш друг мог бы сказать вам, что вы можете использовать свое оружие портала, чтобы создать красно-синий портал и пройти через него.

Очень неприятно пытаться портировать хорошо написанную Ruby / Java / и т. Д. Программу на Go. Одна вещь, которая хорошо работает на одном языке, может не так хорошо работать на другом. Вы даже не спросили нас, какую проблему вы пытаетесь решить. Вы только что опубликовали некоторый бесполезный шаблонный код, который показывает некоторые иерархии классов. Вам не понадобится это в Go, потому что интерфейсы Go более гибкие. (Аналогичная аналогия может быть проведена между прототипированием Javascript и людьми, которые пытаются программировать ООП в Javascript).

В начале трудно придумать хороший дизайн в Go, особенно если вы привыкли к ООП. Но решения в Go обычно меньше, они более гибки и намного проще для понимания. Внимательно рассмотрите все эти пакеты в стандартной библиотеке Go и других внешних пакетах. Например, я думаю, что leveldb-go гораздо проще и понятнее, чем leveldb, даже если вы хорошо знаете оба языка.

Может быть, попытаться решить вашу проблему следующим образом:

  • Создайте функцию, которая принимает минимальный интерфейс для описания страницы и выводит html.
  • Смоделируйте все свои страницы, думая об отношениях "имеет". Например, у вас может быть структура Title, которую вы встраиваете на какую-то страницу с помощью метода GetTitle(), который вы вводите assert и проверяете (не у всех будет заголовок, поэтому вы не хотите, чтобы он был частью " требуемая "функциональность в интерфейсе, который принимает функция.) Вместо того, чтобы думать об иерархии страниц, подумайте о том, как страницы составлены вместе.

Одна вещь, которую я обычно нахожу полезной, это думать об интерфейсах как о захвате функциональности, а о структуре - как о захвате данных.

Другие вопросы по тегам