如何根据Clean Architecture在Golang中实现演示者?

时间:2018-07-01 01:13:11

标签: go architecture clean-architecture

正确的软件体系结构是创建可维护项目的关键。正确的方法是100%主观的, 但最近我喜欢并尝试遵循Robert C. Martin(又名Bob叔叔)的Clean Architecture

尽管我真的很喜欢该理论,但它缺乏针对开发人员可能面临的常见技术挑战的某种实用实施指南。 例如,我一直在努力的事情之一就是正确实现presenter层。

演示者负责接受用例中的“响应”并以某种方式设置其格式 可以将其“呈现”到我的输出设备(无论它是Web还是CLI应用程序)。

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

  1. 用例本身通过某种输出接口调用演示者
  2. 用例返回响应模型,控制器(最初称为用例)将该模型传递给演示者

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

基本上,它完全按照上述内容和在Clean Architecture中的描述进行操作:有一个控制器调用用例(通过边界,此处不存在)。用例会执行一些操作并调用presenter(尚未实现,但这正是问题所在)。

我们的下一步可能是实现演示者。...但是鉴于输出在Go HTTP处理程序中的工作方式,有一个很好的问题要解决。即:请求范围。

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

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

我在哪里可以这样做:

  1. 通过工厂在控制器中
  2. 在通过工厂的用例中(但是再说一次:用例必须接收响应编写器作为参数)

这带来了另一个问题:如果演示者是请求范围的,用例也是吗?

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

或者,我可以将演示者作为用例的参数(没有人说必须在“构造时”注入依赖项)。但这仍然会在某种程度上将演示者耦合到控制器。

还有其他未解决的问题(例如我应该在哪里发送HTTP标头),但是这些问题与Go无关。

这是一个理论上的问题,因为我不确定我是否要使用这种模式,但是到目前为止,我花了很多时间在思考这个问题,而没有找到一个完美的问题。

基于articles and questions,我已经阅读了有关该主题的信息:其他人也没有。

2 个答案:

答案 0 :(得分:1)

根据Clean Architecture,我可以告诉您我的经历。我花了一些时间在这个问题上,阅读文章和测试代码。因此,我想向您推荐以下文章和所附的源代码,它对我有很大帮助:

这是一个非常不错的起点,我以这种方式设计软件,以开发宁静的Web应用程序,直至通过jQuery和Bootstrap向用户演示为止。我可以断言,现在我的软件已真正分散到各个层中。另外,它还帮助我理解te golang接口的功能,并最终对软件的每个部分进行了简单的测试。 希望对您有帮助。

答案 1 :(得分:0)

我认为您可以将rw http.ResponseWriter传递到presenter以允许写出实际的响应。看起来像这样:

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

另一种方法是在闭包内传递请求编写器。这是Gin-gonic的代码  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)
}