我需要为每种构建类型创建构建器(基础)和特定构建器。
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)
}
主要假设/流程:
mvn build
npm install
等)注意:除了build
和path
(应该专门处理)之外,所有其他功能都相同。
喜欢 zip
copy
我应该在哪里放置
zip and copy
(在结构中),例如我应该如何实现它们并将它们路由到构建器?- 醇>
我应该根据假设对项目进行不同的构建吗?
答案 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
足以处理大多数构建命令。请注意Prefn
和Postfn
字段。这些是在Build
内运行命令之前和之后可以运行的钩子函数。 Prefn
可以检查是否安装了软件包管理器,如果不是(或只是返回错误)则安装它。 Postfn
可以运行您的zip
和copy
操作,也可以执行任何清理程序。这是一个用例,前提是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
}
但是这种方法非常特殊,因为无法传递任意值来配置返回的*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 == Npm
,configs[int(manager)]
将从NodeConfig
数组返回configs
。
构建包
此时,让zip
和copy
函数与我Build
一样生活在同一个包中,这很好。在你不得不这样做之前,过早地优化任何东西或者担心这一点几乎没用。这只会带来比你想要的更多的复杂性。在开发代码时,优化始终如一。
如果您希望尽早构建项目很重要,您可以根据API的语义来完成。例如,要创建新的*Builder
,用户可以非常直观地从子包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/filepath,其中包含与文件路径操作相关的实用程序功能,同时保持path
的API基本和干净。
回到你的项目,我会在顶级目录/包中保留最常见,更通用的功能。这就是我将如何处理结构:
buildeasy
├── buildeasy.go
├── python
│ └── python.go
└── node/
└── node.go
虽然包buildeasy
包含NewNodeBuilder
,NewPipBuilder
或仅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
,因为它是一个低于node
和python
子包应该关注的私有函数。这些子包就像调用buildeasy
函数的另一层API,并传递一些特定的值和配置,使其API的用户的生活更轻松。
希望这是有道理的。
答案 1 :(得分:4)
SOLID 的第一个原则是,一段代码应该只有一个责任。
采用上下文,任何builder
关注构建过程的copy
和zip
部分都没有任何意义。这超出了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
中并不是一个坏主意。毕竟,现代编辑可以更有能力对结构和类型进行定义。
最后,所有.go
,builders.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
包中调用它们,您甚至可以嵌入非公开类型(例如myCopier
和builder
)。但是,为什么要将它们嵌入到第一位呢?
您可以为Go项目选择几种不同的有效设计。
在这种情况下,您需要一个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
}
根据特定行为的复杂性,您可能不希望更改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}
}
在比例的另一端,根据您要在项目中使用的程序包粒度,您可能希望为每个构建器都有一个不同的程序包。在这个前提下,您希望将共享功能概括为足以使其成为包的公民身份。例如:
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
}
如果您知道您的构建器将是纯粹的功能,意味着它们不需要任何内部状态,那么您可能希望构建器成为接口的函数类型。如果这是您想要的,您仍然可以从消费者一侧将它们作为单一类型进行操作:
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
界面将位于消费者包中。