如何根据Clean架构在Golang实现presenter?

如何根据Clean架构在Golang实现presenter?,go,architecture,clean-architecture,Go,Architecture,Clean Architecture,正确的软件架构是创建可维护项目的关键。正确的方法是100%主观的, 但最近我喜欢并尝试追随罗伯特·C·马丁(又名鲍勃叔叔) 虽然我很喜欢这个理论,但它缺乏一些实用的实现指南,无法解决开发人员可能面临的常见技术挑战。 例如,我一直在努力解决的问题之一就是正确实现presenter层 演示者负责接受来自我的用例的“响应”,并以某种方式格式化它 它可以“呈现”到我的输出设备(无论它是web还是CLI应用程序) 解决此问题有多种方法,但它们通常属于以下类别之一: 演示者由用例本身通过某种输出接口调用 用

正确的软件架构是创建可维护项目的关键。正确的方法是100%主观的, 但最近我喜欢并尝试追随罗伯特·C·马丁(又名鲍勃叔叔)

虽然我很喜欢这个理论,但它缺乏一些实用的实现指南,无法解决开发人员可能面临的常见技术挑战。 例如,我一直在努力解决的问题之一就是正确实现presenter层

演示者负责接受来自我的用例的“响应”,并以某种方式格式化它 它可以“呈现”到我的输出设备(无论它是web还是CLI应用程序)

解决此问题有多种方法,但它们通常属于以下类别之一:

  • 演示者由用例本身通过某种输出接口调用
  • 用例返回响应模型,控制器(最初称为用例)将该模型传递给演示者
  • 选项1和Clean Architecture/Bob叔叔所说的差不多(在书中和各种帖子中,见下文),选项2是一种可行的替代方法

    听起来很酷,但让我们看看如何在Go中实现它们

    这是我的第一个版本。为了简单起见,我们的输出现在就进入了网络

    另外,请原谅我的简短

    package my_domain
    
    import "http"
    
    type useCase struct {
        presenter presenter
    }
    
    func (uc *useCase) doSomething(arg string) {
        uc.presenter("success")
    }
    
    type presenter interface {
        present(respone interface{})
    }
    
    type controller struct {
        useCase useCase
    }
    
    func (c *controller) Action(rw http.ResponseWriter, req *http.Request) {
        c.useCase("argument")
    }
    
    基本上,它完全按照上面所描述的和干净体系结构中的方式进行:有一个控制器调用用例(通过一个边界,这里不存在)。用例会做一些事情并调用演示者(虽然没有实现,但这正是问题所在)

    我们的下一步可能是实现presenter…但是考虑到Go HTTP处理程序中的输出是如何工作的,有一个很好的问题需要解决。即:请求范围

    每个请求都有自己的响应编写器(传递给http处理程序),在那里应该编写响应。演示者无法访问全局请求范围,它需要响应编写器。因此,如果我想遵循选项1(用例调用呈现者),我必须以某种方式将其传递给呈现者,呈现者以这种方式成为请求范围,而应用程序的其余部分是完全无状态的,没有请求范围,它们只实例化一次

    这也意味着我要么将响应编写器本身传递给用例和演示者(我不希望这样做),要么为每个请求创建一个新的演示者

    我在哪里可以这样做:

  • 通过工厂在控制器中
  • 在用例中,通过工厂(但同样:用例必须接收响应编写器作为参数)
  • 这带来了另一个问题:如果演示者是请求范围的,那么用例也是吗

    如果我想将演示者注入到用例结构中,那么是的,并且还必须在控制器中创建用例

    或者,我可以让演示者成为用例的一个参数(noone说必须在“构建时”注入依赖项)。但这仍然会在某种程度上将演示者与控制器耦合起来

    还有其他未解决的问题(例如,我应该将HTTP头发送到哪里),但这些问题不太具体

    这是一个理论上的问题,因为我还不确定我是否想使用这个模式,但我已经花了相当多的时间思考这个问题,到目前为止还没有找到完美的模式


    根据我所读到的主题:其他人也没有。

    我可以告诉你我在Clean Architecture方面的经验。我花时间在这个问题上,阅读文章和测试代码。因此,我想向您推荐以下帖子和附带的源代码,这对我帮助很大:

    这是一个非常好的起点,我正在以这种方式设计我的软件,开发restful web应用程序,直到通过jQuery和Bootstrap向用户演示。我可以说,现在我的软件真的分成了独立的层。它还帮助我理解了te golang接口的威力,并最终对软件的每个部分进行了简单的测试。
    希望这也能对您有所帮助。

    我认为您可以将
    rw http.ResponseWriter
    传递给
    演示者,以便编写实际的响应。它看起来是这样的:

    type presenter interface {
        present(rw http.ResponseWriter, response interface{})
    }
    
    type myPresenter struct {}
    func (p *myPresenter) present(rw http.ResponseWriter, response interface{}) {
      fmt.Fprintf(rw, "Hello, %+v", response)
    }
    
    或者另一种方法是在闭包中传递请求编写器。这是金戈尼克的密码 HTTP处理程序:

      // presenter.go
      type successWriter func(result interface{})
      type errorWriter func(err error)
    
      type Presenter interface {
        OnSuccess(dw successWriter, result interface{})
        OnError(ew errorWriter, err error, meta map[string]string) // we pass error and some extras
      }
    
      type resourcePresenter struct{}
    
      func (p *resourcePresenter) OnSuccess(dw successWriter, data interface{}) {
        preaparedResult = ... // prepare response data with data        
        dw(preaparedResult)
      }
    
      func (p *resourcePresenter) OnError(ew errorWriter, err error, meta map[string]string) 
      {
        // here we can wrap error to add extra information
        enrichedError := &MyError{ err: err, meta: meta }
        ew(enrichedError)
      }
    
    以及呼叫控制器中的演示者:

    // controller.go
    // RegisterController attaches component to route. 
    func RegisterController(rg *gin.RouterGroup, uc MyUsecase) {
        ctl := &Controller{
            usecase: uc,
            presenter: &resourcePresenter{},
        }
        rg.GET("", ctl.list())
    }
    func (ctl *aController) list() gin.HandlerFunc {
       return func(c *gin.Context) {
          usecaseResp, err := ctl.usecase.List()
          if err != nil {
            ctl.presenter.OnError(
              func(err error) { // closure to pass the HTTP context (ResposeWriter)
                c.Error(err) // add error to gin.Errors, middleware will handle it
              }, 
              err, 
              map[string]string{
                "client": c.ClientIP(),
              }
            )
          }
          return
       }
       ctl.presenter.OnSuccess(func(data interface{}) {
                fmt.Println(`ctl.presenter.OnSuccess `)
                c.JSON(http.StatusOK, data)
            }, usecaseResp)
    }
    

    我已经读过这篇文章,我认为这很好,但它也通过简单地返回响应而不从用例中调用演示者来解决问题。我宁愿看到解决办法。不过还是要谢谢你!我不认为这是鲍勃叔叔的意思,如果你把网络从控制器或演示器中拿出来怎么办?演示者应该被注入控制器,控制器应该被注入网络,网络需要处理响应、请求、cookie、标题等。这样,如果你有一个控制台应用程序,控制器和演示者的功能保持不变。但这是不可能的,因为go http处理程序的工作方式是一个闭包。@BoazHoch Controller它只是域用例和基础结构之间的粘合剂-它不可能是通用的。当一个人将任何新的基础设施连接到应用程序时,他必须创建与之相关的新控制器。例如,您需要附加CLI接口,因此可以通过“CLI控制器”将其粘附到域逻辑。可能是
    controller
    这个词有误导性,我们考虑了不同的概念。我不明白为什么需要为每个基础设施创建一个新的控制器。如果控制器需要@input或实现某种定义良好的接口,那么所有其他基础设施都需要服从它。我认为应该提供一个新的适配器,负责转换来自新基础结构的传入数据