0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

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

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

前提:ここでのclean-architectureの解釈

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

echo.Contextの取り扱い

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

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

そこで、controllerを呼び出す前にecho.Contextcontext.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.Contextecho.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フレームワークをすげかえるなんてことは早々起きなさそうではあります。どこまでクリーンにしたいか?はプロジェクト初期時によく考えることは必要そうです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?