Go言語で変更に強くテスタブルな設計を行う際の具体例について

uzuki

こんにちは、uzuki です。

クリーンアーキテクチャをベースにドメイン駆動設計と DI を取り入れた、変更に強くテスタブル(=テストコードを書きやすい)な設計の具体例を説明します。

Go言語で簡単な REST API を実装する前提で説明しますが、クリーンアーキテクチャやドメイン駆動設計 /DI についての詳細、ソースコードの内容については省きますのであしからずご了承ください。

変更に強くテスタブルな設計とは

変更に強い
インフラの変更やアップデート、特定のロジックの変更にあたり、修正箇所及び影響範囲を限定的に抑えられることを指します。
基本的にインターフェイスでやり取りを行うことにより、変更や修正に対して利用側に影響を与えないようにします。
また、プレゼンテーション層やユースケース(アプリケーション)層などの、頻繁にロジックの変更や追加が行われるような場所においては、処理単位でファイルを分けておくことにより影響範囲が限定的になります。

テスタブル(=テストコードを書きやすい)
特定の構造体に依存せず、モックへの差し替えが容易で、各々のロジックの責任範囲でのテストコードを容易に書けることを指します。
構造体を初期化する際に渡す値をドメインモデル/インターフェイス/プリミティブ型に限定し、処理内で特定の構造体を直接生成しないようにすることで、モック側でテスト条件を容易に揃えることができるようになります。


従来の MVC モデルの場合、度重なる修正や変更により、特にコントローラに様々な処理が詰め込まれ変更時の影響が他に波及しやすくなったり、またテストコードも書きづらくなるほど外部リソースや構造体に依存した実装になりがちです。
責任範囲を明確に分け、一つ一つのファイルに記述する処理をシンプルにすることで、こういった問題が発生しないようあらかじめ設計しておくのが今回の目指す内容となります。

前提

会社情報と社員情報を管理する REST API を想定します。

  • 会社情報は会社 ID/企業名を持ちます。
  • 社員情報は社員 ID/会社 ID/名前/メールアドレスを持ちます。
  • 会社情報と社員情報はデータベース(PostgreSQL)に保持します。
  • 会社情報の取得/作成/更新/削除、社員情報の取得/作成/更新/削除を REST API として提供します。
  • URL パラメータで要素を指定することにより、値をマスクして結果を返します。

なお、これから示すファイル/ディレクトリ構成に関してはテストファイルや go.mod、vendor ディレクトリ等は省略しています。

ディレクトリ構成

ディレクトリ構成に関しては、ドメイン駆動設計を一部取り入れて、project/src 配下でドメイン層/インフラストラクチャ層/プレゼンテーション層/ユースケース(アプリケーション)層に分けています。
それらに属しない、実行時に必要なもの(config のロードや HTTP サーバ等)は project/src/runtime に含めています。

Plain Text

project 配下

Plain Text

setting ディレクトリは config.json(データベースの接続情報やログの書き出し先等を記述)と router.json(REST API のルーティングを記述)で構成しています。
src ディレクトリはソースコードで構成しています。
main.go は数行ですむようにしておき、基本的に main.go の変更を不要にしておきます。
具体的には、後述の project/src/runtime/dependency.go を呼び出し DI コンテナを生成し、DI コンテナ内の router 構造体を実行し、REST API サーバとして実行するようにします。
これにより、設定変更や REST API のルーティングを変更したい場合は setting 配下のみを変更し、機能追加や修正する場合は src 配下のみ(場合により project/setting/router.json の変更も)を変更すれば良いです。

project/src/domain 配下(ドメイン層)

Plain Text

社員情報と会社情報のドメインモデルでドメイン層を構成しています。

domain/company と domain/employee ディレクトリ双方に存在する repository.go に関しては、エンティティを永続化するためのリポジトリ(インフラストラクチャ層で実装する、データベース上のCRUD操作)のインターフェイスを定義しておきます。
repository.go 以外のdomain/company/company.go と domain/employee/employee.go がエンティティ、それ以外は値オブジェクトになります。

golang では1ファイルに複数の構造体を記述できますが、エンティティと各値オブジェクト毎に別ファイルに分けておいたほうが設計的にも commit 時の変更差分的にもスマートになります。

project/src/infrastructure 配下(インフラストラクチャ層)

Plain Text

データベースやロガー等の、インフラストラクチャ層に該当するもので構成しています。

infrastructure/datasource 配下は下記ファイルで構成しています。

  • データベースコネクションのインターフェイスである connection.go
  • データベースリソースのインターフェイスである resource.go
  • データベースコネクションのインターフェイスを実装した、PostgreSQL コネクションの構造体である psql_connection.go
  • データベースリソースのインターフェイスを実装した、PostgreSQL リソースの構造体である psql_resource.go
  • クエリなどの実行結果の構造体を実装している result.go

このように設計しておくことにより、データベースの変更やアップデート時の影響が限定的になります。

infrastructure/http 配下は下記ファイルで構成しています。

  • HTTP リクエストのインターフェイスと構造体を実装している request.go
  • HTTP レスポンスのインターフェイスと構造体を実装している response.go
  • HTTP ステータスの構造体を実装している status.go

後述の project/src/runtime/router.go が、project/src/interface/web 配下のコントローラやミドルウェアに引き渡すための HTTP リクエスト/レスポンスの構造体で構成しています。
gin などの HTTP フレームワークを使う際にも、この構造体に変換しておくことにより、フレームワークの変更やアップデート時の影響を限定的になります。

infrastructure/log 配下は下記ファイルで構成しています。

  • ロガーのインターフェイスである logger.go
  • ロガーのインターフェイスを実装している、ログをファイルに書き出す file_logger.go
  • ロガーのインターフェイスを実装している、ログを標準出力に書き出す stdout_logger.go

ロガーもインターフェイスで定義しておくこにより、docker で動かす場合やテスト時に標準出力への書き出しが行えるようになります。
どちらのロガーを実行時に使うかについては、後述の project/src/runtime/dependency.go でうまく切り替えるように実装しています。

infrastructure/repository 配下は下記ファイルで構成しています。

  • 先述の project/src/domain/company/repository.go を実装している company.go
  • 先述の project/src/domain/employee/repository.go を実装している employee.go

ドメイン層で定義したデータベース上のCRUD操作のインターフェイスを実装した構造体で構成しています。
先述の infrastructure/datasource 内のデータベースコネクションとデータベースリソースのインターフェイスを受け取るようにしておくことで、モックへの差し替えでデータベースを立ち上げなくてもテストコードが実行できるようになります。

project/src/interface 配下(プレゼンテーション層)

Plain Text

今回の例では REST API なので、コントローラとミドルウェアでプレゼンテーション層を構成しています。
コントローラとミドルウェアは、下記のインターフェイスを受け取るように実装しておくことでモックへの差し替えが可能となり、コントローラであってもテストコードを記述することができます。

  • インフラストラクチャ層で定義した、HTTP リクエストと HTTP レスポンスのインターフェイス
  • ユースケース(アプリケーション)層で定義した、各ユースケースのインターフェイス

interface/web/controller 配下は下記ファイルで構成しています。

  • コントローラのインターフェイスである controller.go
  • コントローラのインターフェイスを実装している、会社情報の取得/登録/更新/削除それぞれのコントローラの company ディレクトリ配下のファイル
  • コントローラのインターフェイスを実装している、社員情報の取得/登録/更新/削除それぞれのコントローラの employee ディレクトリ配下のファイル

コントローラのインターフェイスを定義しておくことにより、後述の project/src/runtime/router.go で取り扱いが楽になります。
API 単位でコントローラを作成することによりファイル数は増えてしまいますが、変更の影響がそのファイル内で完結(社員情報を更新 API を変更する場合、employee/update.go のみの変更となる)しますし、API の追加/削除やパス変更に対しても強くなるためお勧めです。

interface/web/middleware 配下は下記ファイルで構成しています。

  • ミドルウェアのインターフェイスである middleware.go
  • ミドルウェアのインターフェイスを実装している、URL パラメータで指定された要素をマスクするミドルウェアの value_mask.go

コントローラと同様の理由で、ミドルウェアのインターフェイスも定義しています。
ミドルウェアの場合は複数のコントローラに適用されたりするため、ディレクトリで分けたりはしていません。

project/src/runtime 配下

Plain Text

実行する上で必要な情報などで構成しています。

下記3つのファイルで構成しています。

  • procjet/setting/config.json の内容を読み取り構造体化する config.go
  • DI コンテナを作成する dependency.go
  • procjet/setting/router.json の内容を読み取り REST API のルーティングを行い、REST API サーバ化する router.go

dependency.go が procjet/setting/config.json のファイルパスを config.go に渡し、config 構造体を生成し、各構造体の生成を行い DI コンテナに格納していきます。
その際に procjet/setting/router.json のファイルパスを router.go に渡し、REST API のルーティングを行っています。

なお、config 構造体を他の構造体にそのまま渡すと、その構造体が config 構造体に依存してしまうため、dependency.go 内で他の構造体を生成する場合には config の値そのものを渡すようにしましょう。

project/src/usecase 配下(ユースケース(アプリケーション)層)

Plain Text

プレゼンテーション層とドメイン層(必要に応じてインフラストラクチャ層にも)の橋渡しをするためのユースケースを、インターフェイスと構造体の実装(各ファイル内にインターフェイスと構造体の実装を記述)でユースケース(アプリケーション)層を構成しています。

先述のドメイン層で定義したデータベース上のCRUD操作のインターフェイスと、インフラストラクチャ層で定義したデータベースコネクションとデータベースリソースのインターフェイスを受け取るようにしておくことで、モックへの差し替えで容易にテストコードを記述できます。

ドメインモデルを操作することになるため、基本的にはドメインモデル毎にディレクトリで分けています。
しかしながら、社員情報と会社情報を同時に取得したりする場合などの他ドメインモデルも同時に処理を行う場合は、新たにディレクトリを作成しそこにユースケースを追加するのも良いかと思います。

前述の project/src/interface 配下でも記載した通り、変更の影響を限定的にするために、操作ごとにファイルを分けています。

まとめ

今回説明した設計の場合、ご覧の通りファイル数が多くなり初期実装コストが高くなりがちです。
しかしながら一つ一つのファイルがシンプルかつ責任範囲が明確となるため、変更時の影響を特定のファイルに限定することができますし、またテストコードも project/main.go と project/src/runtime/dependency.go 以外ならすべて簡単に書けますので、保守性がかなり向上します。
高機能なフレームワークを使うのも手ですが、学習コストが高いのとフレームワーク側の事情に振り回されるのが辛いので、今回このような設計の記事を書いてみました。
これが正解と言うわけではありませんので、この内容を参考によりよい設計を行っていただければ幸いです。

おまけ:全体のファイル構成

Plain Text

uzuki

Posted by uzuki