Necesito crear un constructor (base) y constructores específicos para cada tipo de construcción.

e.g.

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

...

La funcionalidad principal será la siguiente:

Archivo: Builder.go

Interfaz

type Builder interface {
    Build(string) error
}

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

Principales supuestos / proceso:

  1. Para comenzar a construir en cada módulo, necesito obtener la ruta hacia él
  2. Necesito copiar el módulo a una carpeta temporal
  3. Necesito ejecutar la compilación en él (implementar la interfaz de compilación como mvn build npm install etc.)
  4. Una vez finalizada la compilación, comprima el módulo con dep
  5. Cópielo en la nueva carpeta de destino

Nota: además de build y path (que deben manejarse específicamente), todas las demás funciones son idénticas
me gusta zip copy

  1. ¿Dónde debo poner el zip and copy (en la estructura) y, por ejemplo, cómo debo implementarlos y dirigirlos al constructor?

  2. ¿Debería estructurar el proyecto de manera diferente de acuerdo con los supuestos?

go
7
user6124024 15 ene. 2018 a las 17:20

3 respuestas

La mejor respuesta

El primer principio de SOLID dice que un código debe tener una sola responsabilidad.

Toma el contexto, realmente no tiene sentido que cualquier builder se preocupe por la parte copy y zip del proceso de compilación. Está más allá de la responsabilidad de builder. Incluso el uso de la composición (incrustación) no es lo suficientemente limpio.

En pocas palabras, la responsabilidad principal de Builder es construir el código, como su nombre lo indica. Pero más específicamente, la responsabilidad de Builder es construir el código en una ruta. Que camino La forma más idomática es la ruta actual , el directorio de trabajo . Esto agrega dos métodos secundarios a la interfaz: Path() string que devuelve la ruta actual y ChangePath(newPath string) error para cambiar la ruta actual . La implementación sería simple, preservar un solo campo de cadena ya que la ruta actual en su mayoría haría el trabajo. Y se puede extender fácilmente a algún proceso remoto.

Si lo miramos con cuidado, en realidad hay dos conceptos build . Uno es todo el proceso de construcción , desde hacer un directorio temporal para copiarlo, los cinco pasos; el otro es el comando de compilación, que es el tercer paso del proceso.

Eso es muy inspirador. Un proceso es idomático para ser presentado como una función, como lo haría la programación procesal clásica. Entonces escribimos una función Build. Serializa los 5 pasos, simple y llanamente.

Código:

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.

La mayoría de las cosas están cubiertas en los comentarios y realmente me da pereza escribir todas esas copyDir / removeDir cosas. Una cosa que no se menciona en la parte de diseño es la función mkTempDir. Golang no estaría contento si el código está en /tmp/xxx/ ya que está fuera de GOPATH, y le costaría más cambiar GOPATH ya que romperá la búsqueda de rutas de importación, por lo que necesitaría golang una función única para generar un tempdir dentro de GOPATH.

Editar:

Oh, una cosa más que olvidé decir. Es terriblemente feo e irresponsable manejar errores como este. Pero la idea está ahí, y un manejo de errores más decente requiere principalmente el contenido de uso. Así que cámbielo usted mismo, inicie sesión, pánico o lo que quiera.

Editar 2:

Puede reutilizar su ejemplo npm de la siguiente manera.

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
}

Y para combinarlo con otro idioma todos juntos:

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

Un truco es obtener el nombre del idioma. GetLanguageName debe devolver el nombre del idioma que está utilizando el código en path. Esto se puede hacer usando ioutil.ReadDir para detectar nombres de archivos.

También tenga en cuenta que aunque hice la estructura Node muy simple y solo almacena un campo path, puede extenderla fácilmente. Al igual que en la parte Golang, puede agregar opciones de compilación allí.

Edición 3:

Sobre la estructura del paquete:

En primer lugar, pienso literalmente en todo: la función Build, los creadores de lenguaje y otros util / helpers deberían integrarse en un solo paquete. Todos trabajan para una sola tarea: construir un lenguaje. No hay necesidad y casi ninguna expectativa de aislar cualquier pieza de código como otro (sub) paquete.

Entonces eso significa uno para. El resto es realmente un estilo muy personal, pero compartiré el mío:

Pondría la función Build y la interfaz Builder en un archivo llamado main.go. Si el código de front-end es mínimo y muy legible, también los pondría en main.go, pero si es largo y tiene algo de lógica ui, lo pondría en front-end.go o { {X5}} o ui.go, dependiendo del código actual.

Luego, para cada idioma, crearía un archivo .go con el código de idioma. Deja en claro dónde puedo revisarlos. Alternativamente, si el código es realmente pequeño, no es una mala idea ponerlos todos juntos en un builders.go. Después de todo, los editores modernos pueden ser más que capaces de definir estructuras y tipos.

Finalmente, todas las funciones copyDir, zipDir van a util.go. Eso es simple: son utilidades, la mayoría de las veces no queremos molestarlos.

4
leaf bebop 19 ene. 2018 a las 15:20

Veamos cada pregunta una por una:

1. ¿Dónde debo colocar el zip y copiar (en la estructura) y, por ejemplo, cómo debo implementarlos y dirigirlos al constructor?

Una interfaz no transporta ningún dato (suponiendo que desea implementar uno de su código). Es solo un plano que un objeto puede implementar para pasar como un tipo más genérico. En este caso, si no está pasando el tipo Builder a ninguna parte, la interfaz es redundante.

2. ¿Debo estructurar el proyecto de manera diferente de acuerdo con los supuestos?

Esta es mi opinión sobre el proyecto. Explicaré cada parte por separado después del código:

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
}

Ahora esto Builder es lo suficientemente genérico como para manejar la mayoría de los comandos de compilación. Observe los campos Prefn y Postfn. Estas son funciones de enlace que puede ejecutar antes y después de que el comando se ejecute dentro de Build. Prefn puede verificar si, por ejemplo, el administrador de paquetes está instalado e instalarlo si no lo está (o simplemente devolver un error). Postfn puede ejecutar sus operaciones zip y copy, o cualquier rutina de limpieza. Aquí hay un caso de uso, siempre que superbuild sea nuestro nombre de paquete ficticio y el usuario lo esté usando desde afuera:

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

Puede predefinir algunos Prefn sy Postfn s y ponerlos a disposición como opción para el usuario de su programa, suponiendo que sea un programa de línea de comandos, o si es una biblioteca, haga que el usuario los escriba por sí mismo .

función wrapBuilder

Existen algunas técnicas utilizadas para construir una instancia en Go. Primero, los parámetros se pueden pasar a una función constructora (este código es solo para explicación y no se debe usar):

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

Pero este enfoque es muy ad-hoc porque es imposible pasar valores arbitrarios para configurar el *Builder devuelto. Un enfoque más sólido es pasar una instancia config de *Builder:

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

Al usar la función wrapBuilder, puede escribir una función para manejar (re) la asignación de valores de una instancia:

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

Ahora puede pasar cualquier función para configBuilderFn para configurar su instancia *Builder.

Para leer más, consulte https: //dave.cheney .net / 2014/10/17 / functional-options-for-friendly-apis.

matriz de configuraciones

La matriz configs va de la mano con la enumeración de las constantes Manager. Eche un vistazo a la función de fábrica New. la constante de enumeración manager pasada al pasar el parámetro es de tipo Manager que es solo un int debajo. Esto significa que todo lo que teníamos que hacer es acceder configs usando manager como índice en wrapBuilder:

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

Por ejemplo, si manager == Npm, configs[int(manager)] devolverá NodeConfig de la matriz configs.

Estructurando paquete (s)

En este punto, está bien tener las funciones zip y copy para vivir en el mismo paquete que Build como lo hice yo. De poco sirve optimizar prematuramente algo o preocuparse por eso hasta que sea necesario. Eso solo introducirá más complejidad de la que deseas. La optimización se produce constantemente a medida que desarrolla el código.

Si siente que estructurar el proyecto temprano es importante, puede hacerlo en función de la semántica de su API. Por ejemplo, para crear un nuevo *Builder, es bastante intuitivo para el usuario llamar a una función de fábrica New o Create desde un subpaquete 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")   
}

Otro enfoque más detallado es incluir la semántica como parte del nombre de la función, que también se usa en los paquetes estándar de Go:

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

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

En los paquetes estándar de Go, las funciones y métodos detallados son comunes. Esto se debe a que normalmente estructura subpaquetes (subdirectorios) para utilidades más específicas, como ruta / ruta de archivo, que contiene funciones de utilidad relacionadas con la manipulación de la ruta del archivo mientras se mantiene la API básica y limpia de path.

Volviendo a su proyecto, mantendría las funciones más comunes y más genéricas en el directorio / paquete de nivel superior. Así es como abordaría la estructura:

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

Mientras que el paquete buildeasy contiene funciones como NewNodeBuilder, NewPipBuilder o simplemente New que acepta opciones adicionales (como el código anterior), en el subpaquete buildeasy/node, por ejemplo , puede verse así:

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

// ...

O 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)
}

// ...

Tenga en cuenta que en los subpaquetes nunca tiene que llamar a buildeasy.zipAndCopy porque es una función privada que tiene un nivel inferior al que deberían importar los subpaquetes node y python. estos subpaquetes actúan como otra capa de API que llama a las funciones de buildeasy y transfiere algunos valores y configuraciones específicos que facilitan la vida del usuario de su API.

Espero que esto tenga sentido.

4
Pie 'Oh' Pah 22 ene. 2018 a las 18:45

Go no es un lenguaje orientado a objetos. Esto significa que, por diseño, no necesariamente tiene todo el comportamiento de un tipo encapsulado en el tipo mismo. Y esto es útil cuando crees que no tenemos herencia.

Cuando desee construir un tipo sobre otro tipo, use composición en su lugar: un struct puede incrustar otros tipos y exponer sus métodos.

Supongamos que tiene un tipo MyZipper que expone un método Zip(string) y un MyCopier que expone un método 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
    }
}

Y esto es composición en Go. Yendo más allá, incluso puede incrustar tipos no expuestos (por ejemplo, myZipper y myCopier) si solo desea que se los llame desde el paquete builder. Pero entonces, ¿por qué incrustarlos en primer lugar?

Puede elegir entre varios diseños válidos diferentes para su proyecto Go.

Solución 1: paquete único que expone varios tipos de generador

En este caso, desea un paquete builder, que expondrá múltiples constructores.

zip y copy son dos funciones definidas en algún lugar del paquete, no necesitan ser métodos adjuntos a un tipo.

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
}

Solución 2: paquete único, comportamiento específico de incrustación de tipo único

Dependiendo de la complejidad del comportamiento específico, es posible que no desee cambiar todo el comportamiento de la función Generar, sino simplemente inyectar un comportamiento específico:

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

Solución 3: un paquete por constructor específico

En el otro extremo de la escala, dependiendo de la granularidad del paquete que desee usar en su proyecto, es posible que desee tener un paquete distinto para cada constructor. Con esta premisa, desea generalizar la funcionalidad compartida lo suficiente como para darle una ciudadanía como paquete también. Ejemplo:

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
}

Solución 4: funciones!

Si sabe que sus constructores serán puramente funcionales, lo que significa que no necesitan ningún estado interno, entonces es posible que desee que sus constructores sean tipos de funciones en lugar de interfaces. Aún podrá manipularlos como un solo tipo desde el lado del consumidor, si esto es lo que desea:

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

No usaría funciones para su caso particular, porque necesitará resolver problemas muy diferentes con cada BUilder, y definitivamente necesitará algún estado en algún momento.

... Te dejaré el placer de combinar algunas de estas técnicas, si estás teniendo una tarde aburrida.

¡Prima!

  • No tenga miedo de crear múltiples paquetes, ya que esto lo ayudará a diseñar límites claros entre los tipos y aprovechar al máximo la encapsulación.
  • ¡La palabra clave error es una interfaz, no un tipo! Puedes return nil si no tienes errores.
  • Idealmente, no define la interfaz Builder en el paquete builder: no la necesita. La interfaz Builder se ubicará en el paquete del consumidor.
3
Pierre Prinetti 19 ene. 2018 a las 14:35
48264882