目的
クリーンアーキテクチャのプロジェクトを読解し、その過程で学んだことを備忘録として記録する。これにより、アプリケーションサーバーのアーキテクチャに対する理解を深め、今後の学習や開発の基盤を築くことを目的としている。
(初学者による備忘録のため、誤りがある場合はご容赦ください。)
以下は読解したプロジェクト
おすすめの使用方法
Chromeブラウザで2つのウィンドウを開き、この記事にアクセス。
- 左側のウィンドウ: コードを参照するために使用。
- 右側のウィンドウ: 解説を読みながら理解を深めるために使用。
このように並べて表示することで、コードと解説を同時に確認でき、理解がスムーズに進むかも。
クリーンアーキテクチャとは
ソフトウェアを設計するための方法の一つで、重要な「ビジネスロジック」と、データベースやフレームワークなどの「具体的な技術」を分けて作ることを目指す。
この設計の何が嬉しいの?
- 開発の早期開始が可能:各層が独立しているため、フレームワークやデータベースが未決定でも開発を始められる。
- 並行開発が容易:各層が独立しているため、他の部分の完成を待たずに作業を進められる。
- 将来の変更が容易:各層が独立しているため、フレームワークやデータベースの変更がスムーズにできる。
図とコードを対応させる
Accountが生成されるまでの全体図
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}
}
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)
}
なぜ 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
}
CreateAccountInteractor
createAccountInteractor struct {
repo domain.AccountRepository
presenter CreateAccountPresenter
ctxTimeout time.Duration
}
なぜ 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)
}
CreateAccountUseCase< i >
とは?
2つ外側のインフラ層で、ユースケース層のNewCreateAccountInteractor関数を使ってCreateAccountUseCaseインスタンスを生成している。そのインスタンスの受け皿としてインターフェースを定義する。
CreateAccountUseCase< i >
を定義するときのキモチ
- 「ユースケースのインタフェースを定義しよう。
CreateAccountUseCase
にしよう。」 - 「このインタフェースは、アカウント作成のユースケースを表すから、メソッド名は
Execute
にしよう。」 - 「
Execute
の引数は、コンテキストと入力データ (CreateAccountInput
) にしよう。これで必要な情報を渡せる。」 - 「戻り値は、出力データ (
CreateAccountOutput
) とエラーにしよう。エラーは必ず返すようにする。」 - 「インタフェースにすることで、実装を隠蔽できる。依存性逆転の原則を守れるな。」
- 「これで、他の層(例えばコントローラー)がこのインタフェースに依存するようになる。実装の詳細を知らなくていいから、疎結合になる。」
- 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
- 「将来的に別の実装を追加するときも、このインタフェースを満たせばいい。拡張性が高まるな。」
- 「このインタフェースがあることで、アカウント作成のユースケースが何をするのか、明確になる。」
- 「入力と出力の構造体も一緒に定義しておこう。これで、ユースケースの入出力が明確になる。」
- 「これで、アカウント作成のユースケースが独立したコンポーネントとして定義できた。クリーンだ。」
- 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」
追加のキモチ
- 「インタフェースを小さく保とう。単一責任の原則を守るためだ。」
- 「
CreateAccountInput
とCreateAccountOutput
を別の構造体にすることで、入出力の責任を分離しよう。」 - 「このインタフェースがあることで、ドメイン層とプレゼンテーション層の橋渡しができるな。」
- 「将来的に他のユースケースも同じようにインタフェースで定義しよう。一貫性が出る。」
- 「これで、アカウント作成のユースケースがフレームワークやDBに依存しなくなった。テストもしやすい。」
- 「インタフェースを定義することで、チームメンバーとのコミュニケーションも楽になる。何が必要なのかが明確だから。」
- 「これで、アカウント作成のユースケースがしっかりとカプセル化されたな。変更に強くなった。」
CreateAccountPresenter< i >
CreateAccountPresenter interface {
Output(domain.Account) CreateAccountOutput
}
なぜ 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)
}
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
}
Validator<i>
を定義するときのキモチ
- 「バリデーションのインタフェースが必要だな。
Validator
にしよう。」 - 「このインタフェースは、入力データを検証する責任を持つ。」
- 「メソッドは
Validate
とMessages
にしよう。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{}
Logger<i>
を定義するときのキモチ
- 「ロギングのインタフェースが必要だな。
Logger
にしよう。」 - 「このインタフェースは、アプリケーション全体でログを記録する責任を持つ。」
- 「ログレベルごとにメソッドを分けよう。
Infof
、Warnf
、Errorf
、Fatalln
を用意する。」 - 「フォーマット付きのログ記録ができるように、
Infof
、Warnf
、Errorf
はformat
と可変長引数を受け取るようにしよう。」 - 「致命的なエラーの場合は
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),
}
}
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)
}
}
infrastructure/router/gin.go
を定義するときのキモチ
- 「HTTPリクエストを処理するためのルーターが必要だな。
ginEngine
にしよう。」 - 「このルーターは、Ginフレームワークを使ってHTTPリクエストを処理する。」
- 「ルーターの初期化は
newGinServer
関数で行おう。ロガー、データベース、バリデーター、ポート、タイムアウトを受け取る。」 - 「
Listen
メソッドでサーバーを起動しよう。HTTPサーバーの設定を行い、シグナルを受け取ってシャットダウンもできるようにする。」 - 「
setAppHandlers
メソッドでルーティングを設定しよう。各エンドポイントに対応するアクションを紐付ける。」 - 「
buildCreateTransferAction
、buildFindAllTransferAction
、buildCreateAccountAction
、buildFindAllAccountAction
、buildFindBalanceAccountAction
メソッドで各ユースケースをビルドしよう。依存関係を注入してアクションを作成する。」 - 「
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),
)
}
infrastructure/router/gorilla_mux.go
を定義するときのキモチ
- 「HTTPリクエストを処理するためのルーターが必要だな。
gorillaMux
にしよう。」 - 「このルーターは、Gorilla Muxフレームワークを使ってHTTPリクエストを処理する。」
- 「ルーターの初期化は
newGorillaMux
関数で行おう。ロガー、データベース、バリデーター、ポート、タイムアウトを受け取る。」 - 「
Listen
メソッドでサーバーを起動しよう。HTTPサーバーの設定を行い、シグナルを受け取ってシャットダウンもできるようにする。」 - 「
setAppHandlers
メソッドでルーティングを設定しよう。各エンドポイントに対応するアクションを紐付ける。」 - 「
buildCreateTransferAction
、buildFindAllTransferAction
、buildCreateAccountAction
、buildFindAllAccountAction
、buildFindBalanceAccountAction
メソッドで各ユースケースをビルドしよう。依存関係を注入してアクションを作成する。」 - 「
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
}
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}
}
infrastructure/log/zap.go
を定義するときのキモチ
- 「ロギングの実装が必要だな。
zapLogger
にしよう。」 - 「このロガーは、Zapパッケージを使ってログを記録する。」
- 「
NewZapLogger
関数でロガーを初期化しよう。Zapのプロダクション設定を使う。」 - 「
Infof
、Warnf
、Errorf
、Fatalln
メソッドでログを記録しよう。フォーマット付きのログ記録ができるようにする。」 - 「
WithFields
メソッドで追加のフィールドをログに含められるようにしよう。これでコンテキストを追加できる。」 - 「
WithError
メソッドでエラーをログに含められるようにしよう。これでエラーの詳細を記録できる。」 - 「これで、ロギングが独立したコンポーネントとして定義できた。クリーンだ。」
- 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」
追加のキモチ
- 「Zapパッケージを使うことで、高速で構造化されたログ記録ができる。」
- 「
NewZapLogger
関数でロガーを初期化することで、ログの設定を一元管理できる。」 - 「
Infof
、Warnf
、Errorf
、Fatalln
メソッドを用意することで、ログレベルごとの記録ができる。」 - 「
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
}
infrastructure/log/logrus.go
を定義するときのキモチ
- 「ロギングの実装が必要だな。
logrusLogger
にしよう。」 - 「このロガーは、Logrusパッケージを使ってログを記録する。」
- 「
NewLogrusLogger
関数でロガーを初期化しよう。LogrusのJSONフォーマットを使う。」 - 「
Infof
、Warnf
、Errorf
、Fatalln
メソッドでログを記録しよう。フォーマット付きのログ記録ができるようにする。」 - 「
WithFields
メソッドで追加のフィールドをログに含められるようにしよう。これでコンテキストを追加できる。」 - 「
WithError
メソッドでエラーをログに含められるようにしよう。これでエラーの詳細を記録できる。」 - 「
logrusLogEntry
構造体も定義しよう。ログエントリの操作を担当する。」 - 「これで、ロギングが独立したコンポーネントとして定義できた。クリーンだ。」
- 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」
追加のキモチ
- 「Logrusパッケージを使うことで、柔軟で構造化されたログ記録ができる。」
- 「
NewLogrusLogger
関数でロガーを初期化することで、ログの設定を一元管理できる。」 - 「
Infof
、Warnf
、Errorf
、Fatalln
メソッドを用意することで、ログレベルごとの記録ができる。」 - 「
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
}
NoSQL側のAccountRepositoryの実装
AccountNoSQLオブジェクトはAccountRepositoryインタフェースを暗黙に実装している。
以下の記事を参照。
AccountNoSQL
を定義するときのキモチ
- 「MongoDBを使ったアカウントのリポジトリが必要だな。
AccountNoSQL
にしよう。」 - 「このリポジトリは、アカウントの永続化と取得を担当する。」
- 「MongoDBのコレクション名は
accounts
にしよう。これでデータを整理しやすくなる。」 - 「ドメイン層の
Account
を MongoDB に保存するために、accountBSON
構造体を定義しよう。」 accountBSON
は MongoDB のドキュメント形式に合わせる。ID、Name、CPF、Balance、CreatedAt をフィールドに持つ。」- 「
Create
メソッドでアカウントを作成しよう。ドメイン層のAccount
をaccountBSON
に変換して保存する。」 - 「
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
}
}
SQL側のAccountRepositoryの実装
AccountNoSQLオブジェクトはAccountRepositoryインタフェースを暗黙に実装している。
以下の記事を参照。
AccountNoSQL
を定義するときのキモチ
- 「PostgreSQLを使ったアカウントのリポジトリが必要だな。
AccountSQL
にしよう。」 - 「このリポジトリは、アカウントの永続化と取得を担当する。」
- 「SQLクエリを使ってアカウントのデータを操作する。
INSERT
、UPDATE
、SELECT
を使う。」 - 「
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)
}
なぜ NoSQL< i >
を定義するのか?
インフラ層での特定のNoSQLの実装の受け皿を用意するためであり、adapter/repository/account_mongodb.go
のAccountSQLオブジェクトが所有する。
NoSQL< i >
を定義するときのキモチ
- 「NoSQLデータベースの操作を抽象化するインタフェースが必要だな。
NoSQL
にしよう。」 - 「このインタフェースは、NoSQLデータベースに対する基本的な操作を提供する。」
- 「
Store
メソッドでデータを保存しよう。コレクション名とデータを受け取る。」 - 「
Update
メソッドでデータを更新しよう。クエリと更新データを受け取る。」 - 「
FindAll
メソッドで全データを取得しよう。コレクション名とクエリ、結果を受け取る。」 - 「
FindOne
メソッドで特定のデータを取得しよう。コレクション名、クエリ、プロジェクション、結果を受け取る。」 - 「トランザクションをサポートするために、
StartSession
メソッドを用意しよう。セッションを開始して、トランザクションを実行できるようにする。」 - 「
Session
インタフェースも定義しよう。トランザクションの実行とセッションの終了を担当する。」 - 「これで、NoSQLデータベースの操作が抽象化された。具体的な実装に依存しなくなる。」
- 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
- 「将来的に別のNoSQLデータベースを使うときも、このインタフェースを満たす新しい実装を作ればいい。拡張性が高まる。」
- 「これで、NoSQLデータベースの操作が独立したコンポーネントとして定義できた。クリーンだ。」
- 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」
追加のキモチ
- 「NoSQLデータベースの操作を抽象化することで、アプリケーションが特定のデータベースに依存しなくなる。」
- 「トランザクションをサポートすることで、データの整合性を保つことができる。」
- 「
Store
、Update
、FindAll
、FindOne
メソッドを用意することで、基本的な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
}
なぜ 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データベースの操作を抽象化することで、アプリケーションが特定のデータベースに依存しなくなる。」
- 「トランザクションをサポートすることで、データの整合性を保つことができる。」
- 「
ExecuteContext
、QueryContext
、QueryRowContext
メソッドを用意することで、基本的なCRUD操作をカバーする。」 - 「
BeginTx
メソッドでトランザクションを開始できるようにすることで、複雑な操作もサポートする。」 - 「
Rows
とRow
インタフェースを定義することで、データの取得とスキャンを抽象化する。」 - 「
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)
}
暗黙のインタフェース実装
mongoHandlerはNoSQLインタフェースを暗に実装している。理屈は先ほどのAccountRepositoryインタフェースの実装と同じ。
mongoHandler
を定義するときのキモチ
- 「MongoDBとの接続と操作を担当するハンドラが必要だな。
mongoHandler
にしよう。」 - 「このハンドラは、MongoDBに対する基本的な操作を提供する。」
- 「MongoDBの接続情報は設定ファイルから取得しよう。ホスト、ユーザー、パスワード、データベース名が必要だ。」
- 「
NewMongoHandler
関数でMongoDBに接続しよう。接続URIを組み立てて、クライアントを作成する。」 - 「接続が成功したら、データベースとクライアントを保持しておこう。これで後で使える。」
- 「
Store
メソッドでデータを保存しよう。コレクション名とデータを受け取る。」 - 「
Update
メソッドでデータを更新しよう。コレクション名、クエリ、更新データを受け取る。」 - 「
FindAll
メソッドで全データを取得しよう。コレクション名、クエリ、結果を受け取る。」 - 「
FindOne
メソッドで特定のデータを取得しよう。コレクション名、クエリ、プロジェクション、結果を受け取る。」 - 「トランザクションをサポートするために、
StartSession
メソッドを用意しよう。セッションを開始して、トランザクションを実行できるようにする。」 - 「
mongoDBSession
構造体も定義しよう。トランザクションの実行とセッションの終了を担当する。」 - 「これで、MongoDBの操作が抽象化された。具体的な実装に依存しなくなる。」
- 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
- 「将来的に別のNoSQLデータベースを使うときも、このハンドラを拡張すればいい。拡張性が高まる。」
- 「これで、MongoDBの操作が独立したコンポーネントとして定義できた。クリーンだ。」
- 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」
追加のキモチ
- 「MongoDBの接続情報を設定ファイルから取得することで、柔軟性を高める。」
- 「
Store
、Update
、FindAll
、FindOne
メソッドを用意することで、基本的な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()
}
暗黙のインタフェース実装
postgresHandlerはNoSQLインタフェースを暗に実装している。理屈は先ほどのAccountRepositoryインタフェースの実装と同じ。
postgresHandler
を定義するときのキモチ
- 「PostgreSQLとの接続と操作を担当するハンドラが必要だな。
postgresHandler
にしよう。」 - 「このハンドラは、PostgreSQLに対する基本的な操作を提供する。」
- 「PostgreSQLの接続情報は設定ファイルから取得しよう。ホスト、ポート、ユーザー、データベース名、パスワードが必要だ。」
- 「
NewPostgresHandler
関数でPostgreSQLに接続しよう。接続文字列を組み立てて、データベースに接続する。」 - 「接続が成功したら、データベースを保持しておこう。これで後で使える。」
- 「
ExecuteContext
メソッドでクエリを実行しよう。コンテキスト、クエリ、パラメータを受け取る。」 - 「
QueryContext
メソッドで複数行のデータを取得しよう。コンテキスト、クエリ、パラメータを受け取り、Rows
を返す。」 - 「
QueryRowContext
メソッドで単一行のデータを取得しよう。コンテキスト、クエリ、パラメータを受け取り、Row
を返す。」 - 「トランザクションをサポートするために、
BeginTx
メソッドを用意しよう。トランザクションを開始して、Tx
インタフェースを返す。」 - 「
postgresRow
、postgresRows
、postgresTx
構造体も定義しよう。データの取得とトランザクションの実行を担当する。」 - 「これで、PostgreSQLの操作が抽象化された。具体的な実装に依存しなくなる。」
- 「テストのときも、モックを使いやすくなる。インタフェースがあるから、実装を差し替えられる。」
- 「将来的に別のSQLデータベースを使うときも、このハンドラを拡張すればいい。拡張性が高まる。」
- 「これで、PostgreSQLの操作が独立したコンポーネントとして定義できた。クリーンだ。」
- 「まあ、必要になったらリファクタリングすればいいか。今はこれで十分だ。」
追加のキモチ
- 「PostgreSQLの接続情報を設定ファイルから取得することで、柔軟性を高める。」
- 「
ExecuteContext
、QueryContext
、QueryRowContext
メソッドを用意することで、基本的なCRUD操作をカバーする。」 - 「トランザクションをサポートすることで、データの整合性を保つことができる。」
- 「
BeginTx
メソッドでトランザクションを開始できるようにすることで、複雑な操作もサポートする。」 - 「
postgresRow
、postgresRows
、postgresTx
構造体を定義することで、データの取得とトランザクションの実行を抽象化する。」 - 「これで、PostgreSQLの操作が独立したコンポーネントとして定義できた。保守性も高い。」
- 「将来的に別のSQLデータベースを使うときも、このハンドラを拡張すればいい。拡張性が高まる。」
- 「これで、PostgreSQLの操作が抽象化された。クリーンだ。」
- 「まあ、足りないところはリファクタリングで直せばいいか。」
参考資料