在GO中使用构造函数的继承

时间:2018-01-15 14:20:30

标签: 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's 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)
}

主要假设/流程:

  1. 要开始构建每个模块,我需要获取它的路径
  2. 我需要将模块复制到临时文件夹
  3. 我需要在其上运行构建(实现构建界面,如mvn build npm install等)
  4. 构建完成后,使用dep
  5. 压缩模块
  6. 将其复制到新目标文件夹
  7. 注意:除了buildpath(应该专门处理)之外,所有其他功能都相同。 喜欢 zip copy

      
        
    1. 我应该在哪里放置zip and copy(在结构中),例如我应该如何实现它们并将它们路由到构建器?

    2.   
    3. 我应该根据假设对项目进行不同的构建吗?

    4.   

3 个答案:

答案 0 :(得分:4)

让我们逐一审议每个问题:

<强> 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足以处理大多数构建命令。请注意PrefnPostfn字段。这些是在Build内运行命令之前和之后可以运行的钩子函数。 Prefn可以检查是否安装了软件包管理器,如果不是(或只是返回错误)则安装它。 Postfn可以运行您的zipcopy操作,也可以执行任何清理程序。这是一个用例,前提是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")
}

您可以预先定义一些PrefnPostfn并将它们作为程序用户的选项提供,假设它是一个命令行程序,或者如果它是一个库,则拥有该用户自己写。

wrapBuilder功能

在Go中构造实例时使用了一些技巧。首先,可以将参数传递给构造函数(此代码仅供说明而不使用):

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

但是这种方法非常特殊,因为无法传递任意值来配置返回的*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数组与Manager常量的枚举结合在一起。看看New工厂功能。通过参数传递的枚举常量manager是类型Manager,它只是下面的int。这意味着我们所要做的就是使用configs作为manager中的索引来访问wrapBuilder

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

例如,如果manager == Npmconfigs[int(manager)]将从NodeConfig数组返回configs

构建包

此时,让zipcopy函数与我Build一样生活在同一个包中,这很好。在你不得不这样做之前,过早地优化任何东西或者担心这一点几乎没用。这只会带来比你想要的更多的复杂性。在开发代码时,优化始终如一。

如果您希望尽早构建项目很重要,您可以根据API的语义来完成。例如,要创建新的*Builder,用户可以非常直观地从子包New调用工厂函数Createbuildeasy/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/filepath,其中包含与文件路径操作相关的实用程序功能,同时保持path的API基本和干净。

回到你的项目,我会在顶级目录/包中保留最常见,更通用的功能。这就是我将如何处理结构:

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

虽然包buildeasy包含NewNodeBuilderNewPipBuilder或仅New等功能,但在子包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,因为它是一个低于nodepython子包应该关注的私有函数。这些子包就像调用buildeasy函数的另一层API,并传递一些特定的值和配置,使其API的用户的生活更轻松。

希望这是有道理的。

答案 1 :(得分:4)

SOLID 的第一个原则是,一段代码应该只有一个责任。

采用上下文,任何builder关注构建过程的copyzip部分都没有任何意义。这超出了builder的责任。即使使用合成(嵌入)也不够整洁。

缩小范围,Builder的核心职责是建立代码,顾名思义。但更具体地说,Builder的职责是在路径上构建代码。什么路?最具风格的方式是当前路径工作目录。这为界面添加了两种方法:Path() string返回当前路径ChangePath(newPath string) error更改当前路径。实现很简单,保留单个字符串字段,因为当前路径主要完成工作。它可以很容易地扩展到一些远程过程。

如果我们仔细查看,那么实际上就是两个 build 概念。一个是整个建筑过程,从制作临时目录到复制它,所有五个步骤;另一个是构建命令,这是该过程的第三步。

这非常鼓舞人心。正如经典程序编程所做的那样,一个过程是作为一个函数呈现的过程。所以我们写了一个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。如果代码位于/tmp/xxx/之内,那么Golang将会感到不快,因为它会更改GOPATH,因为它会破坏导入路径的搜索,所以golang会要求在GOPATH内生成tempdir的唯一函数。

编辑:

哦,还有一件事我忘了说。处理这样的错误是非常丑陋和不负责任的。但是这个想法是存在的,更好的错误处理主要需要使用内容。因此,请自行更改,记录,恐慌或任何您想要的。

编辑2:

您可以重复使用您的npm示例。

GOPATH

并将它与其他语言结合在一起:

type Node struct {
    path string
}

func (n Node) Build(path string) error {
    //e.g. Run npm install which build's 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部分一样,您可以在那里添加构建选项。

编辑3:

关于包结构:

首先,我认为一切都是:Golang函数,语言构建器和其他util / helper应该放在一个包中。它们都可以完成一项任务:构建语言。没有必要,也没有任何期望将任何代码片段隔离为另一个(子)包。

这意味着一个目录。剩下的确是一些非常个人化的风格,但我会分享我的:

我会将函数Build和接口Build放入名为Builder的文件中。如果前端代码很小并且非常易读,我也会把它们放到main.go中,但是如果它很长并且有一些ui逻辑,我会把它放到main.go或{ {1}}或front-end.go,具体取决于代码。

接下来,对于每种语言,我将使用语言代码创建cli.go文件。它清楚地说明了我可以检查它们的位置。或者,如果代码非常小,那么将它们全部放在ui.go中并不是一个坏主意。毕竟,现代编辑可以更有能力对结构和类型进行定义。

最后,所有.gobuilders.go函数都转到copyDir。这很简单 - 它们是实用程序,大部分时间我们都不想打扰它们。

答案 2 :(得分:3)

Go不是面向对象的语言。这意味着,通过设计,您不必具有封装在类型本身中的类型的所有行为。当你认为我们没有继承权时,这很方便。

当你想在另一种类型上构建一个类型时,你会使用组合: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包中调用它们,您甚至可以嵌入非公开类型(例如myCopierbuilder)。但是,为什么要将它们嵌入到第一位呢?

您可以为Go项目选择几种不同的有效设计。

解决方案1:单个包暴露多个Builder类型

在这种情况下,您需要一个builder包,它将公开多个构建器。

zipcopy是包中某处定义的两个函数,它们不需要是附加到类型的方法。

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:每个specificBuilder一个包

在比例的另一端,根据您要在项目中使用的程序包粒度,您可能希望为每个构建器都有一个不同的程序包。在这个前提下,您希望将共享功能概括为足以使其成为包的公民身份。例如:

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界面将位于消费者包中。