Использование наследования строителей в GO

Мне нужно создать конструктор (базовый) и конкретные сборщики для каждого типа сборки.

e.g.

builder for html project
builder for node.js project
builder for python project
builder for java project

....

Основная функциональность будет выглядеть следующим образом:

Файл: Builder.go

интерфейс

type Builder interface {
    Build(string) error
}

Файл: nodebuilder.go

//This is the struct ???? not sure what to put here...
type Node struct {


}


func (n Node) Build(path string) error {

//e.g. Run npm install which build nodejs projects

        command := exec.Command("npm", "install")
        command.Dir = "../path2dir/"

        Combined, err := command.CombinedOutput()

        if err != nil {
            log.Println(err)
        }
        log.Printf("%s", Combined)
    }

    ...
    //return new(error)
}

Основные предположения/процесс:

  • Чтобы начать сборку на каждом модуле, мне нужно получить путь к ней.
  • Мне нужно скопировать модуль во временную папку
  • Мне нужно запустить сборку на нем (реализовать интерфейс сборки, например mvn build npm install и т.д.)
  • После завершения сборки zip модуль с помощью dep
  • Скопируйте его в новую целевую папку

Примечание: рядом с build и path (которые должны быть обработаны специально) все остальные функции идентичны
как zip copy

  • Где я должен поместить zip and copy (в структуре) и, например, каким образом я должен реализовать их и связать их с конструктором?

  • Должен ли я структурировать проект по-разному в соответствии с предположениями?

Ответ 1

Первый принцип SOLID говорит, что часть кода должна иметь только одну ответственность.

Принимает контекст, действительно нет смысла, чтобы любой builder заботился о части copy и zip процесса сборки. Это выше builder ответственности. Даже использование композиции (встраивание) недостаточно аккуратно.

Упомянув, основная ответственность builder заключается в том, чтобы создать код, как следует из названия. Но более конкретно, ответственность builder заключается в том, чтобы построить код по пути. Какой путь? Самый идеальный путь - это текущий путь, рабочий каталог. Это добавляет к интерфейсу два боковых метода: Path() string, который возвращает текущий путь и ChangePath(newPath string) error, чтобы изменить текущий путь. Имплантация будет простой, сохраните одно строковое поле, поскольку текущий путь будет в основном выполнять задание. И его можно легко расширить до некоторого удаленного процесса.

Если мы посмотрим внимательно, там есть две концепции построения. Один из них - это процесс построения всего здания, начиная с создания temp dir, чтобы скопировать его, все пять шагов; другая - команда сборки , которая является третьим этапом процесса.

Это очень вдохновляет. Процесс должен быть представлен как функция, как классическая процедурная программирование. Поэтому мы пишем функцию Build. Он сериализует все 5 шагов, простых и простых.

код:

package main

import (
    "io/ioutil"
)

//A builder is what used to build the language. It should be able to change working dir.
type Builder interface {
    Build() error //Build builds the code at current dir. It returns an error if failed.
    Path() string //Path returns the current working dir.
    ChangePath(newPath string) error //ChangePath changes the working dir to newPath.
}

//TempDirFunc is what generates a new temp dir. Golang woould requires it in GOPATH, so make it changable.
type TempDirFunc func() string

var DefualtTempDirFunc = func() string {
    name,_ := ioutil.TempDir("","BUILD")
    return name
}

//Build builds a language. It copies the code to a temp dir generated by mkTempdir
//and call the Builder.ChangePath to change the working dir to the temp dir. After
//the copy, it use the Builder to build the code, and then zip it in the tempfile,
//copying the zip file to `toPath`.
func Build(b Builder, toPath string, mkTempDir TempDirFunc) error {

    if mkTempDir == nil {
        mkTempDir = DefaultTempDirFunc
    }

    path,newPath:=b.Path(),mkTempDir()
    defer removeDir(newPath) //clean-up

    if err:=copyDir(path,newPath); err!=nil {
        return err
    }
    if err:=b.ChangePath(newPath) !=nil {
        return err
    }

    if err:=b.Build(); err!=nil {
        return err
    }

    zipName,err:=zipDir(newPath) // I don't understand what is `dep`.
    if err!=nil { 
        return err
    }

    zipPath:=filepath.Join(newPath,zipName)
    if err:=copyFile(zipPath,toPath); err!=nil {
        return err
    }


    return nil
}

//zipDir zips the `path` dir and returns the name of zip. If an error occured, it returns an empty string and an error.
func zipDir(path string) (string,error) {}

//All other funcs is very trivial.

В комментариях есть много вещей, и я действительно рушаюсь, чтобы писать все те вещи copyDir/removeDir. Одна вещь, которая не упоминается в конструктивной части, - это mkTempDir func. Golang был бы недоволен, если код находится в /tmp/xxx/, поскольку он находится вне GOPATH, и это создаст больше проблем с изменением GOPATH, так как это приведет к поломке пути импорта, поэтому для golang потребуется уникальная функция для создания tempdir внутри GOPATH.

Edit:

О, еще одна вещь, которую я забыл сказать. Это ужасно уродливое и безответственное обращение с такими ошибками. Но идея есть, и более приличная обработка ошибок в основном требует содержимого использования. Так что измените его самостоятельно, запишите его, панику или что угодно.

Изменить 2:

Вы можете повторно использовать пример npm следующим образом.

type Node struct {
    path string
}

func (n Node) Build(path string) error {
    //e.g. Run npm install which build nodejs project
    command := exec.Command("npm", "install")
    command.Dir = n.path
    Combined, err := command.CombinedOutput()
    if err != nil {
        log.Println(err)
    }
    log.Printf("%s", Combined)
    return nil
}

func (n *Node) ChangePath(newPath string) error {
    n.path = newPath
}

func (n Node) Path() string {
    return n.path
}

И объединить его с другим языком вместе:

func main() {
    path := GetPathFromInput()
    switch GetLanguageName(path) {
    case "Java":
        Build(&Java{path},targetDirForJava(),nil)
    case "Go":
        Build(&Golang{path,cgoOptions},targetDirForGo(),GoPathTempDir()) //You can disable cgo compile or something like that.
    case "Node":
        Build(&Node{path},targetDirForNode(),nil)
    }
}

Один трюк - это имя языка. GetLanguageName должен возвращать имя языка, используемого в path. Это можно сделать, используя ioutil.ReadDir для обнаружения имен файлов.

Также обратите внимание, что хотя я сделал структуру Node очень простой и только сохраняет поле path, вы можете легко ее расширить. Как и в части Golang, вы можете добавить туда опции сборки.

Изменить 3:

О структуре пакета:

Прежде всего, я думаю буквально все: функция Build, языковые сборщики и другие утилиты/помощники должны быть помещены в один пакет. Все они работают для одной задачи: постройте язык. Нет необходимости и вряд ли ожидать какого-либо фрагмента кода в качестве другого (суб) пакета.

Таким образом, это означает, что один каталог. Остальные действительно очень личный стиль, но я поделюсь с вами:

Я бы поставил функцию Build и интерфейс builder в файл с именем main.go. Если код интерфейса минимальный и очень читаемый, я бы поместил их в main.go, но если он длинный и имеет некоторую ui-логику, я бы поставил его в front-end.go или cli.go или ui.go, в зависимости от кода.

Далее, для каждого языка я создам файл .go с кодом языка. Он дает понять, где я могу их проверить. В качестве альтернативы, если код действительно крошечный, неплохо было собрать все вместе в builders.go. В конце концов, главные редакторы могут быть более чем способны получить определенность структур и типов.

Наконец, все функции copyDir, zipDir переходят к a util.go. Это просто - они утилиты, в большинстве случаев мы просто не хотим их беспокоить.

Ответ 2

Переходите через каждый вопрос один за другим:

1. Где я должен поместить zip и скопировать (в структуру) и, например, как следует реализовать их и вывести их в конструктор?

Интерфейс не несет никаких данных (при условии, что вы хотите реализовать его из своего кода). Это просто проект, который объект может реализовать, чтобы пройти как более общий тип. В этом случае, если вы не проходите Builder в любом месте, интерфейс является избыточным.

2. Должен ли я структурировать проект по-разному в соответствии с предположениями?

Это мой проект. Я объясню каждую часть отдельно после кода:

package buildeasy

import (
        "os/exec"
)


// Builder represents an instance which carries information
// for building a project using command line interface.
type Builder struct {
        // Manager is a name of the package manager ("npm", "pip")
        Manager string
        Cmd     string
        Args    []string
        Prefn   func(string) error
        Postfn  func(string) error
}

func zipAndCopyTo(path string) error {
        // implement zipping and copy to the provided path
        return nil
}

var (
        // Each manager specific configurations
        // are stored as a Builder instance.
        // More fields and values can be added.
        // This technique goes hand-in-hand with
        // `wrapBuilder` function below, which is
        // a technique called "functional options"
        // which is considered a cleanest approach in
        // building API methods.
        // https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
        NodeConfig = &Builder{
                Manager: "npm",
                Postfn:  zipAndCopyTo,
        }
        PythonConfig = &Builder{
                Manager: "pip",
                Postfn:  zipAndCopyTo,
        }
)

// This enum is used by factory function Create to select the
// right config Builder from the array below.
type Manager int

const (
    Npm Manager = iota
    Pip
    // Yarn
    // ...
)

var configs = [...]*Builder{
    NodeConfig,
    PythonConfig,
    // YarnConfig, 
}

// wrapBuilder accepts an original Builder and a function that can
// accept a Builder and then assign relevant value to the first.
func wrapBuilder(original *Builder, wrapperfn func(*Builder)) error {
    if original != nil {
        wrapperfn(original)
        return nil
    }
    return errors.New("Original Builder is nil")
}

func New(manager Manager) *Builder {
    builder := new(Builder)
    // inject / modify properties of builder with relevant
    // value for the manager we want.
    wrapBuilder(builder, configs[int(manager)])
    })
    return builder
}

// Now you can have more specific methods like to install.
// notice that it doesn't matter what this Builder is for.
// All information is contained in it already.
func (b *Builder) Install(pkg string) ([]byte, error) {
    b.Cmd = "install"

    // if package is provided, push its name to the args list
    if pkg != "" {
        b.Args = append([]string{pkg}, b.Args...)
    }

    // This emits "npm install [pkg] [args...]"
    cmd := exec.Command(b.Manager, (append([]string{b.Cmd}, b.Args...))...)
    // default to executing in the current directory
    cmd.Dir = "./"

    combined, err := cmd.CombinedOutput()
    if err != nil {
        return nil, err
    }
    return combined, nil
}



func (b *Builder) Build(path string) error {
    // so default the path to a temp folder
    if path == "" {
        path = "path/to/my/temp"
    }

    // maybe prep the source directory?
    if err := b.Prefn(path); err != nil {
        return err
    }

    // Now you can use Install here
    output, err := b.Install("")
    if err != nil {
        return err
    }

    log.Printf("%s", output)

    // Now zip and copy to where you want
    if err := b.Postfn(path); err != nil {
        return err
    }

    return nil
}

Теперь этот Builder достаточно общий, чтобы обрабатывать большинство команд сборки. Обратите внимание на поля Prefn и Postfn. Это функции перехвата, которые вы можете запустить до и после выполнения команды в Build. Prefn может проверить, установлен ли, скажем, менеджер пакетов и установить его, если он нет (или просто вернуть ошибку). Postfn может выполнять ваши операции zip и copy или любую процедуру очистки. Здесь usecase, если superbuild является нашим вымышленным именем пакета, и пользователь использует его извне:

import "github.com/yourname/buildeasy"

func main() {

        myNodeBuilder := buildeasy.New(buildeasy.NPM)
        myPythonBuilder := buildeasy.New(buildeasy.PIP)

        // if you wanna install only
        myNodeBuilder.Install("gulp")

        // or build the whole thing including pre and post hooks
        myPythonBuilder.Build("my/temp/build")

        // or get creative with more convenient methods
        myNodeBuilder.GlobalInstall("gulp")
}

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

функция wrapBuilder

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

func TempNewBuilder(cmd string) *Builder {
        builder := new(Builder)
        builder.Cmd = cmd
        return builder
}

Но этот подход очень ad-hoc, потому что невозможно передать произвольные значения для настройки возвращаемого *Builder. Более надежный подход - передать экземпляр config *Builder:

func TempNewBuilder(configBuilder *Builder) *Builder {
     builder := new(Builder)
     builder.Manager = configBuilder.Manager
     builder.Cmd = configBuilder.Cmd
     // ...
     return builder    
}

Используя функцию wrapBuilder, вы можете написать функцию для обработки (повторного) присвоения значений экземпляра:

func TempNewBuilder(builder *Builder, configBuilderFn func(*Builder)) *Builder {
     configBuilderFn(builder)
}

Теперь вы можете передать любую функцию для configBuilderFn для настройки экземпляра *Builder.

Чтобы узнать больше, см. https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis.

configs array

configs массив идет рука об руку с перечислением констант Manager. Посмотрите на функцию New factory. константа enum Manager, переданная в проходе, имеет тип Manager, который находится под int. Это означает, что все, что нам нужно было сделать, это доступ configs с использованием Manager в качестве индекса в wrapBuilder:

wrapBuilder(builder, configs[int(manager)])

Например, если manager == Npm, configs[int(manager)] вернет массив NodeConfig из configs.

Структурирующий пакет (ы)

В этот момент хорошо иметь функции zip и copy, чтобы жить в том же пакете, что и Build, как я сделал. Там мало пользы в преждевременной оптимизации чего-либо или беспокоиться об этом, пока вам не придется. Это приведет только к большей сложности, чем вам хотелось бы. Оптимизация происходит последовательно по мере разработки кода.

Если вам кажется, что структурирование проекта рано важно, вы можете сделать это на основе семантики вашего API. Например, чтобы создать новый *Builder, пользователь может называть функцию factory New или Create из подпакета buildeasy/builder:

// This is a user using your `buildeasy` package

import (
        "github.com/yourname/buildeasy"
        "github.com/yourname/buildeasy/node"
        "github.com/yourname/buildeasy/python"
)

var targetDir = "path/to/temp"

func main() {
        myNodeBuilder := node.New()   
        myNodeBuilder.Build(targetDir)
        myPythonBuilder := python.New()
        myPythonBuilder.Install("tensorflow")   
}

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

myNodeBuilder := buildeasy.NewNodeBuilder()
myPythonBuilder := buildeasy.NewPipBuilder()

// or 
mySecondNodeBuilder := buildeasy.New(buildeasy.Yarn)

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

Возвращаясь к вашему проекту, я бы сохранил наиболее распространенные, более общие функции в каталоге/пакете верхнего уровня. Вот как я мог бы заняться структурой:

buildeasy
├── buildeasy.go
├── python
│   └── python.go
└── node/
    └── node.go

Хотя пакет buildeasy содержит такие функции, как NewNodeBuilder, NewPipBuilder или просто New, который принимает дополнительные параметры (например, приведенный выше код), например, в subpackage buildeasy/node может выглядеть следующим образом:

package node

import "github.com/yourname/buildeasy"

func New() *buildeasy.Builder {
        return buildeasy.New(buildeasy.Npm)
}

func NewWithYarn() *buildeasy.Builder {
        return buildeasy.New(buildeasy.Yarn)
}

// ...

или buildeasy/python:

package python

import "github.com/yourname/buildeasy"

func New() *buildeasy.Builder {
        return buildeasy.New(buildeasy.Pip)
}

func NewWithEasyInstall() *buildeasy.Builder {
        return buildeasy.New(buildeasy.EasyInstall)
}

// ...

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

Надеюсь, что это имеет смысл.

Ответ 3

Go не является объектно-ориентированным языком. Это означает, что по дизайну вы не обязательно должны иметь все поведение типа, инкапсулированного в самом типе. И это удобно, если вы думаете, что у нас нет наследования.

Если вы хотите создать тип для другого типа, вместо этого вы используете композицию: a struct может вставлять другие типы и выставлять свои методы.

Скажем, что у вас есть тип MyZipper, который предоставляет метод Zip(string) и MyCopier, который предоставляет метод Copy(string):

type Builder struct {
    MyZipper
    MyCopier
}

func (b Builder) Build(path string) error {

    // some code

    err := b.Zip(path)
    if err != nil {
        return err
    }

    err := b.Copy(path)
    if err != nil {
        return err
    }
}

Это композиция в Go. Кроме того, вы можете даже внедрять неопубликованные типы (например, MyZipper и MyCopier), если вы хотите, чтобы их вызывали из пакета builder. Но почему, зачем их внедрять в первую очередь?

Вы можете выбрать между несколькими различными действительными проектами для проекта Go.

решение 1: одиночный пакет, отображающий несколько типов Builder

В этом случае вам нужен пакет builder, который будет выставлять несколько сборщиков.

zip и copy - это две функции, определенные где-то в пакете, они не должны быть методами, привязанными к типу.

package builder

func zip(zip, args string) error {
    // zip implementation
}

func cp(copy, arguments string) error {
    // copy implementation
}

type NodeBuilder struct{}

func (n NodeBuilder) Build(path string) error {
    // node-specific code here

    if err := zip(the, args); err != nil {
        return err
    }

    if err := cp(the, args); err != nil {
        return err
    }

    return nil
}

type PythonBuilder struct{}

func (n PythonBuilder) Build(path string) error {
    // python-specific code here

    if err := zip(the, args); err != nil {
        return err
    }

    if err := cp(the, args); err != nil {
        return err
    }

    return nil
}

решение 2: одиночный пакет, однопотоковое вложение определенного поведения

В зависимости от сложности конкретного поведения вы можете не захотеть изменить все поведение функции Build, а просто ввести конкретное поведение:

package builder

import (
    "github.com/me/myproj/copier"
    "github.com/me/myproj/zipper"
)

type Builder struct {
    specificBehaviour func(string) error
}

func (b Builder) Build(path string) error {
    if err := specificBehaviour(path); err != nil {
        return err
    }

    if err := zip(the, args); err != nil {
        return err
    }

    if err := copy(the, args); err != nil {
        return err
    }

    return nil
}

func nodeSpecificBehaviour(path string) error {
    // node-specific code here
}

func pythonSpecificBehaviour(path string) error {
    // python-specific code here
}

func NewNode() Builder {
    return Builder{nodeSpecificBehaviour}
}

func NewPython() Builder {
    return Builder{pythonSpecificBehaviour}
}

решение 3: один пакет на конкретныйBuilder

На другом конце шкалы, в зависимости от гранулярности пакета, которую вы хотите использовать в своем проекте, вы можете иметь отдельный пакет для каждого строителя. С этой предпосылкой вы хотите обобщить общую функциональность, чтобы дать ей гражданство в качестве пакета. Пример:

package node

import (
    "github.com/me/myproj/copier"
    "github.com/me/myproj/zipper"
)

type Builder struct {
}

func (b Builder) Build(path string) error {
    // node-specific code here

    if err := zipper.Zip(the, args); err != nil {
        return err
    }

    if err := copier.Copy(the, args); err != nil {
        return err
    }

    return nil
}

решение 4: функции!

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

package builder

type Builder func(string) error

func NewNode() Builder {
    return func(string) error {

        // node-specific behaviour

        if err := zip(the, args); err != nil {
            return err
        }

        if err := copy(the, args); err != nil {
            return err
        }

        return nil
    }
}

func NewPython() Builder {
    return func(string) error {

        // python-specific behaviour

        if err := zip(the, args); err != nil {
            return err
        }

        if err := copy(the, args); err != nil {
            return err
        }

        return nil
    }
}

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

... Я оставлю вам удовольствие объединить некоторые из этих методов, если у вас скучный день.

Бонус!

  • Не бойтесь создавать несколько пакетов, так как это поможет вам разработать четкие границы между типами и в полной мере использовать инкапсуляцию.
  • Ключевое слово error - это интерфейс, а не тип! Вы можете return nil, если у вас нет ошибок.
  • В идеале вы не определяете интерфейс builder в пакете builder: он вам не нужен. Интерфейс builder будет размещаться в потребительском пакете.