clean-architectureのcontroller層でwebフレームワーク固有の実装をしたくない

[`labstack/echo`](https://github.com/labstack/echo)などを用いて開発をする際、[公式サンプル](https://echo.labstack.com/docs/request)のように、query, path, jsonをバインドする処理や、レスポンスを行う処理がecho.Contextに強く依存しています。

このwebフレームワーク独自の実装をclean-architectureで言うところの`web`層に隠蔽できないかと言うことでトライしてみました。↓結果

https://github.com/sYamaz/clean-architecture

## 前提：ここでのclean-architectureの解釈

1. `web`でhttpリクエストを受け付け、パス情報をもとに`controller`を呼び出す
1. `controller`でリクエスト時のパラメータを解釈し`usecase`を呼び出す
1. `usecase`で処理した結果を`presenter`に渡す
1. `presenter`でレスポンス用の形に変換
1. `web`でhttpリクエストに対するレスポンスを返却する

## echo.Contextの取り扱い

上記前提を実現するには`echo.Context`を`web`->`controller`->`usecase`->`presenter`->`web`と伝搬させなければいけません。

しかし、この記事のタイトルの通り、`echo`と言う要素をweb層以外には知らせたくはありません。

そこで、controllerを呼び出す前に`echo.Context`を`context.Context`でラップします。各層へは`context.Context`が伝搬されていきます。

## web -> controller

web層とcontroller層との連携部分を一部載せます。
routerはcontrollerを呼び出す前にecho固有の情報をラップするため、同じweb層の処理を呼びます

```web/router.go
package web

import (
	"github.com/labstack/echo/v4"
	"github.com/sYamaz/clean-architecture/controller"
)

type (
	router struct {
		e *echo.Echo
	}

	Router interface {
		Echo() *echo.Echo
	}
)

func NewRouter(e *echo.Echo,
	// webhandler
	handler Handler,
	// controller
	hello controller.HelloController,
	// ...
) Router {
	e.GET("/hello", handler.Func(hello.Get))
	return &router{
		e: e,
	}
}

func (r *router) Echo() *echo.Echo {
	return r.e
}
```

`echo.Context`を`echo.Context.Request().Context()`内にラップし、実際にcontrollerに渡すコンテキストは`context.Context`になります。
```web/handler.go
package web

import (
	"github.com/labstack/echo/v4"
	"github.com/sYamaz/clean-architecture/controller"
)

type (
	handler struct {
		operator ContextOperator
	}

	Handler interface {
		Func(controllerFunc controller.ControllerFunc) echo.HandlerFunc
	}
)

func NewWebHandler(operator ContextOperator) Handler {
	return &handler{
		operator: operator,
	}
}

func (h *handler) Func(controllerFunc controller.ControllerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		ctx := h.operator.Wrap(c.Request().Context(), c)
		return controllerFunc(ctx)
	}
}

```

最後に呼び出されるcontrollerです。bindのタグ情報がcontroller層にありますが、これはechoでなくてもreflectでパッケージで実現可能なのでcontroller層に残しています。

```controller/hello.go
package controller

import (
	"context"

	"github.com/sYamaz/clean-architecture/usecase"
)

type (
	helloController struct {
		binder  Binder
		service usecase.GreetingService
	}

	HelloController interface {
		Get(ctx context.Context) error
	}
)

func NewHelloController(binder Binder, service usecase.GreetingService) HelloController {
	return &helloController{
		binder:  binder,
		service: service,
	}
}

func (c *helloController) Get(ctx context.Context) error {
	type Request struct {
		Name string `query:"name"`
	}
	req := new(Request)

	if err := c.binder.Bind(ctx, req); err != nil {
		return err
	}

	return c.service.Hello(ctx, req.Name)
}

```

例えば、echoを使ったバインドの他にもreflectを使ってバインドさせることもできます（コメントアウト部）

以下例ではヘッダーをバインドさせていますが、クエリやパス、jsonも力をかければできるはず。

```web/binder.go
package web

import (
	"context"

	"github.com/sYamaz/clean-architecture/controller"
	customerror "github.com/sYamaz/clean-architecture/custom-error"
)

type (
	binder struct {
		operator ContextOperator
	}
)

func NewBinder(operator ContextOperator) controller.Binder {
	return &binder{
		operator: operator,
	}
}

func (b *binder) Bind(ctx context.Context, i interface{}) error {
	c := b.operator.Unwrap(ctx)
	// echoのbind機能を使う場合
	if err := c.Bind(i); err != nil {
		return customerror.NewBindError(err)
	}
	return nil
}

// func (b *binder) Bind(ctx context.Context, i interface{}) error {
// 	// 自力で`header:"KEY"`を文字列型としてbindしたい場合の例
// 	v := reflect.ValueOf(i).Elem()
// 	t := reflect.TypeOf(i).Elem()
// 	for index := 0; index < v.NumField(); index++ {
// 		headerKey := t.Field(index).Tag.Get("header")
// 		headerValue := c.Request().Header.Get(headerKey)
// 		v.Field(index).SetString(headerValue)
// 	}
// 	return nil
// }
```


## presenter -> web

※データの流れはpresenter -> webですが、参照は逆です。

ここでもecho要素は出てこないようにしています。

```presenter/greeting.go
package presenter

import (
	"context"

	"github.com/sYamaz/clean-architecture/usecase"
)

type (
	greetingPresenter struct {
		sender ResultSender
	}

	GreetingPresenter interface {
		Reply(ctx context.Context, name string) error
	}
)

func NewGreetingPresenter(sender ResultSender) usecase.GreetingPresenter {
	return &greetingPresenter{
		sender: sender,
	}
}

func (p *greetingPresenter) Reply(ctx context.Context, message string) error {
	type Response struct {
		Greeting string `json:"greeting"`
	}
	res := Response{
		Greeting: message,
	}

	return p.sender.SendJson(ctx, &res)
}

```

echo要素が出てくるのはやっぱり最後の最後、web層です

```web/http-sender.go
package web

import (
	"context"
	"net/http"

	"github.com/sYamaz/clean-architecture/presenter"
)

type (
	httpResultSender struct {
		operator ContextOperator
	}
)

func NewResultSender(operator ContextOperator) presenter.ResultSender {
	return &httpResultSender{
		operator: operator,
	}
}

func (s *httpResultSender) SendJson(ctx context.Context, i interface{}) error {
	c := s.operator.Unwrap(ctx)
	return c.JSON(http.StatusOK, i)
}

```

## まとめ

clean architectureを意識しつつ、webフレームワーク固有の実装をweb層に止めることができました。

とはいえ、webフレームワークをすげかえるなんてことは早々起きなさそうではあります。どこまでクリーンにしたいか？はプロジェクト初期時によく考えることは必要そうです。