Goのアーキテクチャー
全体的な考え
拡張性・保守性があり、パフォーマンス効率を考えた設計をする。
そのためにまず、フロントエンド側にビジネスロジックを記載する利口なUIはできるだけ避けるべき。ビジネスロジックは、できるだけサーバー側に置き、ドメイン単位で作り重複は避ける。
フロントエンドにビジネスロジックを置いたときの弊害として、2点あります。
1点フロントエンドは画面単位で作成するので、同じビジネスロジックが重複しやすく、ビジネスロジックが変更になった場合、広範囲に広がります。
例えば、商品の合計値を計算するロジックを考えたとき、購入履歴画面、購入確認画面、購入一覧画面の3画面で表示することを想定する。それぞれの画面で異なる人が実装すると、それぞれ、その画面に紐付いたところに合計値を計算するロジックが記述されます。そrで、消費税の計算方法が変わったときに、3つそれぞれ変更する必要があります。そうすると、修正時間が膨らみ、修正の漏れ、そもそも記述コードの量が増えるため、パフォーマンスも悪くなる。
2点目は、UIはより変更されやすく、かつ、変更されやすくすべきだが、ビジネスロジックがUIに絡むと変更が難しくなります。
モダンなデザインは移り変わりは激しく、それに対応しないとUXは非常に悪くなります。なので、UIは、変更されやすくすべきです。例えば、商品価格の値だけを別のページに移動するってなった際、ロジックが紐づいているとそのロジックも移動する異なります。しかし、そのロジックは、他のロジックに依存していた場合、可読性が下がることが容易に想像できます。
また、フロントエンドに側にロジックを増やさないように、マジックナンバーを返すこともなるべく控えるようにします。
0とか1が返されても、フロント側では、それをわかりやすくするために、変換するためロジックが発生します。
// 0とはどういう状態?
// 1はadmin?
{
status: 0,
permission: 1,
}
// ok
{
status: 'loading',
permission: 'admin',
}
ドメインのルールを構成するには、どうするのはかは後述のアーキテクチャーに関わる。
レイヤードアーキテクチャ
以下の層に分けて、アプリケーションを構築。
- ユーザインターフェース
- アプケーション
- ドメイン
- インフラストラクチャー
上ほど抽象度が高くなっており、上位レイヤーが下位レイヤーを利用する。
利用の向きは以下の通り。
ユーザインターフェース
↓
アプリケーション
↓
ドメイン
↓
インフラ
また、依存関係は以下の通りとなる。
ユーザインターフェース
↓
アプリケーション
↓
ドメイン
↑
インフラ
ただし、ドメインやアプリケーションからインフラストラクチャーへの依存はインターフェースを通して利用するため、インフラからドメインに依存している。
ユーザインターフェース
第三者の利用者とサービスをつなげるインターフェース部分です。
クライアント側の入力を受け取り、ユーザ側に結果を返す役割を担います。
下記ディレクトリでは、ハンドラー層のことです。
アプリケーション
ユースケースの進行役を担います。
進行役のため、ドメインのルールやロジックは禁止です。
例えば、ユーザ名は50文字までいったことや、「|[{<>}]?!@#$%^&*()_」は禁止文字であることは記述禁止です。
ユースケースなので、たとえば、ユーザを登録登録の一連処理、商品を購入する一連処理をかく。
下記ディレクトリでいうと、サービス層です。
ドメイン
ドメインに関数ことを書きます。
例えば、ユーザ名、パスワード、商品があり、それぞれ50文字であることや、利用禁止文字があるなどのルール(ロジック)があれば、記述します。
ドメイン駆動開発では、ここが一番大切。ここをみれば、どういった仕様なのか?知れるようにすることが大事になってくる。コードそのものをドキュメントとする考えとなっており、別途ドキュメントを残す必要がないことを目指す。別途ドキュメントを残すと、更新漏れがあったり、記述ミスがある可能性があったり、手間が増える。コードそのものをドキュメントとすることが一番確実。
上記のディレクトリではドメイン層です。
インフラストラクチャー
ここでは、他の層を支える技術的な基盤を描きます。
データベースやキャッシュサーバーなどの処理などです。
DBに依存する記述やSQLなどはこの層に書く。
抽象度が高い存在(ユーザを新規登録する、など)にとって、何に保存するかは関係なくどうでも良い。何かの媒体を使って保存してくれることのみが、大事。なので、その情報を切り分けることで、変更に強くなる。
ディレクトリ構成
レイアードアーキテクチャーに則る
利用は上から下とする。逆はダメ!
ハンドラー→サービス→ドメイン→リポジトリ(依存関係はまた別)
- ハンドラー層
- クライアントからきたデータのバリデーションを行う
- クライアントにステータスなどを含めたデータ返す
- ユースケートの依頼はインターフェースを利用してサービス層に依頼
- サービス層
- ドメインとリポジトリを活用してユースケース(機能)の実装を行う
- リポジトリは、インターフェースを通して利用する
- ドメイン層
- ユーザや、パスワードなどドメインは関することはドメイン層
- サービス間をまたがるドメインに関数ロジックはここに記述
- 凝縮度を意識してドメイン層を作成すること
- リポジトリは、インターフェースを通して利用する
- リポジトリ層
- SQLを書いてDBにアクセスするのはここのみ
- ドメイン層、サービス層で定義したインターフェースを実装すること
ドメイン駆動開発では、ドメインをまず作成し、それを利用し、ユースケースを作っていく開発(ボトルアップ開発)である
インターフェース
あるモジュールAが、他のモジュールBを利用しているとき、AはBに依存しているといいます。
Bを変更することによって、Aはその変更の影響を受ける可能性があります。つまり、Bを変更したら、利用箇所は全て変更する必要があり、Aは変更していないのにも関わらず、デグレードを起こす可能性があります。
ここで、AはUserApplicationService、BはUserMySQLRepositoryとします。
UserMySQLRepositoryをPostgressに変更したとき、利用側も全て変更する必要がありますね。
そこで、インターフェースを利用することで、モジュールを利用する側、利用される側お互い疎結合になり、変更に強くなるかつ、お互いの処理の中身を知らなくても、よくなります。つまり、インターフェースさえ決めれば、モジュールを作る人と、モジュールを利用する人を分担して作業することもできます。これを、依存関係の逆転と言います。
// インターフェース
type IUserRepository interface {
Find(ctx context.Context, db repository.Execer, u *entity.User) error
}
この際、上位層に持たせることが大事です。上位っていうのは、関数を利用する側であり、より抽象的な、人間がわかりやすいっといった感じですかね。
例えば、以下の2点でどちらがわかりやすいでしょうか?
- 名前とメールアドレスを利用してユーザを登録する(ちょっと強引かもしれまえんが、、)
type UserRepository interface {
RegisterUser(ctx context.Context, db repository.Execer, u *entity.User) error
}
- MySQLというDBでINSERTを利用してstring型のユーザ名とメールアドレスを登録する
type UserInsertWithMySQL interface {
InsertUserWithMySQL(ctx context.Context, db repository.Execer, name, email *string ) error
}
利用者であれば、圧倒的に、1の方がわかりやすいのはないでしょうか。利用者にとって、MySQLやInsertなど具体的な保存サービスやSQLなど保存に関する具体的な方法はどうでもいいです。何らかの、サービスやツールなどを利用して保存してくれるっていうことわかれば、いいです。
2. だと具体的なので、わかりにくいです。また、もし仮に、MySQLからPostgressにDBを変更した時を考えます。そうすると、以下の様に修正する必要が出てくるかなって思います。
type UserInsertWithPostgress interface {
InsertUserWithMySQL(ctx context.Context, db repository.Execer, name, email *string ) error
}
そうすると、インターフェースを利用しているのにも関わらず、このインターフェースを利用しているところすべて、変更する必要があります。何十、何百箇所修正するかもと考えるとゾッとします。
仮に、1をインターフェースに利用していれば、Postgressに変更しても、インターフェースを変更する必要がありません。変更するのは、インターフェースを実装している関数の1箇所のみです。
なので、より上位層にインターフェースを持たせることを念頭に置いて、インターフェースを利用しましょう。
これを依存関係の逆転と言います。
今回、
ハンドラー層からサービス層を利用する時のインターフェースは、ハンドラー側、
ドメイン層、サービス層からリポジトリ層を利用する時のインターフェースはサービス側またドメイン層にする
Go DDD