14

Proper software architecture is key to create a project that is maintainable. What proper means is 100% subjective, but lately I like and try to follow Clean Architecture by Robert C. Martin (aka Uncle Bob).

Although I really like the theory, it lacks some sort of practical implementation guide for common technical challenges developers may face. One of the things I've been struggling with for example is properly implementing the presenter layer.

The presenter is responsible for accepting the "response" from my use case and formatting it in a way that it can be "presented" to my output device (regardless if it is a web or a CLI application).

There are multiple approaches for this problem, but they usually fall under one of these categories:

  1. The presenter is called by the use case itself through some sort of output interface
  2. The use case returns the response model and the controller (which originally called the use case) passes this model to the presenter

Option 1 is more or less the same as what Clean Architecture/Uncle Bob says (in the book and in various posts, see later), Option 2 is rather an alternative approach which works.

Sounds cool, but let's see how we can implement them in Go.

Here is my first version. For simplicity, our output goes to the web now.

Also, please excuse my brevity.

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

Basically it does exactly as described above and in Clean Architecture: There is a controller which calls a use case (through a boundary, which is not present here). The use case does something and calls the presenter (which is not implemented, but it's exactly the question).

Our next step could be implementing the presenter....but given how output works in Go HTTP handlers there is a nice problem to solve. Namely: request scope.

Every request has it's own response writer (passed to the http handler) where the response should be written. There is no global request scope that can be accessed by the presenter, it needs the response writer. So if I want to follow option 1 (use case calling the presenter), I have to pass it somehow to the presenter which becomes request scoped this way, while the rest of the application is completely stateless and not request scoped, they are instantiated once.

That also means that I either pass the response writer itself to the use case and the presenter (and I would rather not do that) or create a new presenter for each request.

Where can I do that:

  1. In the controller via factories
  2. In the use case via factories (but then again: the use case would have to receive the response writer as a parameter)

This bring in another problem: if the presenter is request scoped, is the use case too?

If I want to inject the presenter into the use case struct, then yes it is and the use case has to be created in the controller as well.

Alternatively I can make the presenter a parameter of the use case (noone said a dependency must be injected at "construction time"). But that would still somewhat couple the presenter to the controller.

There are other, unanswered issues (like where should I send HTTP headers for example), but those are less Go specific.

This is a theoretical question as I'm not yet sure that I want to use this pattern, but I've spent quite an amount of time thinking about this problem without finding the perfect one so far.

Based on the articles and questions I've read about the topic: others haven't either.

mark.sagikazar
  • 1,032
  • 8
  • 19

2 Answers2

3

I can tell you about my experience according to Clean Architecture. I spent time on that issue, reading articles and testing code. So I'd like to suggest you the following post and the source code attached, it helped me a lot:

It's a really good starting point, I'm designing my software in that way developing restful web app up to the presentation to the user passing through jQuery and Bootstrap. I can claim that now my software is really diveded into indipendent layers. Also it helped me to undestand the power of te golang interfaces and finally make simple testing each part of software. Hope this help you too.

Danilo
  • 193
  • 10
  • 2
    I've already read this article and I think this is great, but it also solves the problem by simply returning the response and does not call the presenter from the use case. I would rather see a solution for that. But thanks anyway! – mark.sagikazar Jul 01 '18 at 13:56
0

I think you can pass the rw http.ResponseWriter to the presenter to allow write the actual response. It would look like this:

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

Or another way is to pass a request writer within a closure. Here the code with Gin-gonic HTTP handler:

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

And a call to the presenter in the controller:

// 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)
}
Dr.eel
  • 1,837
  • 3
  • 18
  • 28
  • I don't think this is what uncle bob meant, what if you take the web out of the controller or the presenter? The presenter should be injected into controller, controller should be injected to web and web needs to deal response, request, cookies, headers etc. this way if you would have a console app, the functions of controller and presenter remain intact. But this is not possible since the way go http handlers work as a closure. – Boaz Hoch Jan 11 '20 at 15:42
  • 1
    @BoazHoch Controllers it's only a glue between the Domain usecase and the infrastructure - it can't be universal. When one connects any new infrastructure to the app he have to create new controllers related to it. For example you attaching CLI interface, so you glue it to the domain logic by "cli controller". May be the word `controller` is misleading and we think about different conceptions. – Dr.eel Jan 13 '20 at 08:21
  • I don't see why you would need to create a new controller for each infrastructure. If the controller expect an @input or implements some kind of interface which is well defined, all other infrastructure would need to obey to it. I'd rather think that a new adapter should be presented taking care of converting the incoming data from the new infrastructure into that conforming to the controller interface. – Boaz Hoch Jan 16 '20 at 06:32