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?

[読解シリーズ]アーキテクチャ初学者によるクリーンアーキテクチャプロジェクト読解の備忘録

Last updated at Posted at 2025-03-07

目的

クリーンアーキテクチャのプロジェクトを読解し、その過程で学んだことを備忘録として記録する。これにより、アプリケーションサーバーのアーキテクチャに対する理解を深め、今後の学習や開発の基盤を築くことを目的としている。
(初学者による備忘録のため、誤りがある場合はご容赦ください。)

以下は読解したプロジェクト

おすすめの使用方法

Chromeブラウザで2つのウィンドウを開き、この記事にアクセス。

  • 左側のウィンドウ: コードを参照するために使用。
  • 右側のウィンドウ: 解説を読みながら理解を深めるために使用。

このように並べて表示することで、コードと解説を同時に確認でき、理解がスムーズに進むかも。
image.png

クリーンアーキテクチャとは

ソフトウェアを設計するための方法の一つで、重要な「ビジネスロジック」と、データベースやフレームワークなどの「具体的な技術」を分けて作ることを目指す。
image.png

この設計の何が嬉しいの?

  1. 開発の早期開始が可能:各層が独立しているため、フレームワークやデータベースが未決定でも開発を始められる。
  2. 並行開発が容易:各層が独立しているため、他の部分の完成を待たずに作業を進められる。
  3. 将来の変更が容易:各層が独立しているため、フレームワークやデータベースの変更がスムーズにできる。

図とコードを対応させる

Accountが生成されるまでの全体図

image.png

domain/account.go

package domain

import (
	"context"
	"errors"
	"time"
)

var (
	ErrAccountNotFound = errors.New("account not found")

	ErrAccountOriginNotFound = errors.New("account origin not found")

	ErrAccountDestinationNotFound = errors.New("account destination not found")

	ErrInsufficientBalance = errors.New("origin account does not have sufficient balance")
)

type AccountID string

func (a AccountID) String() string {
	return string(a)
}

type (
	AccountRepository interface {
		Create(context.Context, Account) (Account, error)
		UpdateBalance(context.Context, AccountID, Money) error
		FindAll(context.Context) ([]Account, error)
		FindByID(context.Context, AccountID) (Account, error)
		FindBalance(context.Context, AccountID) (Account, error)
	}

	Account struct {
		id        AccountID
		name      string
		cpf       string
		balance   Money
		createdAt time.Time
	}
)

func NewAccount(ID AccountID, name, CPF string, balance Money, createdAt time.Time) Account {
	return Account{
		id:        ID,
		name:      name,
		cpf:       CPF,
		balance:   balance,
		createdAt: createdAt,
	}
}

func (a *Account) Deposit(amount Money) {
	a.balance += amount
}

func (a *Account) Withdraw(amount Money) error {
	if a.balance < amount {
		return ErrInsufficientBalance
	}

	a.balance -= amount

	return nil
}

func (a Account) ID() AccountID {
	return a.id
}

func (a Account) Name() string {
	return a.name
}

func (a Account) CPF() string {
	return a.cpf
}

func (a Account) Balance() Money {
	return a.balance
}

func (a Account) CreatedAt() time.Time {
	return a.createdAt
}

func NewAccountBalance(balance Money) Account {
	return Account{balance: balance}
}

対応部分は赤枠部分
image.png

AccountRepository< i >

AccountRepository interface {
		Create(context.Context, Account) (Account, error)
		UpdateBalance(context.Context, AccountID, Money) error
		FindAll(context.Context) ([]Account, error)
		FindByID(context.Context, AccountID) (Account, error)
		FindBalance(context.Context, AccountID) (Account, error)
	}

対応部分は青枠部分
image.png

なぜ AccountRepository< i > を定義するのか?

2つ外側のアダプタ層で AccountRepository オブジェクトを定義するため、ドメイン層ではその受け皿としてインターフェースを定義する。

AccountRepository< i >を定義するときのキモチ

  • 「リポジトリはインタフェースにしないと。実装に依存したらテストできなくなるし、データベース変えるときに死ぬ。」
  • 「Createは必須だな。アカウント作る機能がないと始まらない。」
  • 「UpdateBalanceもいるわ。出金や入金で使うから。」
  • 「FindAllは管理画面で使うかも。」
  • 「FindByIDは絶対必要だ。IDで検索しないと何もできない。」
  • 「FindBalanceもいるか?残高だけ取得するケースもあるし。」
  • 「エラーはちゃんと返さないと。アカウントが見つからないとか、残高不足とか。」
  • 「コンテキストも渡さないと。キャンセルやタイムアウト制御できないし。」
  • 「これでドメイン層がデータ層に依存しなくなるな。」
  • 「テストも楽だし、将来の変更にも強くなる。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

usecase/create_account.go

package usecase

import (
	"context"
	"time"

	"github.com/gsabadini/go-clean-architecture/domain"
)

type (
	// CreateAccountUseCase input port
	CreateAccountUseCase interface {
		Execute(context.Context, CreateAccountInput) (CreateAccountOutput, error)
	}

	// CreateAccountInput input data
	CreateAccountInput struct {
		Name    string `json:"name" validate:"required"`
		CPF     string `json:"cpf" validate:"required"`
		Balance int64  `json:"balance" validate:"gt=0,required"`
	}

	// CreateAccountPresenter output port
	CreateAccountPresenter interface {
		Output(domain.Account) CreateAccountOutput
	}

	// CreateAccountOutput output data
	CreateAccountOutput struct {
		ID        string  `json:"id"`
		Name      string  `json:"name"`
		CPF       string  `json:"cpf"`
		Balance   float64 `json:"balance"`
		CreatedAt string  `json:"created_at"`
	}

	createAccountInteractor struct {
		repo       domain.AccountRepository
		presenter  CreateAccountPresenter
		ctxTimeout time.Duration
	}
)

// NewCreateAccountInteractor creates new createAccountInteractor with its dependencies
func NewCreateAccountInteractor(
	repo domain.AccountRepository,
	presenter CreateAccountPresenter,
	t time.Duration,
) CreateAccountUseCase {
	return createAccountInteractor{
		repo:       repo,
		presenter:  presenter,
		ctxTimeout: t,
	}
}

// Execute orchestrates the use case
func (a createAccountInteractor) Execute(ctx context.Context, input CreateAccountInput) (CreateAccountOutput, error) {
	ctx, cancel := context.WithTimeout(ctx, a.ctxTimeout)
	defer cancel()

	var account = domain.NewAccount(
		domain.AccountID(domain.NewUUID()),
		input.Name,
		input.CPF,
		domain.Money(input.Balance),
		time.Now(),
	)

	account, err := a.repo.Create(ctx, account)
	if err != nil {
		return a.presenter.Output(domain.Account{}), err
	}

	return a.presenter.Output(account), nil
}

対応部分は赤枠部分
image.png

CreateAccountInteractor

createAccountInteractor struct {
		repo       domain.AccountRepository
		presenter  CreateAccountPresenter
		ctxTimeout time.Duration
	}

対応部分は青枠部分
image.png

なぜ createAccountInteractor 構造体を定義するのか?

ユースケース(ビジネスロジック)をまとめるためである。
アカウント作成には、

  • データ保存(repo
  • 出力の整形(presenter
  • 処理時間の制限(ctxTimeout
    といった役割がある。

関数だけで書くと引数管理が煩雑だが、createAccountInteractor にまとめることで、シンプルに扱うことができる。

createAccountInteractor を定義するときのキモチ

  • 「ユースケースを実装するための構造体が必要だな。createAccountInteractor にしよう。」
  • 「リポジトリは絶対に必要だ。アカウントを作成するんだから。」
  • 「リポジトリはインタフェースで受け取ろう。実装に依存したらテストできなくなるし。」
  • 「プレゼンターも必要だな。出力を整形する責任を持たせよう。」
  • 「コンテキストのタイムアウトも設定できるようにしよう。長時間の処理を防ぐためだ。」
  • 「コンストラクタ作っとくか。リポジトリ、プレゼンター、タイムアウトを渡さないと動かないようにしないと。」
  • 「メソッドは Execute にしよう。コンテキストと入力データを渡せばいいか。」
  • 「エラーハンドリングはしっかりやらないと。リポジトリのエラーはそのまま返すか。」
  • 「バリデーションはここでやるべきか?いや、ドメイン層でやった方がいいかも。」
  • 「この構造体なら、テストでリポジトリのモック使えるな。楽だ。」
  • 「将来的にログやメトリクスを追加するときも、ここに書けばいいか。」
  • 「これでアカウント作成の責任はこの構造体に閉じたな。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

追加のキモチ

  • 「プレゼンターをインタフェースにすることで、出力形式を柔軟に変えられるようにしよう。」
  • 「タイムアウトを設定することで、リソースの無駄遣いを防ごう。」
  • 「ドメイン層の Account を使うことで、ビジネスロジックを明確に分離しよう。」
  • 「入力データと出力データを別の構造体に分けることで、責務を明確にしよう。」
  • 「エラーハンドリングはシンプルに、でもしっかりと。エラーが発生したら即座に返そう。」
  • 「将来的に機能を追加するときも、この構造体を拡張すればいいか。」
  • 「これで、アカウント作成のユースケースが独立して動くようになったな。」
  • 「テストが書きやすくなったし、保守性も上がった。これで安心だ。」

func NewCreateAccountInteractorについて

func NewCreateAccountInteractor(
	repo domain.AccountRepository,
	presenter CreateAccountPresenter,
	t time.Duration,
) CreateAccountUseCase {
	return createAccountInteractor{
		repo:       repo,
		presenter:  presenter,
		ctxTimeout: t,
	}
}

なぜ戻り値がCreateAccountUseCase型指定なのにcreateAccountInteractor型を返しても良いのか?

なぜ func NewCreateAccountInteractor を定義するのか?

別の層にて、repo,presenter,tを使用してcreateAccountInteractorオブジェクトを生成するために必要な関数であるため。

テストコード(usecase/create_account_test.go)では以下のように使用される。

var uc = NewCreateAccountInteractor(tt.repository, tt.presenter, time.Second)

インフラ層(infrastructure/router/gin.go)ではこのように使われる

uc = usecase.NewCreateAccountInteractor(
				repository.NewAccountNoSQL(g.db),
				presenter.NewCreateAccountPresenter(),
				g.ctxTimeout,
			)

CreateAccountUseCase< i >

CreateAccountUseCase interface {
		Execute(context.Context, CreateAccountInput) (CreateAccountOutput, error)
}

対応部分は青枠部分
image.png

CreateAccountUseCase< i > とは?

2つ外側のインフラ層で、ユースケース層のNewCreateAccountInteractor関数を使ってCreateAccountUseCaseインスタンスを生成している。そのインスタンスの受け皿としてインターフェースを定義する。

CreateAccountUseCase< i > を定義するときのキモチ

  • 「ユースケースのインタフェースを定義しよう。CreateAccountUseCase にしよう。」
  • 「このインタフェースは、アカウント作成のユースケースを表すから、メソッド名は Execute にしよう。」
  • Execute の引数は、コンテキストと入力データ (CreateAccountInput) にしよう。これで必要な情報を渡せる。」
  • 「戻り値は、出力データ (CreateAccountOutput) とエラーにしよう。エラーは必ず返すようにする。」
  • 「インタフェースにすることで、実装を隠蔽できる。依存性逆転の原則を守れるな。」
  • 「これで、他の層(例えばコントローラー)がこのインタフェースに依存するようになる。実装の詳細を知らなくていいから、疎結合になる。」
  • 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
  • 「将来的に別の実装を追加するときも、このインタフェースを満たせばいい。拡張性が高まるな。」
  • 「このインタフェースがあることで、アカウント作成のユースケースが何をするのか、明確になる。」
  • 「入力と出力の構造体も一緒に定義しておこう。これで、ユースケースの入出力が明確になる。」
  • 「これで、アカウント作成のユースケースが独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「インタフェースを小さく保とう。単一責任の原則を守るためだ。」
  • CreateAccountInputCreateAccountOutput を別の構造体にすることで、入出力の責任を分離しよう。」
  • 「このインタフェースがあることで、ドメイン層とプレゼンテーション層の橋渡しができるな。」
  • 「将来的に他のユースケースも同じようにインタフェースで定義しよう。一貫性が出る。」
  • 「これで、アカウント作成のユースケースがフレームワークやDBに依存しなくなった。テストもしやすい。」
  • 「インタフェースを定義することで、チームメンバーとのコミュニケーションも楽になる。何が必要なのかが明確だから。」
  • 「これで、アカウント作成のユースケースがしっかりとカプセル化されたな。変更に強くなった。」

CreateAccountPresenter< i >

CreateAccountPresenter interface {
		Output(domain.Account) CreateAccountOutput
	}

対応部分は青枠部分
image.png

なぜ CreateAccountPresenter< i > を定義するのか?

1つ外側のアダプタ層で CreateAccountPresenter オブジェクトを定義するため、ユースケース層ではその受け皿としてインターフェースを定義する。

CreateAccountPresenter< i > を定義するときのキモチ

  • 「出力を整形する責任を持つプレゼンターが必要だな。CreateAccountPresenter にしよう。」
  • 「このインタフェースは、ドメイン層のデータをプレゼンテーション層に適した形に変換する役割だ。」
  • 「メソッド名は Output にしよう。ドメインの Account を受け取って、CreateAccountOutput を返す。」
  • 「インタフェースにすることで、出力形式を柔軟に変えられるようにしよう。JSON、XML、HTML など、必要に応じて実装を差し替えられる。」
  • 「これで、プレゼンテーション層がドメイン層に依存しなくなる。疎結合になるな。」
  • 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
  • 「将来的に出力形式を変更するときも、このインタフェースを満たす新しい実装を作ればいい。拡張性が高まる。」
  • 「このインタフェースがあることで、出力の責任が明確になる。ユースケース層はビジネスロジックに集中できる。」
  • CreateAccountOutput を返すことで、出力データの構造も明確になる。これでフロントエンドやAPIクライアントが使いやすくなる。」
  • 「これで、アカウント作成の出力が独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「プレゼンターのインタフェースを小さく保とう。単一責任の原則を守るためだ。」
  • 「ドメイン層の Account をそのまま返さず、CreateAccountOutput に変換することで、ドメイン層の詳細を隠蔽できる。」
  • 「このインタフェースがあることで、ユースケース層とプレゼンテーション層の橋渡しができるな。」
  • 「将来的に他のプレゼンターも同じようにインタフェースで定義しよう。一貫性が出る。」
  • 「これで、出力のロジックがユースケース層から分離された。ユースケース層はビジネスロジックに集中できる。」
  • 「インタフェースを定義することで、チームメンバーとのコミュニケーションも楽になる。何が必要なのかが明確だから。」
  • 「これで、出力の責任がしっかりとカプセル化されたな。変更に強くなった。」
  • 「出力形式を変更するときも、このインタフェースを満たす新しい実装を作ればいい。既存のコードに影響を与えない。」
  • 「これで、アカウント作成のユースケースが完全に独立したな。出力の責任も明確になった。」

adapter/api/action/create_account.go

package action

import (
	"encoding/json"
	"net/http"
	"strings"

	"github.com/gsabadini/go-clean-architecture/adapter/api/logging"
	"github.com/gsabadini/go-clean-architecture/adapter/api/response"
	"github.com/gsabadini/go-clean-architecture/adapter/logger"
	"github.com/gsabadini/go-clean-architecture/adapter/validator"
	"github.com/gsabadini/go-clean-architecture/usecase"
)

type CreateAccountAction struct {
	uc        usecase.CreateAccountUseCase
	log       logger.Logger
	validator validator.Validator
}

func NewCreateAccountAction(uc usecase.CreateAccountUseCase, log logger.Logger, v validator.Validator) CreateAccountAction {
	return CreateAccountAction{
		uc:        uc,
		log:       log,
		validator: v,
	}
}

func (a CreateAccountAction) Execute(w http.ResponseWriter, r *http.Request) {
	const logKey = "create_account"

	var input usecase.CreateAccountInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		logging.NewError(
			a.log,
			err,
			logKey,
			http.StatusBadRequest,
		).Log("error when decoding json")

		response.NewError(err, http.StatusBadRequest).Send(w)
		return
	}
	defer r.Body.Close()

	if errs := a.validateInput(input); len(errs) > 0 {
		logging.NewError(
			a.log,
			response.ErrInvalidInput,
			logKey,
			http.StatusBadRequest,
		).Log("invalid input")

		response.NewErrorMessage(errs, http.StatusBadRequest).Send(w)
		return
	}

	a.cleanCPF(input.CPF)

	output, err := a.uc.Execute(r.Context(), input)
	if err != nil {
		logging.NewError(
			a.log,
			err,
			logKey,
			http.StatusInternalServerError,
		).Log("error when creating a new account")

		response.NewError(err, http.StatusInternalServerError).Send(w)
		return
	}
	logging.NewInfo(a.log, logKey, http.StatusCreated).Log("success creating account")

	response.NewSuccess(output, http.StatusCreated).Send(w)
}

func (a CreateAccountAction) validateInput(input usecase.CreateAccountInput) []string {
	var msgs []string

	err := a.validator.Validate(input)
	if err != nil {
		for _, msg := range a.validator.Messages() {
			msgs = append(msgs, msg)
		}
	}

	return msgs
}

func (a CreateAccountAction) cleanCPF(cpf string) string {
	return strings.Replace(strings.Replace(cpf, ".", "", -1), "-", "", -1)
}

対応部分は赤枠部分
image.png

CreateAccountActionとは

インフラ層側からユースケースを実行することができるように用意された変換オブジェクト。

CreateAccountAction を定義するときのキモチ

  • 「HTTPリクエストを処理するためのアクションが必要だな。CreateAccountAction にしよう。」
  • 「このアクションは、ユースケースを呼び出してアカウントを作成する役割だ。」
  • 「ユースケース (CreateAccountUseCase) は絶対に必要だ。これがないとアカウントを作成できない。」
  • 「ログも必要だな。エラーや成功を記録するために。」
  • 「バリデーションも必要だ。入力データが正しいか確認しないと。」
  • 「コンストラクタを作って、依存関係を注入できるようにしよう。これでテストしやすくなる。」
  • 「メソッド名は Execute にしよう。HTTPリクエストを処理する責任を持たせる。」
  • 「リクエストボディをデコードして、入力データ (CreateAccountInput) に変換しよう。」
  • 「バリデーションエラーがあれば、即座にエラーレスポンスを返そう。無効なデータは処理しない。」
  • 「CPFのフォーマットを整える必要があるな。ドットやハイフンを削除しよう。」
  • 「ユースケースを呼び出して、アカウントを作成しよう。エラーがあればログに記録して、エラーレスポンスを返す。」
  • 「成功したら、ログに記録して成功レスポンスを返そう。」
  • 「これで、HTTP層の責任が明確になったな。リクエストを受け取り、ユースケースを呼び出し、レスポンスを返す。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「HTTP層の責任を明確に分離しよう。ビジネスロジックはユースケースに任せる。」
  • 「ログをしっかり記録することで、デバッグや監視がしやすくなる。」
  • 「バリデーションをここで行うことで、無効なデータがユースケースに渡るのを防ぐ。」
  • 「CPFのフォーマットを整えることで、ユースケース層で余計な処理をしなくて済む。」
  • 「エラーハンドリングはしっかりやろう。ユーザーにわかりやすいエラーメッセージを返す。」
  • 「成功レスポンスは、フロントエンドやAPIクライアントが使いやすい形にしよう。」
  • 「これで、アカウント作成のHTTPエンドポイントが完成したな。クリーンだ。」
  • 「テストのときも、モックを使いやすくなる。依存関係を注入しているから。」
  • 「将来的に機能を追加するときも、このアクションを拡張すればいいか。」
  • 「これで、アカウント作成のフローが一通り完成した。責任が分離されて、保守性も高い。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

NewCreateAccountActionとは

1つ外側のインフラ層側からCreateAccountActionオブジェクトを生成するために必要な関数。

adapter/validator/validator.go

package validator

type Validator interface {
	Validate(interface{}) error
	Messages() []string
}

対応部分は赤枠部分
image.png

Validator<i> を定義するときのキモチ

  • 「バリデーションのインタフェースが必要だな。Validator にしよう。」
  • 「このインタフェースは、入力データを検証する責任を持つ。」
  • 「メソッドは ValidateMessages にしよう。Validate で検証を行い、Messages でエラーメッセージを取得する。」
  • Validate の引数は interface{} にしよう。これでどんな型のデータでも検証できる。」
  • 「エラーメッセージはスライスで返そう。複数のエラーがある場合に対応するためだ。」
  • 「インタフェースにすることで、バリデーションの実装を柔軟に差し替えられるようにしよう。」
  • 「これで、バリデーションのロジックが特定の実装に依存しなくなる。疎結合になるな。」
  • 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
  • 「将来的にバリデーションのルールを変更するときも、このインタフェースを満たす新しい実装を作ればいい。拡張性が高まる。」
  • 「このインタフェースがあることで、バリデーションの責任が明確になる。ユースケース層やHTTP層は検証の詳細を知らなくていい。」
  • 「これで、バリデーションが独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「バリデーションのインタフェースを小さく保とう。単一責任の原則を守るためだ。」
  • Validate メソッドでエラーを返すことで、検証結果を簡単に確認できる。」
  • Messages メソッドでエラーメッセージを取得することで、ユーザーにわかりやすいフィードバックを提供できる。」
  • 「このインタフェースがあることで、バリデーションのロジックが再利用しやすくなる。」
  • 「将来的に他のバリデーションも同じようにインタフェースで定義しよう。一貫性が出る。」
  • 「これで、バリデーションの責任がしっかりとカプセル化されたな。変更に強くなった。」
  • 「バリデーションのルールを変更するときも、このインタフェースを満たす新しい実装を作ればいい。既存のコードに影響を与えない。」
  • 「これで、入力データの検証が独立したコンポーネントとして定義できた。保守性も高い。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

adapter/logger/logger.go

package logger

type Logger interface {
	Infof(format string, args ...interface{})
	Warnf(format string, args ...interface{})
	Errorf(format string, args ...interface{})
	Fatalln(args ...interface{})
	WithFields(keyValues Fields) Logger
	WithError(err error) Logger
}

type Fields map[string]interface{}

対応部分は赤枠部分
image.png

Logger<i> を定義するときのキモチ

  • 「ロギングのインタフェースが必要だな。Logger にしよう。」
  • 「このインタフェースは、アプリケーション全体でログを記録する責任を持つ。」
  • 「ログレベルごとにメソッドを分けよう。InfofWarnfErrorfFatalln を用意する。」
  • 「フォーマット付きのログ記録ができるように、InfofWarnfErrorfformat と可変長引数を受け取るようにしよう。」
  • 「致命的なエラーの場合は Fatalln を使おう。これでアプリケーションを終了させられる。」
  • 「追加のフィールドをログに含められるように、WithFields メソッドを用意しよう。これでコンテキストを追加できる。」
  • 「エラーをログに含められるように、WithError メソッドも用意しよう。これでエラーの詳細を記録できる。」
  • 「インタフェースにすることで、ロギングの実装を柔軟に差し替えられるようにしよう。」
  • 「これで、ロギングのロジックが特定の実装に依存しなくなる。疎結合になるな。」
  • 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
  • 「将来的にロギングの方法を変更するときも、このインタフェースを満たす新しい実装を作ればいい。拡張性が高まる。」
  • 「このインタフェースがあることで、ロギングの責任が明確になる。アプリケーションの他の部分はログの詳細を知らなくていい。」
  • 「これで、ロギングが独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「ログレベルを分けることで、重要度に応じたログ記録ができる。」
  • 「フォーマット付きのログ記録ができるようにすることで、柔軟なログ出力が可能になる。」
  • WithFields メソッドでコンテキストを追加できるようにすることで、デバッグや監視がしやすくなる。」
  • WithError メソッドでエラーをログに含められるようにすることで、エラーの追跡が容易になる。」
  • 「インタフェースを小さく保とう。単一責任の原則を守るためだ。」
  • 「このインタフェースがあることで、ロギングのロジックが再利用しやすくなる。」
  • 「将来的に他のロギングも同じようにインタフェースで定義しよう。一貫性が出る。」
  • 「これで、ロギングの責任がしっかりとカプセル化されたな。変更に強くなった。」
  • 「ロギングの方法を変更するときも、このインタフェースを満たす新しい実装を作ればいい。既存のコードに影響を与えない。」
  • 「これで、ロギングが独立したコンポーネントとして定義できた。保守性も高い。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

adapter/presenter/create_account.go

package presenter

import (
	"time"

	"github.com/gsabadini/go-clean-architecture/domain"
	"github.com/gsabadini/go-clean-architecture/usecase"
)

type createAccountPresenter struct{}

func NewCreateAccountPresenter() usecase.CreateAccountPresenter {
	return createAccountPresenter{}
}

func (a createAccountPresenter) Output(account domain.Account) usecase.CreateAccountOutput {
	return usecase.CreateAccountOutput{
		ID:        account.ID().String(),
		Name:      account.Name(),
		CPF:       account.CPF(),
		Balance:   account.Balance().Float64(),
		CreatedAt: account.CreatedAt().Format(time.RFC3339),
	}
}

対応部分は赤枠部分
image.png

createAccountPresenter を定義するときのキモチ

  • 「出力を整形するプレゼンターが必要だな。createAccountPresenter にしよう。」
  • 「このプレゼンターは、ドメイン層の Account をプレゼンテーション層に適した形に変換する役割だ。」
  • 「コンストラクタを作って、依存関係を注入できるようにしよう。これでテストしやすくなる。」
  • 「メソッド名は Output にしよう。ドメインの Account を受け取って、CreateAccountOutput を返す。」
  • Account の各フィールドを CreateAccountOutput にマッピングしよう。ID、Name、CPF、Balance、CreatedAt を変換する。」
  • 「ID は文字列に変換しよう。UUID をそのまま返すのは避ける。」
  • 「Balance は Float64 に変換しよう。金額は小数点以下も扱えるようにする。」
  • 「CreatedAt は time.RFC3339 形式にフォーマットしよう。日付の表示形式を統一する。」
  • 「これで、出力データの構造が明確になる。フロントエンドやAPIクライアントが使いやすくなる。」
  • 「プレゼンターの責任は出力の整形だけに限定しよう。ビジネスロジックは含めない。」
  • 「これで、アカウント作成の出力が独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「ドメイン層の Account をそのまま返さず、CreateAccountOutput に変換することで、ドメイン層の詳細を隠蔽できる。」
  • 「出力形式を柔軟に変えられるように、プレゼンターをインタフェースで定義しよう。」
  • 「これで、ユースケース層が出力形式に依存しなくなる。疎結合になるな。」
  • 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
  • 「将来的に出力形式を変更するときも、このプレゼンターを拡張すればいい。拡張性が高まる。」
  • 「出力の責任をプレゼンターに閉じ込めることで、ユースケース層はビジネスロジックに集中できる。」
  • 「これで、アカウント作成のフローが一通り完成した。責任が分離されて、保守性も高い。」
  • 「出力形式を変更するときも、このプレゼンターを修正すればいい。既存のコードに影響を与えない。」
  • 「これで、アカウント作成の出力が独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

infrastructure/router/gin.go

package router

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gsabadini/go-clean-architecture/adapter/api/action"
	"github.com/gsabadini/go-clean-architecture/adapter/logger"
	"github.com/gsabadini/go-clean-architecture/adapter/presenter"
	"github.com/gsabadini/go-clean-architecture/adapter/repository"
	"github.com/gsabadini/go-clean-architecture/adapter/validator"
	"github.com/gsabadini/go-clean-architecture/usecase"
)

type ginEngine struct {
	router     *gin.Engine
	log        logger.Logger
	db         repository.NoSQL
	validator  validator.Validator
	port       Port
	ctxTimeout time.Duration
}

func newGinServer(
	log logger.Logger,
	db repository.NoSQL,
	validator validator.Validator,
	port Port,
	t time.Duration,
) *ginEngine {
	return &ginEngine{
		router:     gin.New(),
		log:        log,
		db:         db,
		validator:  validator,
		port:       port,
		ctxTimeout: t,
	}
}

func (g ginEngine) Listen() {
	gin.SetMode(gin.ReleaseMode)
	gin.Recovery()

	g.setAppHandlers(g.router)

	server := &http.Server{
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 15 * time.Second,
		Addr:         fmt.Sprintf(":%d", g.port),
		Handler:      g.router,
	}

	stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		g.log.WithFields(logger.Fields{"port": g.port}).Infof("Starting HTTP Server")
		if err := server.ListenAndServe(); err != nil {
			g.log.WithError(err).Fatalln("Error starting HTTP server")
		}
	}()

	<-stop

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer func() {
		cancel()
	}()

	if err := server.Shutdown(ctx); err != nil {
		g.log.WithError(err).Fatalln("Server Shutdown Failed")
	}

	g.log.Infof("Service down")
}

/* TODO ADD MIDDLEWARE */
func (g ginEngine) setAppHandlers(router *gin.Engine) {
	router.POST("/v1/transfers", g.buildCreateTransferAction())
	router.GET("/v1/transfers", g.buildFindAllTransferAction())

	router.GET("/v1/accounts/:account_id/balance", g.buildFindBalanceAccountAction())
	router.POST("/v1/accounts", g.buildCreateAccountAction())
	router.GET("/v1/accounts", g.buildFindAllAccountAction())

	router.GET("/v1/health", g.healthcheck())
}

func (g ginEngine) buildCreateTransferAction() gin.HandlerFunc {
	return func(c *gin.Context) {
		var (
			uc = usecase.NewCreateTransferInteractor(
				repository.NewTransferNoSQL(g.db),
				repository.NewAccountNoSQL(g.db),
				presenter.NewCreateTransferPresenter(),
				g.ctxTimeout,
			)

			act = action.NewCreateTransferAction(uc, g.log, g.validator)
		)

		act.Execute(c.Writer, c.Request)
	}
}

func (g ginEngine) buildFindAllTransferAction() gin.HandlerFunc {
	return func(c *gin.Context) {
		var (
			uc = usecase.NewFindAllTransferInteractor(
				repository.NewTransferNoSQL(g.db),
				presenter.NewFindAllTransferPresenter(),
				g.ctxTimeout,
			)
			act = action.NewFindAllTransferAction(uc, g.log)
		)

		act.Execute(c.Writer, c.Request)
	}
}

func (g ginEngine) buildCreateAccountAction() gin.HandlerFunc {
	return func(c *gin.Context) {
		var (
			uc = usecase.NewCreateAccountInteractor(
				repository.NewAccountNoSQL(g.db),
				presenter.NewCreateAccountPresenter(),
				g.ctxTimeout,
			)
			act = action.NewCreateAccountAction(uc, g.log, g.validator)
		)

		act.Execute(c.Writer, c.Request)
	}
}

func (g ginEngine) buildFindAllAccountAction() gin.HandlerFunc {
	return func(c *gin.Context) {
		var (
			uc = usecase.NewFindAllAccountInteractor(
				repository.NewAccountNoSQL(g.db),
				presenter.NewFindAllAccountPresenter(),
				g.ctxTimeout,
			)
			act = action.NewFindAllAccountAction(uc, g.log)
		)

		act.Execute(c.Writer, c.Request)
	}
}

func (g ginEngine) buildFindBalanceAccountAction() gin.HandlerFunc {
	return func(c *gin.Context) {
		var (
			uc = usecase.NewFindBalanceAccountInteractor(
				repository.NewAccountNoSQL(g.db),
				presenter.NewFindAccountBalancePresenter(),
				g.ctxTimeout,
			)
			act = action.NewFindAccountBalanceAction(uc, g.log)
		)

		q := c.Request.URL.Query()
		q.Add("account_id", c.Param("account_id"))
		c.Request.URL.RawQuery = q.Encode()

		act.Execute(c.Writer, c.Request)
	}
}

func (g ginEngine) healthcheck() gin.HandlerFunc {
	return func(c *gin.Context) {
		action.HealthCheck(c.Writer, c.Request)
	}
}

対応部分は赤枠部分
image.png

infrastructure/router/gin.go を定義するときのキモチ

  • 「HTTPリクエストを処理するためのルーターが必要だな。ginEngine にしよう。」
  • 「このルーターは、Ginフレームワークを使ってHTTPリクエストを処理する。」
  • 「ルーターの初期化は newGinServer 関数で行おう。ロガー、データベース、バリデーター、ポート、タイムアウトを受け取る。」
  • Listen メソッドでサーバーを起動しよう。HTTPサーバーの設定を行い、シグナルを受け取ってシャットダウンもできるようにする。」
  • setAppHandlers メソッドでルーティングを設定しよう。各エンドポイントに対応するアクションを紐付ける。」
  • buildCreateTransferActionbuildFindAllTransferActionbuildCreateAccountActionbuildFindAllAccountActionbuildFindBalanceAccountAction メソッドで各ユースケースをビルドしよう。依存関係を注入してアクションを作成する。」
  • healthcheck メソッドでヘルスチェックエンドポイントを設定しよう。サーバーの状態を確認できるようにする。」
  • 「これで、HTTPリクエストの処理が独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「Ginフレームワークを使うことで、HTTPリクエストの処理を簡単に実装できる。」
  • 「ルーターの初期化関数で依存関係を注入することで、テストしやすくなる。」
  • Listen メソッドでサーバーの起動とシャットダウンを制御することで、リソースの管理がしやすくなる。」
  • setAppHandlers メソッドでルーティングを設定することで、エンドポイントの追加や変更が容易になる。」
  • 「各アクションをビルドするメソッドを用意することで、依存関係の注入が簡単になる。」
  • 「ヘルスチェックエンドポイントを設定することで、サーバーの状態を監視しやすくなる。」
  • 「これで、HTTPリクエストの処理が独立したコンポーネントとして定義できた。保守性も高い。」
  • 「将来的にエンドポイントを追加するときも、このルーターを拡張すればいい。拡張性が高まる。」
  • 「これで、HTTPリクエストの処理が抽象化された。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

infrastructure/router/gorilla_mux.go

package router

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gsabadini/go-clean-architecture/adapter/api/action"
	"github.com/gsabadini/go-clean-architecture/adapter/api/middleware"
	"github.com/gsabadini/go-clean-architecture/adapter/logger"
	"github.com/gsabadini/go-clean-architecture/adapter/presenter"
	"github.com/gsabadini/go-clean-architecture/adapter/repository"
	"github.com/gsabadini/go-clean-architecture/adapter/validator"
	"github.com/gsabadini/go-clean-architecture/usecase"

	"github.com/gorilla/mux"
	"github.com/urfave/negroni"
)

type gorillaMux struct {
	router     *mux.Router
	middleware *negroni.Negroni
	log        logger.Logger
	db         repository.SQL
	validator  validator.Validator
	port       Port
	ctxTimeout time.Duration
}

func newGorillaMux(
	log logger.Logger,
	db repository.SQL,
	validator validator.Validator,
	port Port,
	t time.Duration,
) *gorillaMux {
	return &gorillaMux{
		router:     mux.NewRouter(),
		middleware: negroni.New(),
		log:        log,
		db:         db,
		validator:  validator,
		port:       port,
		ctxTimeout: t,
	}
}

func (g gorillaMux) Listen() {
	g.setAppHandlers(g.router)
	g.middleware.UseHandler(g.router)

	server := &http.Server{
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 15 * time.Second,
		Addr:         fmt.Sprintf(":%d", g.port),
		Handler:      g.middleware,
	}

	stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		g.log.WithFields(logger.Fields{"port": g.port}).Infof("Starting HTTP Server")
		if err := server.ListenAndServe(); err != nil {
			g.log.WithError(err).Fatalln("Error starting HTTP server")
		}
	}()

	<-stop

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer func() {
		cancel()
	}()

	if err := server.Shutdown(ctx); err != nil {
		g.log.WithError(err).Fatalln("Server Shutdown Failed")
	}

	g.log.Infof("Service down")
}

func (g gorillaMux) setAppHandlers(router *mux.Router) {
	api := router.PathPrefix("/v1").Subrouter()

	api.Handle("/transfers", g.buildCreateTransferAction()).Methods(http.MethodPost)
	api.Handle("/transfers", g.buildFindAllTransferAction()).Methods(http.MethodGet)

	api.Handle("/accounts/{account_id}/balance", g.buildFindBalanceAccountAction()).Methods(http.MethodGet)
	api.Handle("/accounts", g.buildCreateAccountAction()).Methods(http.MethodPost)
	api.Handle("/accounts", g.buildFindAllAccountAction()).Methods(http.MethodGet)

	api.HandleFunc("/health", action.HealthCheck).Methods(http.MethodGet)
}

func (g gorillaMux) buildCreateTransferAction() *negroni.Negroni {
	var handler http.HandlerFunc = func(res http.ResponseWriter, req *http.Request) {
		var (
			uc = usecase.NewCreateTransferInteractor(
				repository.NewTransferSQL(g.db),
				repository.NewAccountSQL(g.db),
				presenter.NewCreateTransferPresenter(),
				g.ctxTimeout,
			)
			act = action.NewCreateTransferAction(uc, g.log, g.validator)
		)

		act.Execute(res, req)
	}

	return negroni.New(
		negroni.HandlerFunc(middleware.NewLogger(g.log).Execute),
		negroni.NewRecovery(),
		negroni.Wrap(handler),
	)
}

func (g gorillaMux) buildFindAllTransferAction() *negroni.Negroni {
	var handler http.HandlerFunc = func(res http.ResponseWriter, req *http.Request) {
		var (
			uc = usecase.NewFindAllTransferInteractor(
				repository.NewTransferSQL(g.db),
				presenter.NewFindAllTransferPresenter(),
				g.ctxTimeout,
			)
			act = action.NewFindAllTransferAction(uc, g.log)
		)

		act.Execute(res, req)
	}

	return negroni.New(
		negroni.HandlerFunc(middleware.NewLogger(g.log).Execute),
		negroni.NewRecovery(),
		negroni.Wrap(handler),
	)
}

func (g gorillaMux) buildCreateAccountAction() *negroni.Negroni {
	var handler http.HandlerFunc = func(res http.ResponseWriter, req *http.Request) {
		var (
			uc = usecase.NewCreateAccountInteractor(
				repository.NewAccountSQL(g.db),
				presenter.NewCreateAccountPresenter(),
				g.ctxTimeout,
			)
			act = action.NewCreateAccountAction(uc, g.log, g.validator)
		)

		act.Execute(res, req)
	}

	return negroni.New(
		negroni.HandlerFunc(middleware.NewLogger(g.log).Execute),
		negroni.NewRecovery(),
		negroni.Wrap(handler),
	)
}

func (g gorillaMux) buildFindAllAccountAction() *negroni.Negroni {
	var handler http.HandlerFunc = func(res http.ResponseWriter, req *http.Request) {
		var (
			uc = usecase.NewFindAllAccountInteractor(
				repository.NewAccountSQL(g.db),
				presenter.NewFindAllAccountPresenter(),
				g.ctxTimeout,
			)
			act = action.NewFindAllAccountAction(uc, g.log)
		)

		act.Execute(res, req)
	}

	return negroni.New(
		negroni.HandlerFunc(middleware.NewLogger(g.log).Execute),
		negroni.NewRecovery(),
		negroni.Wrap(handler),
	)
}

func (g gorillaMux) buildFindBalanceAccountAction() *negroni.Negroni {
	var handler http.HandlerFunc = func(res http.ResponseWriter, req *http.Request) {
		var (
			uc = usecase.NewFindBalanceAccountInteractor(
				repository.NewAccountSQL(g.db),
				presenter.NewFindAccountBalancePresenter(),
				g.ctxTimeout,
			)
			act = action.NewFindAccountBalanceAction(uc, g.log)
		)

		var (
			vars = mux.Vars(req)
			q    = req.URL.Query()
		)

		q.Add("account_id", vars["account_id"])
		req.URL.RawQuery = q.Encode()

		act.Execute(res, req)
	}

	return negroni.New(
		negroni.HandlerFunc(middleware.NewLogger(g.log).Execute),
		negroni.NewRecovery(),
		negroni.Wrap(handler),
	)
}

対応部分は赤枠部分
image.png

infrastructure/router/gorilla_mux.go を定義するときのキモチ

  • 「HTTPリクエストを処理するためのルーターが必要だな。gorillaMux にしよう。」
  • 「このルーターは、Gorilla Muxフレームワークを使ってHTTPリクエストを処理する。」
  • 「ルーターの初期化は newGorillaMux 関数で行おう。ロガー、データベース、バリデーター、ポート、タイムアウトを受け取る。」
  • Listen メソッドでサーバーを起動しよう。HTTPサーバーの設定を行い、シグナルを受け取ってシャットダウンもできるようにする。」
  • setAppHandlers メソッドでルーティングを設定しよう。各エンドポイントに対応するアクションを紐付ける。」
  • buildCreateTransferActionbuildFindAllTransferActionbuildCreateAccountActionbuildFindAllAccountActionbuildFindBalanceAccountAction メソッドで各ユースケースをビルドしよう。依存関係を注入してアクションを作成する。」
  • healthcheck メソッドでヘルスチェックエンドポイントを設定しよう。サーバーの状態を確認できるようにする。」
  • 「これで、HTTPリクエストの処理が独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「Gorilla Muxフレームワークを使うことで、HTTPリクエストの処理を簡単に実装できる。」
  • 「ルーターの初期化関数で依存関係を注入することで、テストしやすくなる。」
  • Listen メソッドでサーバーの起動とシャットダウンを制御することで、リソースの管理がしやすくなる。」
  • setAppHandlers メソッドでルーティングを設定することで、エンドポイントの追加や変更が容易になる。」
  • 「各アクションをビルドするメソッドを用意することで、依存関係の注入が簡単になる。」
  • 「ヘルスチェックエンドポイントを設定することで、サーバーの状態を監視しやすくなる。」
  • 「これで、HTTPリクエストの処理が独立したコンポーネントとして定義できた。保守性も高い。」
  • 「将来的にエンドポイントを追加するときも、このルーターを拡張すればいい。拡張性が高まる。」
  • 「これで、HTTPリクエストの処理が抽象化された。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

infrastructure/validation/go_playground.go

package validation

import (
	"errors"

	"github.com/gsabadini/go-clean-architecture/adapter/validator"

	"github.com/go-playground/locales/en"
	ut "github.com/go-playground/universal-translator"
	go_playground "github.com/go-playground/validator/v10"
	en_translations "github.com/go-playground/validator/v10/translations/en"
)

type goPlayground struct {
	validator *go_playground.Validate
	translate ut.Translator
	err       error
	msg       []string
}

func NewGoPlayground() (validator.Validator, error) {
	var (
		language         = en.New()
		uni              = ut.New(language, language)
		translate, found = uni.GetTranslator("en")
	)

	if !found {
		return nil, errors.New("translator not found")
	}

	v := go_playground.New()
	if err := en_translations.RegisterDefaultTranslations(v, translate); err != nil {
		return nil, errors.New("translator not found")
	}

	return &goPlayground{validator: v, translate: translate}, nil
}

func (g *goPlayground) Validate(i interface{}) error {
	if len(g.msg) > 0 {
		g.msg = nil
	}

	g.err = g.validator.Struct(i)
	if g.err != nil {
		return g.err
	}

	return nil
}

func (g *goPlayground) Messages() []string {
	if g.err != nil {
		for _, err := range g.err.(go_playground.ValidationErrors) {
			g.msg = append(g.msg, err.Translate(g.translate))
		}
	}

	return g.msg
}

対応部分は赤枠部分
image.png

infrastructure/validation/go_playground.go を定義するときのキモチ

  • 「バリデーションの実装が必要だな。goPlayground にしよう。」
  • 「このバリデーションは、Go Playgroundの validator パッケージを使って入力データを検証する。」
  • NewGoPlayground 関数でバリデーションを初期化しよう。英語の翻訳も設定する。」
  • Validate メソッドで入力データを検証しよう。エラーがあれば返す。」
  • Messages メソッドでエラーメッセージを取得しよう。翻訳されたメッセージを返す。」
  • 「これで、入力データの検証が独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「Go Playgroundの validator パッケージを使うことで、入力データの検証を簡単に実装できる。」
  • 「英語の翻訳を設定することで、エラーメッセージをユーザーにわかりやすく表示できる。」
  • Validate メソッドで入力データを検証することで、無効なデータがシステムに渡るのを防ぐ。」
  • Messages メソッドでエラーメッセージを取得することで、ユーザーにフィードバックを提供できる。」
  • 「これで、入力データの検証が独立したコンポーネントとして定義できた。保守性も高い。」
  • 「将来的に別のバリデーションパッケージを使うときも、このコンポーネントを拡張すればいい。拡張性が高まる。」
  • 「これで、入力データの検証が抽象化された。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

infrastructure/log/zap.go

package log

import (
	"github.com/gsabadini/go-clean-architecture/adapter/logger"
	"go.uber.org/zap"
)

type zapLogger struct {
	logger *zap.SugaredLogger
}

func NewZapLogger() (logger.Logger, error) {
	log, err := zap.NewProduction()
	if err != nil {
		return nil, err
	}

	sugar := log.Sugar()
	defer log.Sync()

	return &zapLogger{logger: sugar}, nil
}

func (l *zapLogger) Infof(format string, args ...interface{}) {
	l.logger.Infof(format, args...)
}

func (l *zapLogger) Warnf(format string, args ...interface{}) {
	l.logger.Warnf(format, args...)
}

func (l *zapLogger) Errorf(format string, args ...interface{}) {
	l.logger.Errorf(format, args...)
}

func (l *zapLogger) Fatalln(args ...interface{}) {
	l.logger.Fatal(args)
}

func (l *zapLogger) WithFields(fields logger.Fields) logger.Logger {
	var f = make([]interface{}, 0)
	for index, field := range fields {
		f = append(f, index)
		f = append(f, field)
	}

	log := l.logger.With(f...)
	return &zapLogger{logger: log}
}

func (l *zapLogger) WithError(err error) logger.Logger {
	var log = l.logger.With(err.Error())
	return &zapLogger{logger: log}
}

対応部分は赤枠部分
image.png

infrastructure/log/zap.go を定義するときのキモチ

  • 「ロギングの実装が必要だな。zapLogger にしよう。」
  • 「このロガーは、Zapパッケージを使ってログを記録する。」
  • NewZapLogger 関数でロガーを初期化しよう。Zapのプロダクション設定を使う。」
  • InfofWarnfErrorfFatalln メソッドでログを記録しよう。フォーマット付きのログ記録ができるようにする。」
  • WithFields メソッドで追加のフィールドをログに含められるようにしよう。これでコンテキストを追加できる。」
  • WithError メソッドでエラーをログに含められるようにしよう。これでエラーの詳細を記録できる。」
  • 「これで、ロギングが独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「Zapパッケージを使うことで、高速で構造化されたログ記録ができる。」
  • NewZapLogger 関数でロガーを初期化することで、ログの設定を一元管理できる。」
  • InfofWarnfErrorfFatalln メソッドを用意することで、ログレベルごとの記録ができる。」
  • WithFields メソッドで追加のフィールドをログに含められるようにすることで、デバッグや監視がしやすくなる。」
  • WithError メソッドでエラーをログに含められるようにすることで、エラーの追跡が容易になる。」
  • 「これで、ロギングが独立したコンポーネントとして定義できた。保守性も高い。」
  • 「将来的に別のロギングパッケージを使うときも、このコンポーネントを拡張すればいい。拡張性が高まる。」
  • 「これで、ロギングが抽象化された。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

infrastructure/log/logrus.go

package log

import (
	"github.com/gsabadini/go-clean-architecture/adapter/logger"
	"github.com/sirupsen/logrus"
)

type logrusLogger struct {
	logger *logrus.Logger
}

func NewLogrusLogger() logger.Logger {
	log := logrus.New()
	log.SetFormatter(&logrus.JSONFormatter{
		TimestampFormat: "2006-01-02 15:04:05",
	})

	return &logrusLogger{logger: log}
}

func (l *logrusLogger) Infof(format string, args ...interface{}) {
	l.logger.Infof(format, args...)
}

func (l *logrusLogger) Warnf(format string, args ...interface{}) {
	l.logger.Warnf(format, args...)
}

func (l *logrusLogger) Errorf(format string, args ...interface{}) {
	l.logger.Errorf(format, args...)
}

func (l *logrusLogger) Fatalln(args ...interface{}) {
	l.logger.Fatalln(args...)
}

func (l *logrusLogger) WithFields(fields logger.Fields) logger.Logger {
	return &logrusLogEntry{
		entry: l.logger.WithFields(convertToLogrusFields(fields)),
	}
}

func (l *logrusLogger) WithError(err error) logger.Logger {
	return &logrusLogEntry{
		entry: l.logger.WithError(err),
	}
}

type logrusLogEntry struct {
	entry *logrus.Entry
}

func (l *logrusLogEntry) Infof(format string, args ...interface{}) {
	l.entry.Infof(format, args...)
}

func (l *logrusLogEntry) Warnf(format string, args ...interface{}) {
	l.entry.Warnf(format, args...)
}

func (l *logrusLogEntry) Errorf(format string, args ...interface{}) {
	l.entry.Errorf(format, args...)
}

func (l *logrusLogEntry) Fatalln(args ...interface{}) {
	l.entry.Fatalln(args...)
}

func (l *logrusLogEntry) WithFields(fields logger.Fields) logger.Logger {
	return &logrusLogEntry{
		entry: l.entry.WithFields(convertToLogrusFields(fields)),
	}
}

func (l *logrusLogEntry) WithError(err error) logger.Logger {
	return &logrusLogEntry{
		entry: l.entry.WithError(err),
	}
}

func convertToLogrusFields(fields logger.Fields) logrus.Fields {
	logrusFields := logrus.Fields{}
	for index, field := range fields {
		logrusFields[index] = field
	}

	return logrusFields
}

対応部分は赤枠部分
image.png

infrastructure/log/logrus.go を定義するときのキモチ

  • 「ロギングの実装が必要だな。logrusLogger にしよう。」
  • 「このロガーは、Logrusパッケージを使ってログを記録する。」
  • NewLogrusLogger 関数でロガーを初期化しよう。LogrusのJSONフォーマットを使う。」
  • InfofWarnfErrorfFatalln メソッドでログを記録しよう。フォーマット付きのログ記録ができるようにする。」
  • WithFields メソッドで追加のフィールドをログに含められるようにしよう。これでコンテキストを追加できる。」
  • WithError メソッドでエラーをログに含められるようにしよう。これでエラーの詳細を記録できる。」
  • logrusLogEntry 構造体も定義しよう。ログエントリの操作を担当する。」
  • 「これで、ロギングが独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「Logrusパッケージを使うことで、柔軟で構造化されたログ記録ができる。」
  • NewLogrusLogger 関数でロガーを初期化することで、ログの設定を一元管理できる。」
  • InfofWarnfErrorfFatalln メソッドを用意することで、ログレベルごとの記録ができる。」
  • WithFields メソッドで追加のフィールドをログに含められるようにすることで、デバッグや監視がしやすくなる。」
  • WithError メソッドでエラーをログに含められるようにすることで、エラーの追跡が容易になる。」
  • logrusLogEntry 構造体を定義することで、ログエントリの操作を抽象化する。」
  • 「これで、ロギングが独立したコンポーネントとして定義できた。保守性も高い。」
  • 「将来的に別のロギングパッケージを使うときも、このコンポーネントを拡張すればいい。拡張性が高まる。」
  • 「これで、ロギングが抽象化された。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

adapter/repository/account_mongodb.go

package repository

import (
	"context"
	"time"

	"github.com/gsabadini/go-clean-architecture/domain"
	"github.com/pkg/errors"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
)

type accountBSON struct {
	ID        string    `bson:"id"`
	Name      string    `bson:"name"`
	CPF       string    `bson:"cpf"`
	Balance   int64     `bson:"balance"`
	CreatedAt time.Time `bson:"created_at"`
}

type AccountNoSQL struct {
	collectionName string
	db             NoSQL
}

func NewAccountNoSQL(db NoSQL) AccountNoSQL {
	return AccountNoSQL{
		db:             db,
		collectionName: "accounts",
	}
}

func (a AccountNoSQL) Create(ctx context.Context, account domain.Account) (domain.Account, error) {
	var accountBSON = accountBSON{
		ID:        account.ID().String(),
		Name:      account.Name(),
		CPF:       account.CPF(),
		Balance:   account.Balance().Int64(),
		CreatedAt: account.CreatedAt(),
	}

	if err := a.db.Store(ctx, a.collectionName, accountBSON); err != nil {
		return domain.Account{}, errors.Wrap(err, "error creating account")
	}

	return account, nil
}

func (a AccountNoSQL) UpdateBalance(ctx context.Context, ID domain.AccountID, balance domain.Money) error {
	var (
		query  = bson.M{"id": ID}
		update = bson.M{"$set": bson.M{"balance": balance}}
	)

	if err := a.db.Update(ctx, a.collectionName, query, update); err != nil {
		switch err {
		case mongo.ErrNilDocument:
			return errors.Wrap(domain.ErrAccountNotFound, "error updating account balance")
		default:
			return errors.Wrap(err, "error updating account balance")
		}
	}

	return nil
}

func (a AccountNoSQL) FindAll(ctx context.Context) ([]domain.Account, error) {
	var accountsBSON = make([]accountBSON, 0)

	if err := a.db.FindAll(ctx, a.collectionName, bson.M{}, &accountsBSON); err != nil {
		switch err {
		case mongo.ErrNilDocument:
			return []domain.Account{}, errors.Wrap(domain.ErrAccountNotFound, "error listing accounts")
		default:
			return []domain.Account{}, errors.Wrap(err, "error listing accounts")
		}
	}

	var accounts = make([]domain.Account, 0)

	for _, accountBSON := range accountsBSON {
		var account = domain.NewAccount(
			domain.AccountID(accountBSON.ID),
			accountBSON.Name,
			accountBSON.CPF,
			domain.Money(accountBSON.Balance),
			accountBSON.CreatedAt,
		)

		accounts = append(accounts, account)
	}

	return accounts, nil
}

func (a AccountNoSQL) FindByID(ctx context.Context, ID domain.AccountID) (domain.Account, error) {
	var (
		accountBSON = &accountBSON{}
		query       = bson.M{"id": ID}
	)

	if err := a.db.FindOne(ctx, a.collectionName, query, nil, accountBSON); err != nil {
		switch err {
		case mongo.ErrNoDocuments:
			return domain.Account{}, domain.ErrAccountNotFound
		default:
			return domain.Account{}, errors.Wrap(err, "error fetching account")
		}
	}

	return domain.NewAccount(
		domain.AccountID(accountBSON.ID),
		accountBSON.Name,
		accountBSON.CPF,
		domain.Money(accountBSON.Balance),
		accountBSON.CreatedAt,
	), nil
}

func (a AccountNoSQL) FindBalance(ctx context.Context, ID domain.AccountID) (domain.Account, error) {
	var (
		accountBSON = &accountBSON{}
		query       = bson.M{"id": ID}
		projection  = bson.M{"balance": 1, "_id": 0}
	)

	if err := a.db.FindOne(ctx, a.collectionName, query, projection, accountBSON); err != nil {
		switch err {
		case mongo.ErrNoDocuments:
			return domain.Account{}, domain.ErrAccountNotFound
		default:
			return domain.Account{}, errors.Wrap(err, "error fetching account balance")
		}
	}

	return domain.NewAccountBalance(domain.Money(accountBSON.Balance)), nil
}

対応部分は赤枠部分
image.png

NoSQL側のAccountRepositoryの実装

AccountNoSQLオブジェクトはAccountRepositoryインタフェースを暗黙に実装している。
以下の記事を参照。

AccountNoSQL を定義するときのキモチ

  • 「MongoDBを使ったアカウントのリポジトリが必要だな。AccountNoSQL にしよう。」
  • 「このリポジトリは、アカウントの永続化と取得を担当する。」
  • 「MongoDBのコレクション名は accounts にしよう。これでデータを整理しやすくなる。」
  • 「ドメイン層の Account を MongoDB に保存するために、accountBSON 構造体を定義しよう。」
  • accountBSON は MongoDB のドキュメント形式に合わせる。ID、Name、CPF、Balance、CreatedAt をフィールドに持つ。」
  • Create メソッドでアカウントを作成しよう。ドメイン層の AccountaccountBSON に変換して保存する。」
  • UpdateBalance メソッドでアカウントの残高を更新しよう。MongoDBの $set を使って更新する。」
  • FindAll メソッドで全アカウントを取得しよう。MongoDBから取得したデータをドメイン層の Account に変換する。」
  • FindByID メソッドで特定のアカウントを取得しよう。IDで検索して、ドメイン層の Account に変換する。」
  • FindBalance メソッドでアカウントの残高を取得しよう。プロジェクションを使って必要なフィールドだけ取得する。」
  • 「エラーハンドリングはしっかりやろう。MongoDBのエラーをドメイン層のエラーに変換する。」
  • 「これで、アカウントの永続化と取得が MongoDB を使って実装できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「ドメイン層の Account を MongoDB のドキュメント形式に変換することで、データの整合性を保つ。」
  • 「MongoDBの操作を抽象化した NoSQL インタフェースを使うことで、データベースの実装に依存しないようにする。」
  • 「エラーハンドリングをしっかり行うことで、ユーザーにわかりやすいエラーメッセージを返す。」
  • FindBalance メソッドでプロジェクションを使うことで、必要なデータだけ取得してパフォーマンスを向上させる。」
  • 「これで、アカウントの永続化と取得が独立したコンポーネントとして定義できた。保守性も高い。」
  • 「将来的にデータベースを変更するときも、このリポジトリを拡張すればいい。拡張性が高まる。」
  • 「これで、アカウントのデータアクセスが MongoDB を使って実装できた。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

adapter/repository/account_postgres.go

package repository

import (
	"context"
	"database/sql"
	"time"

	"github.com/gsabadini/go-clean-architecture/domain"
	"github.com/pkg/errors"
)

type AccountSQL struct {
	db SQL
}

func NewAccountSQL(db SQL) AccountSQL {
	return AccountSQL{
		db: db,
	}
}

func (a AccountSQL) Create(ctx context.Context, account domain.Account) (domain.Account, error) {
	var query = `
		INSERT INTO 
			accounts (id, name, cpf, balance, created_at)
		VALUES 
			($1, $2, $3, $4, $5)
	`

	if err := a.db.ExecuteContext(
		ctx,
		query,
		account.ID(),
		account.Name(),
		account.CPF(),
		account.Balance(),
		account.CreatedAt(),
	); err != nil {
		return domain.Account{}, errors.Wrap(err, "error creating account")
	}

	return account, nil
}

func (a AccountSQL) UpdateBalance(ctx context.Context, ID domain.AccountID, balance domain.Money) error {
	tx, ok := ctx.Value("TransactionContextKey").(Tx)
	if !ok {
		var err error
		tx, err = a.db.BeginTx(ctx)
		if err != nil {
			return errors.Wrap(err, "error updating account balance")
		}
	}

	query := "UPDATE accounts SET balance = $1 WHERE id = $2"

	if err := tx.ExecuteContext(ctx, query, balance, ID); err != nil {
		return errors.Wrap(err, "error updating account balance")
	}

	return nil
}

func (a AccountSQL) FindAll(ctx context.Context) ([]domain.Account, error) {
	var query = "SELECT * FROM accounts"

	rows, err := a.db.QueryContext(ctx, query)
	if err != nil {
		return []domain.Account{}, errors.Wrap(err, "error listing accounts")
	}

	var accounts = make([]domain.Account, 0)
	for rows.Next() {
		var (
			ID        string
			name      string
			CPF       string
			balance   int64
			createdAt time.Time
		)

		if err = rows.Scan(&ID, &name, &CPF, &balance, &createdAt); err != nil {
			return []domain.Account{}, errors.Wrap(err, "error listing accounts")
		}

		accounts = append(accounts, domain.NewAccount(
			domain.AccountID(ID),
			name,
			CPF,
			domain.Money(balance),
			createdAt,
		))
	}
	defer rows.Close()

	if err = rows.Err(); err != nil {
		return []domain.Account{}, err
	}

	return accounts, nil
}

func (a AccountSQL) FindByID(ctx context.Context, ID domain.AccountID) (domain.Account, error) {
	tx, ok := ctx.Value("TransactionContextKey").(Tx)
	if !ok {
		var err error
		tx, err = a.db.BeginTx(ctx)
		if err != nil {
			return domain.Account{}, errors.Wrap(err, "error find account by id")
		}
	}

	var (
		query     = "SELECT * FROM accounts WHERE id = $1 LIMIT 1 FOR NO KEY UPDATE"
		id        string
		name      string
		CPF       string
		balance   int64
		createdAt time.Time
	)

	err := tx.QueryRowContext(ctx, query, ID).Scan(&id, &name, &CPF, &balance, &createdAt)
	switch {
	case err == sql.ErrNoRows:
		return domain.Account{}, domain.ErrAccountNotFound
	default:
		return domain.NewAccount(
			domain.AccountID(id),
			name,
			CPF,
			domain.Money(balance),
			createdAt,
		), err
	}
}

func (a AccountSQL) FindBalance(ctx context.Context, ID domain.AccountID) (domain.Account, error) {
	var (
		query   = "SELECT balance FROM accounts WHERE id = $1"
		balance int64
	)

	err := a.db.QueryRowContext(ctx, query, ID).Scan(&balance)
	switch {
	case err == sql.ErrNoRows:
		return domain.Account{}, domain.ErrAccountNotFound
	default:
		return domain.NewAccountBalance(domain.Money(balance)), err
	}
}

対応部分は赤枠部分
image.png

SQL側のAccountRepositoryの実装

AccountNoSQLオブジェクトはAccountRepositoryインタフェースを暗黙に実装している。
以下の記事を参照。

AccountNoSQL を定義するときのキモチ

  • 「PostgreSQLを使ったアカウントのリポジトリが必要だな。AccountSQL にしよう。」
  • 「このリポジトリは、アカウントの永続化と取得を担当する。」
  • 「SQLクエリを使ってアカウントのデータを操作する。INSERTUPDATESELECT を使う。」
  • Create メソッドでアカウントを作成しよう。ドメイン層の Account を SQL に変換して保存する。」
  • UpdateBalance メソッドでアカウントの残高を更新しよう。トランザクションを使ってデータの整合性を保つ。」
  • FindAll メソッドで全アカウントを取得しよう。SQLクエリで取得したデータをドメイン層の Account に変換する。」
  • FindByID メソッドで特定のアカウントを取得しよう。IDで検索して、ドメイン層の Account に変換する。」
  • FindBalance メソッドでアカウントの残高を取得しよう。必要なフィールドだけ取得する。」
  • 「エラーハンドリングはしっかりやろう。SQLのエラーをドメイン層のエラーに変換する。」
  • 「これで、アカウントの永続化と取得が PostgreSQL を使って実装できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「ドメイン層の Account を SQL のテーブル形式に変換することで、データの整合性を保つ。」
  • 「SQLの操作を抽象化した SQL インタフェースを使うことで、データベースの実装に依存しないようにする。」
  • 「トランザクションを使ってデータの整合性を保つ。特に UpdateBalance では重要だ。」
  • 「エラーハンドリングをしっかり行うことで、ユーザーにわかりやすいエラーメッセージを返す。」
  • FindBalance メソッドで必要なフィールドだけ取得することで、パフォーマンスを向上させる。」
  • 「これで、アカウントの永続化と取得が独立したコンポーネントとして定義できた。保守性も高い。」
  • 「将来的にデータベースを変更するときも、このリポジトリを拡張すればいい。拡張性が高まる。」
  • 「これで、アカウントのデータアクセスが PostgreSQL を使って実装できた。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

adapter/repository/nosql.go

package repository

import "context"

type NoSQL interface {
	Store(context.Context, string, interface{}) error
	Update(context.Context, string, interface{}, interface{}) error
	FindAll(context.Context, string, interface{}, interface{}) error
	FindOne(context.Context, string, interface{}, interface{}, interface{}) error
	StartSession() (Session, error)
}

type Session interface {
	WithTransaction(context.Context, func(context.Context) error) error
	EndSession(context.Context)
}

対応部分は赤枠部分
image.png

なぜ NoSQL< i > を定義するのか?

インフラ層での特定のNoSQLの実装の受け皿を用意するためであり、adapter/repository/account_mongodb.goのAccountSQLオブジェクトが所有する。

NoSQL< i > を定義するときのキモチ

  • 「NoSQLデータベースの操作を抽象化するインタフェースが必要だな。NoSQL にしよう。」
  • 「このインタフェースは、NoSQLデータベースに対する基本的な操作を提供する。」
  • Store メソッドでデータを保存しよう。コレクション名とデータを受け取る。」
  • Update メソッドでデータを更新しよう。クエリと更新データを受け取る。」
  • FindAll メソッドで全データを取得しよう。コレクション名とクエリ、結果を受け取る。」
  • FindOne メソッドで特定のデータを取得しよう。コレクション名、クエリ、プロジェクション、結果を受け取る。」
  • 「トランザクションをサポートするために、StartSession メソッドを用意しよう。セッションを開始して、トランザクションを実行できるようにする。」
  • Session インタフェースも定義しよう。トランザクションの実行とセッションの終了を担当する。」
  • 「これで、NoSQLデータベースの操作が抽象化された。具体的な実装に依存しなくなる。」
  • 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
  • 「将来的に別のNoSQLデータベースを使うときも、このインタフェースを満たす新しい実装を作ればいい。拡張性が高まる。」
  • 「これで、NoSQLデータベースの操作が独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「NoSQLデータベースの操作を抽象化することで、アプリケーションが特定のデータベースに依存しなくなる。」
  • 「トランザクションをサポートすることで、データの整合性を保つことができる。」
  • StoreUpdateFindAllFindOne メソッドを用意することで、基本的なCRUD操作をカバーする。」
  • StartSession メソッドでトランザクションを実行できるようにすることで、複雑な操作もサポートする。」
  • 「これで、NoSQLデータベースの操作が独立したコンポーネントとして定義できた。保守性も高い。」
  • 「将来的に別のNoSQLデータベースを使うときも、このインタフェースを満たす新しい実装を作ればいい。拡張性が高まる。」
  • 「これで、NoSQLデータベースの操作が抽象化された。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

adapter/repository/sql.go

package repository

import "context"

type SQL interface {
	ExecuteContext(context.Context, string, ...interface{}) error
	QueryContext(context.Context, string, ...interface{}) (Rows, error)
	QueryRowContext(context.Context, string, ...interface{}) Row
	BeginTx(ctx context.Context) (Tx, error)
}

type Rows interface {
	Scan(dest ...interface{}) error
	Next() bool
	Err() error
	Close() error
}

type Row interface {
	Scan(dest ...interface{}) error
}

type Tx interface {
	ExecuteContext(context.Context, string, ...interface{}) error
	QueryContext(context.Context, string, ...interface{}) (Rows, error)
	QueryRowContext(context.Context, string, ...interface{}) Row
	Commit() error
	Rollback() error
}

対応部分は赤枠部分
image.png

なぜ SQL< i > を定義するのか?

インフラ層での特定のSQLの実装の受け皿を用意するためであり、adapter/repository/account_postgres.goのAccountSQLオブジェクトが所有する。

SQL< i > を定義するときのキモチ

  • 「SQLデータベースの操作を抽象化するインタフェースが必要だな。SQL にしよう。」
  • 「このインタフェースは、SQLデータベースに対する基本的な操作を提供する。」
  • ExecuteContext メソッドでクエリを実行しよう。コンテキスト、クエリ、パラメータを受け取る。」
  • QueryContext メソッドで複数行のデータを取得しよう。コンテキスト、クエリ、パラメータを受け取り、Rows を返す。」
  • QueryRowContext メソッドで単一行のデータを取得しよう。コンテキスト、クエリ、パラメータを受け取り、Row を返す。」
  • 「トランザクションをサポートするために、BeginTx メソッドを用意しよう。トランザクションを開始して、Tx インタフェースを返す。」
  • Rows インタフェースも定義しよう。複数行のデータをスキャンするためのメソッドを提供する。」
  • Row インタフェースも定義しよう。単一行のデータをスキャンするためのメソッドを提供する。」
  • Tx インタフェースも定義しよう。トランザクション内でのクエリ実行、コミット、ロールバックを担当する。」
  • 「これで、SQLデータベースの操作が抽象化された。具体的な実装に依存しなくなる。」
  • 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
  • 「将来的に別のSQLデータベースを使うときも、このインタフェースを満たす新しい実装を作ればいい。拡張性が高まる。」
  • 「これで、SQLデータベースの操作が独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「SQLデータベースの操作を抽象化することで、アプリケーションが特定のデータベースに依存しなくなる。」
  • 「トランザクションをサポートすることで、データの整合性を保つことができる。」
  • ExecuteContextQueryContextQueryRowContext メソッドを用意することで、基本的なCRUD操作をカバーする。」
  • BeginTx メソッドでトランザクションを開始できるようにすることで、複雑な操作もサポートする。」
  • RowsRow インタフェースを定義することで、データの取得とスキャンを抽象化する。」
  • Tx インタフェースを定義することで、トランザクション内での操作を抽象化する。」
  • 「これで、SQLデータベースの操作が独立したコンポーネントとして定義できた。保守性も高い。」
  • 「将来的に別のSQLデータベースを使うときも、このインタフェースを満たす新しい実装を作ればいい。拡張性が高まる。」
  • 「これで、SQLデータベースの操作が抽象化された。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

infrastructure/database/mongo_handler.go

package database

import (
	"context"
	"fmt"
	"github.com/gsabadini/go-clean-architecture/adapter/repository"
	"log"

	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type mongoHandler struct {
	db     *mongo.Database
	client *mongo.Client
}

func NewMongoHandler(c *config) (*mongoHandler, error) {
	ctx, cancel := context.WithTimeout(context.Background(), c.ctxTimeout)
	defer cancel()

	uri := fmt.Sprintf(
		"%s://%s:%s@mongodb-primary,mongodb-secondary,mongodb-arbiter/?replicaSet=replicaset",
		c.host,
		c.user,
		c.password,
	)

	clientOpts := options.Client().ApplyURI(uri)
	client, err := mongo.Connect(ctx, clientOpts)
	if err != nil {
		log.Fatal(err)
	}

	err = client.Ping(ctx, nil)
	if err != nil {
		log.Fatal(err)
	}

	return &mongoHandler{
		db:     client.Database(c.database),
		client: client,
	}, nil
}

func (mgo mongoHandler) Store(ctx context.Context, collection string, data interface{}) error {
	if _, err := mgo.db.Collection(collection).InsertOne(ctx, data); err != nil {
		return err
	}

	return nil
}

func (mgo mongoHandler) Update(ctx context.Context, collection string, query interface{}, update interface{}) error {
	if _, err := mgo.db.Collection(collection).UpdateOne(ctx, query, update); err != nil {
		return err
	}

	return nil
}

func (mgo mongoHandler) FindAll(ctx context.Context, collection string, query interface{}, result interface{}) error {
	cur, err := mgo.db.Collection(collection).Find(ctx, query)
	if err != nil {
		return err
	}

	defer cur.Close(ctx)
	if err = cur.All(ctx, result); err != nil {
		return err
	}

	if err := cur.Err(); err != nil {
		return err
	}

	return nil
}

func (mgo mongoHandler) FindOne(
	ctx context.Context,
	collection string,
	query interface{},
	projection interface{},
	result interface{},
) error {
	var err = mgo.db.Collection(collection).
		FindOne(
			ctx,
			query,
			options.FindOne().SetProjection(projection),
		).Decode(result)
	if err != nil {
		return err
	}

	return nil
}

func (mgo *mongoHandler) StartSession() (repository.Session, error) {
	session, err := mgo.client.StartSession()
	if err != nil {
		log.Fatal(err)
	}

	return newMongoHandlerSession(session), nil
}

type mongoDBSession struct {
	session mongo.Session
}

func newMongoHandlerSession(session mongo.Session) *mongoDBSession {
	return &mongoDBSession{session: session}
}

func (m *mongoDBSession) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
	callback := func(sessCtx mongo.SessionContext) (interface{}, error) {
		err := fn(sessCtx)
		if err != nil {
			return nil, err
		}
		return nil, nil
	}

	_, err := m.session.WithTransaction(ctx, callback)
	if err != nil {
		return err
	}

	return nil
}

func (m *mongoDBSession) EndSession(ctx context.Context) {
	m.session.EndSession(ctx)
}

対応部分は赤枠部分
image.png

暗黙のインタフェース実装

mongoHandlerはNoSQLインタフェースを暗に実装している。理屈は先ほどのAccountRepositoryインタフェースの実装と同じ。

mongoHandler を定義するときのキモチ

  • 「MongoDBとの接続と操作を担当するハンドラが必要だな。mongoHandler にしよう。」
  • 「このハンドラは、MongoDBに対する基本的な操作を提供する。」
  • 「MongoDBの接続情報は設定ファイルから取得しよう。ホスト、ユーザー、パスワード、データベース名が必要だ。」
  • NewMongoHandler 関数でMongoDBに接続しよう。接続URIを組み立てて、クライアントを作成する。」
  • 「接続が成功したら、データベースとクライアントを保持しておこう。これで後で使える。」
  • Store メソッドでデータを保存しよう。コレクション名とデータを受け取る。」
  • Update メソッドでデータを更新しよう。コレクション名、クエリ、更新データを受け取る。」
  • FindAll メソッドで全データを取得しよう。コレクション名、クエリ、結果を受け取る。」
  • FindOne メソッドで特定のデータを取得しよう。コレクション名、クエリ、プロジェクション、結果を受け取る。」
  • 「トランザクションをサポートするために、StartSession メソッドを用意しよう。セッションを開始して、トランザクションを実行できるようにする。」
  • mongoDBSession 構造体も定義しよう。トランザクションの実行とセッションの終了を担当する。」
  • 「これで、MongoDBの操作が抽象化された。具体的な実装に依存しなくなる。」
  • 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
  • 「将来的に別のNoSQLデータベースを使うときも、このハンドラを拡張すればいい。拡張性が高まる。」
  • 「これで、MongoDBの操作が独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「MongoDBの接続情報を設定ファイルから取得することで、柔軟性を高める。」
  • StoreUpdateFindAllFindOne メソッドを用意することで、基本的なCRUD操作をカバーする。」
  • 「トランザクションをサポートすることで、データの整合性を保つことができる。」
  • StartSession メソッドでトランザクションを実行できるようにすることで、複雑な操作もサポートする。」
  • mongoDBSession 構造体を定義することで、トランザクションの実行とセッションの終了を抽象化する。」
  • 「これで、MongoDBの操作が独立したコンポーネントとして定義できた。保守性も高い。」
  • 「将来的に別のNoSQLデータベースを使うときも、このハンドラを拡張すればいい。拡張性が高まる。」
  • 「これで、MongoDBの操作が抽象化された。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

infrastructure/database/postgres_handler.go

package database

import (
	"context"
	"database/sql"
	"fmt"
	"log"

	"github.com/gsabadini/go-clean-architecture/adapter/repository"

	_ "github.com/lib/pq"
)

type postgresHandler struct {
	db *sql.DB
}

func NewPostgresHandler(c *config) (*postgresHandler, error) {
	var ds = fmt.Sprintf(
		"host=%s port=%s user=%s dbname=%s sslmode=disable password=%s",
		c.host,
		c.port,
		c.user,
		c.database,
		c.password,
	)

	fmt.Println(ds)
	db, err := sql.Open(c.driver, ds)
	if err != nil {
		return &postgresHandler{}, err
	}

	err = db.Ping()
	if err != nil {
		log.Fatalln(err)
	}

	return &postgresHandler{db: db}, nil
}

func (p postgresHandler) BeginTx(ctx context.Context) (repository.Tx, error) {
	tx, err := p.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		return postgresTx{}, err
	}

	return newPostgresTx(tx), nil
}

func (p postgresHandler) ExecuteContext(ctx context.Context, query string, args ...interface{}) error {
	_, err := p.db.ExecContext(ctx, query, args...)
	if err != nil {
		return err
	}

	return nil
}

func (p postgresHandler) QueryContext(ctx context.Context, query string, args ...interface{}) (repository.Rows, error) {
	rows, err := p.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}

	row := newPostgresRows(rows)

	return row, nil
}

func (p postgresHandler) QueryRowContext(ctx context.Context, query string, args ...interface{}) repository.Row {
	row := p.db.QueryRowContext(ctx, query, args...)

	return newPostgresRow(row)
}

type postgresRow struct {
	row *sql.Row
}

func newPostgresRow(row *sql.Row) postgresRow {
	return postgresRow{row: row}
}

func (pr postgresRow) Scan(dest ...interface{}) error {
	if err := pr.row.Scan(dest...); err != nil {
		return err
	}

	return nil
}

type postgresRows struct {
	rows *sql.Rows
}

func newPostgresRows(rows *sql.Rows) postgresRows {
	return postgresRows{rows: rows}
}

func (pr postgresRows) Scan(dest ...interface{}) error {
	if err := pr.rows.Scan(dest...); err != nil {
		return err
	}

	return nil
}

func (pr postgresRows) Next() bool {
	return pr.rows.Next()
}

func (pr postgresRows) Err() error {
	return pr.rows.Err()
}

func (pr postgresRows) Close() error {
	return pr.rows.Close()
}

type postgresTx struct {
	tx *sql.Tx
}

func newPostgresTx(tx *sql.Tx) postgresTx {
	return postgresTx{tx: tx}
}

func (p postgresTx) ExecuteContext(ctx context.Context, query string, args ...interface{}) error {
	_, err := p.tx.ExecContext(ctx, query, args...)
	if err != nil {
		return err
	}

	return nil
}

func (p postgresTx) QueryContext(ctx context.Context, query string, args ...interface{}) (repository.Rows, error) {
	rows, err := p.tx.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}

	row := newPostgresRows(rows)

	return row, nil
}

func (p postgresTx) QueryRowContext(ctx context.Context, query string, args ...interface{}) repository.Row {
	row := p.tx.QueryRowContext(ctx, query, args...)

	return newPostgresRow(row)
}

func (p postgresTx) Commit() error {
	return p.tx.Commit()
}

func (p postgresTx) Rollback() error {
	return p.tx.Rollback()
}

対応部分は赤枠部分
image.png

暗黙のインタフェース実装

postgresHandlerはNoSQLインタフェースを暗に実装している。理屈は先ほどのAccountRepositoryインタフェースの実装と同じ。

postgresHandler を定義するときのキモチ

  • 「PostgreSQLとの接続と操作を担当するハンドラが必要だな。postgresHandler にしよう。」
  • 「このハンドラは、PostgreSQLに対する基本的な操作を提供する。」
  • 「PostgreSQLの接続情報は設定ファイルから取得しよう。ホスト、ポート、ユーザー、データベース名、パスワードが必要だ。」
  • NewPostgresHandler 関数でPostgreSQLに接続しよう。接続文字列を組み立てて、データベースに接続する。」
  • 「接続が成功したら、データベースを保持しておこう。これで後で使える。」
  • ExecuteContext メソッドでクエリを実行しよう。コンテキスト、クエリ、パラメータを受け取る。」
  • QueryContext メソッドで複数行のデータを取得しよう。コンテキスト、クエリ、パラメータを受け取り、Rows を返す。」
  • QueryRowContext メソッドで単一行のデータを取得しよう。コンテキスト、クエリ、パラメータを受け取り、Row を返す。」
  • 「トランザクションをサポートするために、BeginTx メソッドを用意しよう。トランザクションを開始して、Tx インタフェースを返す。」
  • postgresRowpostgresRowspostgresTx 構造体も定義しよう。データの取得とトランザクションの実行を担当する。」
  • 「これで、PostgreSQLの操作が抽象化された。具体的な実装に依存しなくなる。」
  • 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
  • 「将来的に別のSQLデータベースを使うときも、このハンドラを拡張すればいい。拡張性が高まる。」
  • 「これで、PostgreSQLの操作が独立したコンポーネントとして定義できた。クリーンだ。」
  • 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」

追加のキモチ

  • 「PostgreSQLの接続情報を設定ファイルから取得することで、柔軟性を高める。」
  • ExecuteContextQueryContextQueryRowContext メソッドを用意することで、基本的なCRUD操作をカバーする。」
  • 「トランザクションをサポートすることで、データの整合性を保つことができる。」
  • BeginTx メソッドでトランザクションを開始できるようにすることで、複雑な操作もサポートする。」
  • postgresRowpostgresRowspostgresTx 構造体を定義することで、データの取得とトランザクションの実行を抽象化する。」
  • 「これで、PostgreSQLの操作が独立したコンポーネントとして定義できた。保守性も高い。」
  • 「将来的に別のSQLデータベースを使うときも、このハンドラを拡張すればいい。拡張性が高まる。」
  • 「これで、PostgreSQLの操作が抽象化された。クリーンだ。」
  • 「まあ、足りないところはリファクタリングで直せばいいか。」

参考資料

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?