3
3

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.

本記事は Go Advent Calendar 2021 の16日目の記事です。

はじめに🎄

Clean Architectureの概念にそって、GoでWebのBackend APIを開発する時に
依存性の注入が多く、特にデータのCRUD処理を行うRepositoryが課題になったので解決策を考えてみました。

Repositoryが多くなる問題

例えばECサイトでユーザーが購入をするユースケースの場合、
ざっくりと売る人, 商品, 買う人があって、そのデータたちの取得と保存をするRepositoryがそれぞれあるとします。

その場合、購入のユースケースに必要なリポジトリは3つ以上になってDIするときもコードが長くなって可読性が落ちることや、機能追加時に必要なRepositoryが増えると、更にusecaseに追加するなどによって保守性が低くなります。

type ConsumerUsecase struct {
	consumerRepo repository.ConsumerRepository
	productRepo  repository.ProductRepository
	sellerRepo   repository.SellerRepository
	// ... どんどん追加される😰
}

func (u *ConsumerUsecase) Buy(consumerID, productID int) error {
	product := u.productRepo.GetProduct(productID)
	if product.IsSoldout() {
		return errors.New("Soldout")
	}

	if err := u.consumerRepo.Buy(product); err != nil {
		return err
	}

	seller := u.sellerRepo.GetSeller(product.SellerID)
	u.Notification(seller)

	return nil
}

複数のテーブルをまたぐ処理は?

単純にRepositoryが増えるだけでなく、複数のテーブルをまたぐ処理の場合(特にトランザクションを張る必要がある処理)は、どのRepositoryに処理を置くべきか基準が曖昧になって、命名にも迷うことになり設計にブレが発生します。

UsecaseごとにRepositoryを作ったら?

では、usecaseごとに一つのRepositoryを持つ形だと先程の問題が解決できそうです。
しかし、各リポジトリごと同じメソッドがありまして、実装部をinterfaceごと作ってしまうと、コードの再利用がうまくできない場面が発生します。

// 買う人のUsecaseで使われるrepository
type ConsumerRepository interface {
	GetConsumer(id int) model.Consumer
	GetProduct(id int) model.Product // 商品情報は買う人側でも必要な情報
	LikedProducts(id int) []model.Product
	...
}

// 売る人のUsecaseで使われるrepository
type SellerRepository interface {
	GetConsumer(id int) model.Consumer
	GetProduct(id int) model.Product // 商品情報は売る人側でも必要
	SaleHistory() []model.Sale
	...
}

「買うと売るは一つのユースケースじゃないか!」と思いますが、例えのためのコードですのでご了承ください。

各Repositoryごとに実装を分けるとソースコードのファイル管理面的には利点がありますが、同じ役割をするメソッドを両方書くのは無駄で、保守性が低くなります。
また、重複する部分は関数に取り出すのも考えましたが、元の引数とDBコネクションまで渡す必要があります。

Duck Typingを活用しよう 🦆

GoはDuck Typingを採用しているので、活用すればシンプルに解決できるかと思います。
すべてのRepositoryのメソッドを一つの実装部にまとめて置くと、実装の重複もなせるし、命名やコードを置く場所を悩むことは減ると思います。
interfaceを満たさないことがあればビルドもできず、Compilerが教えてくれるので実装漏れは防げます。

type SqlRepo struct {
	db *sql.DB
}
func NewSqlRepo() *SqlRepo {
	return &SqlRepo{}
}

func (r *SqlRepo) GetConsumer(id int) model.Consumer {
	...
}
func (r *SqlRepo) LikedProducts(id int) []model.Product {
	...
}
func (r *SqlRepo) OrderHistory(id int) []model.Product {
	...
}
func (r *SqlRepo) GetProduct(id int) model.Product {
	...
}

...

func (r *SqlRepo) SaleHistory() []model.Product {
	...
}

ただ、repositoryのメソッドが多くなって、各実装自体も複雑で長くなれば、一つのファイルで管理するのは
読みづらさとコンフリクト等の課題が新たに発生します。
その時は、構造体とConstructor、返すモデルごとなど、いくつかのファイルに分けるのもありかと思います。

構造体とメソッドが別ファイルになるのはデメリットがありそうですが、パッケージに他の実装を入れない構成での運用やIDEの機能でカバーできるので、前述した課題に比べるとさほど辛くはないと思います。

まとめ🎅🏼

今まで私は、一つのinterfaceに紐づく一つ以上の実装はよくやっていましたが、
Duck Typingを活用した複数のinterfaceに紐づく一つの実装はあんまりやっていませんでした。
この方法で業務のGo x Clean Architectureで開発しているところも改修してみようと思います。

Goを勉強し始めた頃、このDuck Typingを理解するのが難しかった覚えがありますが、よく活用すると利点が多くありそうです。

3
3
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
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?