6
4

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.

RestAPIをCleanArchitectureで実装する際のreq/resの持ち回りについて

Last updated at Posted at 2022-02-08

zennに投稿した記事の転載です。

概要

提題の通り、CleanArchitectureに則ってRestAPIを実装する場合に、
以下の2点に迷ったので、結論とそれに至るまでの考えを書き残しています。

  • requestオブジェクトはどのレイヤーまで持ち回すべきか?
  • responseオブジェクトはどのレイヤーで生成するべきか?

背景

よくあるシンプルなCRUDアプリを実装する際、以下のような処理を実装するとします。

リクエストボディからタスク名を受け取る
      ↓
DBに新規タスクとしてレコードを登録する
      ↓
登録したレコードの情報をレスポンスとして返す      

この場合、controller→usecase→repositoryとMVCやレイヤーアーキテクチャ的にパッケージを掘っていき処理を実現して行くのは誰もが違和感なく考えられることですが、
今回はCleanArchtectureを採用しているため、controller → usecase ← repositoryのような依存関係で実装する必要があります。
※詳しくは後述するCleanArchtectureのドーナツ絵を参考にしてください。

そこで、上記依存関係に則ってコード書いている時にふとこんなことを思いました。

「controllerからusecaseに値を渡す際に、requestオブジェクトのまま値を渡すのは正しいのか?」
「responseオブジェクトは本来どこで生成すべきか?」

Golangのコードで言うと以下のような実装です。

package controller

type TaskController struct {
	taskUseCase usecase.TaskUseCase
}

func NewTaskController(usecase.TaskUseCase) *TaskController {
	return &TaskController{
		taskUseCase: taskUseCase,
	}
}

func (tc *TaskController) Create(req requestdto.TaskRequest) {
	res, err := tc.taskUseCase.Create(req)
	if err != nil {
		c.JSON(500, err)
		return
	}
	c.JSON(200, res)
}
package usecase

type TaskInteractor struct {
	taskRepository TaskRepository
}

func NewTaskInteractor(taskRepository TaskRepository) *TaskUseCase {
	return &TaskInteractor{
		taskRepository: taskRepository,
	}

}

func (ti *TaskInteractor) Create(req requestdto.TaskRequest) *responsedto.TaskResponse {
    var newTask = model.Task{
        taskName : req.taskName,
    }
    // 登録した結果が返る
    result, err := ti.taskRepository.Create(newTask)
    if err != nil {
        return nil, err
    }
    return responsedto.TaskResponse{
        Result : result
    }
}

一見するとMVCなどでよく見る割と当たり前な実装です。

また、両コードの依存性も依存性の逆転の原則に則っており、CleanArchtectureの依存ルールにも反さないようにしています。
image.png

※依存性の逆転の原則
 ・上位と下位のレイヤーの両方が抽象に依存すべき
 ・実装が抽象に依存すべき
参考:https://speakerdeck.com/hiroki_hasegawa/domeinqu-dong-she-ji-toyi-cun-xing-ni-zhuan-falseyuan-ze?slide=14

感じた違和感

上述したコードではcontroller → usecase ← repositoryといったように、
Applicationレイヤー(usecase)にInterfaceレイヤー(controller, repository)が依存するようになっています。

ですが、request/responseはpresenterパッケージに属しているのでこちらもInterfaceレイヤーのコードとなります。
ここで先程のusecaseのCreateメソッドをもう一度見てみると

func (ti *TaskInteractor) Create(req requestdto.TaskRequest) *responsedto.TaskResponse {
    //...処理
}

Createメソッドがrequest/responseの両方に依存しており、presenter ← usecaseという依存関係になってしまっています。

これではCleanArchtectureの依存ルールに反してしまいます。

ならばどうするべきか?

この違和感に気づいた際に考えたのは以下の2つの解決策でした。

  • usecaseパッケージにrequest/responseのinterfaceを用意し、抽象に依存させることで依存関係を逆転させる。
  • そもそもusecaseパッケージでrequest/responseパッケージの資源を操作しない。

usecaseパッケージにreqest/responseのinterfaceを用意

結論からいうとボツ案です。

理由は簡単でrequest/responseはDTOであるため、あらゆるパッケージに依存しない疎結合な資源でなくてはならないからです。
そのため依存関係の逆転を実現するためにusecase側にinterfaceを置くと、外部のパッケージに依存する密結合な資源となってしまいます。
また、そもそも構造体は振る舞いやデータに着目するものであって、抽象化するべきものではない。
(こんな論理たてなくとも普通に考えてreq/resにinterfaceがある時点で怪しいコードですが。。。)

usecaseパッケージでrequest/responseパッケージの資源を操作しない。

こちらが採用案です。

実現方法は簡単で、提題の通りにusecaseは操作に必要なもののみを受け取り、操作した結果のみを返す形式に下だけです。

修正したコードは以下の通り

package controller

type TaskController struct {
	taskUseCase usecase.TaskUseCase
}

func NewTaskController(usecase.TaskUseCase) *TaskController {
	return &TaskController{
		taskUseCase: taskUseCase,
	}
}

func (tc *TaskController) Create(req requestdto.TaskRequest) {
	result, err := tc.taskUseCase.Create(req.taskName)
	if err != nil {
		c.JSON(500, err)
		return
	}
	c.JSON(200, responsedto.TaskResponse{
    Result : result
  })
}
package usecase

type TaskInteractor struct {
	taskRepository TaskRepository
}

func NewTaskInteractor(taskRepository TaskRepository) *TaskUseCase {
	return &TaskCheckInteractor{
		taskRepository: taskRepository,
	}

}

func (ti *TaskInteractor) Create(taskName string) *model.Task {
  var newTask = model.Task{
    taskName : taskName,
  }
  // 登録した結果が返る
	result, err := ti.taskRepository.Create(newTask)
	if err != nil {
		return nil, err
	}
  return &result
}

requestの分解とresponseの生成をcontrollerで行っています。

request/responseが属するpresenterパッケージは、controllerパッケージと同じInterfaceレイヤーの資源のため、
依存関係的にも問題がありません。

また、このusecaseのテストコードという観点でも、テストコードの依存がrequest/responseに波及することがないため、依存関係をテスト対象のみに留めることができたり、
controllerパッケージ側の都合でrequest/responseが変更されたりしても、usecase及びusecaseのテストコードに影響が出るということもありません。

追記

今回はusecase側のCreateメソッドがstringを引数としているので、処理がrequestの分解のみになってますが、
引数が多い場合は、さらにusecase側にDTOを作成しそのDTOにrequestの値を詰め直すのが理想でしょう。

まとめ

まとめると

  • requestオブジェクトはApplicationレイヤーまで持ち回すべきではない
  • responseオブジェクトはApplicationレイヤーで生成するべきではない

となり、

  • reqest/responseオブジェクトはInterfaceレイヤーで分解/生成を行う

となりました。

今までcontrollerやusecase、他には今回取り上げていませんがentitesやbusinessruleなどの依存関係には気を払っていましたが、reqest/responseはMVCの頃と同じようになにも疑わずに扱っていました。

ちょっとcontrollerにバリデーション以外の処理を書くのもMVCになれてると違和感ですが、ここまでの論理の積み上げでは正しいと思うので、これがあるべきとして納得しようと思ってますw
(理想はcontrollerにたどり着いた際にバリデーションとリフレクションをこなしてくれる横断処理があるといいでしょう)

終わりに

ここまで読んでくださりありがとうございました。

1からCleanArchtectureに挑戦しようとしている方や、お仕事で使う(使ってる)という方の参考になると幸いです。
また、有識者の方で意見等ありましたら遠慮なく言ってくださると嬉しいです。

最後にLGTMいただけると筆者の励みになりますので、心に余裕がある方はよろしくおねがいします!

6
4
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
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?