はじめに
現在、「顔による新しいマッチングアプローチ」というコンセプトで
アプリケーション開発に取り組んでいます。
その中でログイン機能を実装するにあたり、
UseCase 層の設計・実装を進めていました。
実装を進める中で、Go + クリーンアーキテクチャにおける
「コンストラクタ」の役割について、
自分の中でうまく言語化できていない点がいくつも出てきました。
本記事では、それらの疑問を整理しながら、
Go のコンストラクタで何が起きているのか、
そして クリーンアーキテクチャにおいてどんな意味を持つのか を
自分なりにまとめます。
出てきた疑問
実装を進める中で、次のような疑問が出てきました。
- そもそもGoにおけるコンストラクタは何をしているのか説明できない
- なぜコンストラクタによる初期化が必要なのか
- UserUseCaseのコンストラクタで、validatorのような外部ライブラリを生成してしまって良いのか
この記事では、これらの疑問に対して、Go言語の使用レベルで何が起きているのかを整理し、クリーンアーキテクチャの観点からどう考えると良いかを説明します。
全体コード
type UserUseCaseInterface interface {
// 新規登録
Register(ctx context.Context, input *dto.UserRegisterInput) (*dto.UserOutput, error)
// ログイン
Login(ctx context.Context, input *dto.UserLoginInput) (*dto.AuthTokenOutput, error)
// プロフィール取得
GetProfile(ctx context.Context, userID string) (*dto.UserOutput, error)
// プロフィール更新
UpdateProfile(ctx context.Context, userID string, input *dto.UserUpdateInput) (*dto.UserOutput, error)
}
type UserUseCase struct {
userRepo repository.UserRepositoryInterface
validator *validator.Validate
}
// コンストラクタ
func NewUserUseCase(userRepo repository.UserRepositoryInterface) UserUseCaseInterface {
return &UserUseCase{
userRepo: userRepo,
validator: validator.New(),
}
}
コンストラクタは何をしている関数なのか?
もしコンストラクタを使わずに次のように構造体を生成した場合、
uc := &UserUseCase{}
という状態になる。
これは、正しく動作しない可能性のある状態のフィールド(nilのまま)が
簡単に作れてしまう ということを意味する。
nilだとどう問題が起きるのか?
nilは何も入っていない箱なので、そこに対してメソッドを呼ぶとGoがpanicを起こしてしまう。
例:
var uc *UserUseCase = &UserUseCase{}
この時:
uc.userRepo == nil
// ここでメソッドを呼ぼうとすると
uc.userRepo.Create(ctx, user)
となるが、、、
nil.Create()と存在しない構造体の中のメソッドを呼び出すということになってしまっている。それが結果的にpanicを引き起こしてします。これがコンストラクタをしないことにより起きる弊害です。
これは、内部的には「nil pointer dereference」と呼ばれる状態で、
存在しないメモリアドレスに対してアクセスしようとした結果、
Go が panic を発生させてプログラムを強制終了させるというものです。
ここで、コンストラクタを用意することで、
- UseCaseが動作するために必要な依存関係を必ず渡す
- 正しく初期化された状態で構造体を使用することができる
という制約を課すことができるため、コンストラクタが必要となります。
返り値をInterfaceとしているのはどういうこと?
func NewUserUseCase(userRepo repository.UserRepositoryInterface) UserUseCaseInterface {
return &UserUseCase{
userRepo: userRepo,
validator: validator.New(),
}
}
この関数は、*UserUseCaseという構造体を生成し、それをUserUseCaseInterface型として返している。
Go では、ある構造体が interface に定義されたメソッドをすべて実装していれば、
明示的な宣言がなくても、その interface を満たしたとみなされる。
そのため、*UserUseCaseはUserUseCaseInterfaceに定義されたRegister()/Login()/GetProfile()/UpdateProfile()を実装すればいいよねということになります。
なぜInterfaceを返すのか
これを例えば、Controllerから呼び出しをするという時に、Controller側は、
UserUseCaseInterfaceが提供しているメソッドの振る舞いだけを知ればよく、そのメソッドがどういう処理をしているかの具体的な処理はController側は全く気にしなくてもいいということ。つまり、具体的な実装には依存せず、UserUseCaseInterface という抽象(interface)にのみ依存しているということが分かります。
これにより、UseCaseの実装を何か変更したとしても、interfaceが変わらなければ、Controllerは、変更による影響を受けないことができます。
Controller → UseCaseのinterfaceによって抽象化されたものに依存している。
これはクリーンアーキテクチャにおける
**依存性逆転の原則(DIP)**を満たした設計になります。
UserUseCaseのコンストラクタで、validatorのような外部ライブラリを生成してしまって良いのか
結論から言うと、今回のケースでは問題ない と判断しました。
実際、海外のプロジェクトや実装例を見ても、
UseCase や Service 層で validator のような外部ライブラリを
直接保持しているケースは多く見られました。
重要のは、
外部ライブラリを持つことが良いのか、悪いことなのかよりも、その依存が将来変更・差し替えされる可能性があるかどうかを基準に設計を判断することが大事だと分かりました。
今回は、2つの軸から判断をしました。
① フィールドに持たせ、使い回しが可能
構造体のフィールドを参照するだけでバリデーションを実行でき、
同じ validator を使い回すことができます。
② validatorは今のところ変更可能性が低い
また、validator は現時点では差し替えの必要性が低く、変更可能性もほぼ想定していないため、Interface を使わずに外部ライブラリの型を直接利用しています。外部ライブラリを構造体に含めること自体が問題なのではなく、その依存を将来差し替える必要があるかどうかを考えた上で、この設計は良いかを判断するのが大事みたいですね!
別のValidatorなどを使用するときは、Interfaceでしっかり抽象化していこうと思います。
終わりに
今回は、Go + クリーンアーキテクチャにおける
コンストラクタの役割について整理しました。
「なぜコンストラクタが必要なのか」
「なぜ Interface を返すのか」
を自分の中で言語化することで、
設計の意図や依存関係の考え方を深く理解できたと感じています。
同じように、
「なんとなく書いているけど説明できない、何やっているかよく分からない」
と感じている方の参考になれば嬉しいです。
今後も実装の中で気づいたことや、
理解が曖昧だった部分を整理しながら発信していこうと思います。
以上!!!!!!!!!!!