如何根据 Clean Architecture 实现 Golang 中的 Presenter?

Posted

技术标签:

【中文标题】如何根据 Clean Architecture 实现 Golang 中的 Presenter?【英文标题】:How to implement the presenter in Golang according to the Clean Architecture? 【发布时间】:2018-12-09 17:00:27 【问题描述】:

正确的软件架构是创建可维护项目的关键。正确的意思是100%主观的, 但最近我喜欢并尝试关注 Robert C. Martin(又名 Bob 叔叔)的 Clean Architecture。

虽然我真的很喜欢这个理论,但它缺乏一些实用的实施指南来解决开发人员可能面临的常见技术挑战。 例如,我一直在努力解决的一件事是正确实现演示者层。

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

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

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

选项 1 或多或少与 Clean Architecture/Uncle 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(调用演示者的用例),我必须以某种方式将它传递给以这种方式成为请求范围的演示者,而应用程序的其余部分是完全无状态的并且不是请求范围的,它们被实例化一次.

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

我在哪里可以这样做:

    通过工厂在控制器中 在通过工厂的用例中(但话又说回来:用例必须接收响应编写器作为参数)

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

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

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

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

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

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

【问题讨论】:

【参考方案1】:

我可以告诉你我根据 Clean Architecture 的经验。我花时间在这个问题上,阅读文章和测试代码。所以我想建议你下面的帖子和附上的源代码,它对我帮助很大:

Applying The Clean Architecture to Go applications

这是一个非常好的起点,我正在以这种方式设计我的软件,开发宁静的网络应用程序,直到通过 jQuery 和 Bootstrap 向用户展示。我可以声称,现在我的软件真的深入到了独立的层中。它还帮助我理解了 golang 接口的强大功能,并最终对软件的每个部分进行了简单的测试。 希望这对您也有帮助。

【讨论】:

我已经阅读了这篇文章,我认为这很好,但它也通过简单地返回响应并且不从用例调用演示者来解决问题。我宁愿看到一个解决方案。不过还是谢谢!【参考方案2】:

我认为您可以将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)

【讨论】:

我不认为这是 bob 大叔的意思,如果你把网络从控制器或演示者中拿出来怎么办? Presenter 应该被注入到控制器中,控制器应该被注入到 web 和 web 需要处理响应、请求、cookie、headers 等。这样如果你有一个控制台应用程序,控制器和 Presenter 的功能保持不变。但这是不可能的,因为 go http 处理程序作为闭包工作。 @BoazHoch 控制器它只是域用例和基础设施之间的粘合剂 - 它不能通用。当一个人将任何新的基础设施连接到应用程序时,他必须创建与之相关的新控制器。例如,您附加 CLI 接口,因此您可以通过“cli 控制器”将其粘合到域逻辑上。可能是controller 这个词具有误导性,我们会考虑不同的概念。 我不明白您为什么需要为每个基础架构创建一个新控制器。如果控制器期望 @input 或实现某种定义良好的接口,则所有其他基础设施都需要服从它。我宁愿认为应该提供一个新的适配器,负责将来自新基础架构的传入数据转换为符合控制器接口的数据。

以上是关于如何根据 Clean Architecture 实现 Golang 中的 Presenter?的主要内容,如果未能解决你的问题,请参考以下文章

如何注入动态创建的用例(android,clean architecture,dagger2)

在 Clean Architecture 中,UI 代码在哪一层?

聊聊go-bank-transfer项目对Clean Architecture的实践

超实用教程!一探Golang怎样践行Clean Architecture?

android clean architecture 已停止运行啥意思

使用Android MVP Clean Architecture实现交互者