Golang開発者のためのクリーンアーキテクチャ
はじめに
クリーンアーキテクチャは、ソフトウェア設計の分野で非常に重要な概念です。
しかし、その理解は容易ではなく、明確な正解が存在するわけではありません。
多くの人が異なる解釈を持ち、他の設計思想と混在していることもあります。
この記事では、自分なりの視点からクリーンアーキテクチャを解釈し、その整理した内容を共有します。
このアーキテクチャの目的は、システムの各層を独立させ、変更に強く、テストしやすい設計を実現することです。
この記事では、クリーンアーキテクチャの基本概念、Golangでの実装方法、およびディレクトリ構成について詳しく説明します。
なお、この記事では個人的な見解を述べており、必ずしも正解を書いているわけではありません。もし誤りがあれば、ぜひご指摘いただけると幸いです。
クリーンアーキテクチャの基本概念
クリーンアーキテクチャの元となったのは、ロバート・C・マーチン(通称「アンクルボブ」)によるThe Clean Code Blogです。
特に以下の図は、クリーンアーキテクチャの依存関係を示すものとしてよく知られています。
引用元: The Clean Code Blog
この図が示す通り、クリーンアーキテクチャは複数の同心円で表され、それぞれが異なるレイヤーを表しています。
各レイヤーは、内側のレイヤーに依存し、外側のレイヤーからは独立しています。
インフラストラクチャ(Infrastructure)
インフラストラクチャ層は、外部システムやフレームワークとの連携を扱います。
例:MySQL接続
package infrastructure
func NewDB() *sql.DB {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
log.Fatal(err)
}
if err := db.Ping(); err != nil {
log.Fatal(err)
}
return db
}
コントローラー(Controller)
コントローラー層は、外部からのリクエストを受け取り、適切なユースケースを呼び出し、レスポンスを生成する役割があります。
この層では、入力値のバリデーションやエラーハンドリングも行います。
コントローラーにはビジネスロジックを含まず、処理はすべてUsecaseに移譲されます。
例:Echoフレームワークを使用したコントローラーの実装
package controllers
type UserController struct {
updateUserNameInteractor usecases.IUpdateUserNameInteractor
userPresenter presenters.IUserPresenter
errorPresenter presenters.IErrorPresenter
}
func NewUserController(
updateUserNameInteractor usecases.IUpdateUserNameInteractor,
userPresenter presenters.IUserPresenter,
errorPresenter presenters.IErrorPresenter,
) *UserController {
return &UserController{
updateUserNameInteractor: updateUserNameInteractor,
userPresenter: userPresenter,
errorPresenter: errorPresenter,
}
}
func (c *UserController) UpdateUserName(e echo.Context, userID string) error {
req := api.UpdateUserNameRequest{}
if err := e.Bind(&req); err != nil {
return c.errorPresenter.PresentBadRequest(e, "invalid request")
}
input := &input.UpdateUserNameInput{
UserID: userID,
NewName: req.Name,
}
ctx := e.Request().Context()
output, err := c.updateUserNameInteractor.Execute(ctx, input)
if err != nil {
return c.errorPresenter.PresentInternalServerError(e, err)
}
return c.userPresenter.PresentUpdateUserName(e, output)
}
プレゼンター(Presenter)
プレゼンター層は、ユースケースの出力を受け取り、それをユーザーインターフェース(UI)に適した形式に変換する役割を果たします。
この役割には、データのフォーマット変更やエラーメッセージの設定など、レスポンスの生成が含まれます。
例:正常系のレスポンス生成
package presenters
type IUserPresenter interface {
PresentUpdateUserName(c echo.Context, output *output.UpdateUserNameOutput) error
}
type UserPresenter struct{}
func NewUserPresenter() IUserPresenter {
return &UserPresenter{}
}
func (p *UserPresenter) PresentUpdateUserName(c echo.Context, output *output.UpdateUserNameOutput) error {
response := api.UpdateUserNameResponse{
ID: output.User.GetID(),
Name: output.User.GetName(),
}
return c.JSON(http.StatusOK, response)
}
例:共通のエラーレスポンス生成
package presenters
type IErrorPresenter interface {
PresentBadRequest(c echo.Context, message string) error
PresentInternalServerError(c echo.Context, err error) error
}
type ErrorPresenter struct{}
func NewErrorPresenter() IErrorPresenter {
return &ErrorPresenter{}
}
func (p *ErrorPresenter) PresentBadRequest(c echo.Context, message string) error {
response := struct {
Error string `json:"error"`
}{
Error: message,
}
return c.JSON(http.StatusBadRequest, response)
}
func (p *ErrorPresenter) PresentInternalServerError(c echo.Context, err error) error {
response := struct {
Error string `json:"error"`
}{
Error: err.Error(),
}
return c.JSON(http.StatusInternalServerError, response)
}
ユースケース(Use Cases)
ユースケースは、アプリケーション固有のビジネスルールを定義します。
これはシステムがどのように動作するかを決定するもので、ユーザーや他のシステムがどのようにシステムと対話するかを記述します。
Input PortとOutput Port
Input Port と Output Port は、ユースケースの境界を定義するDTO(Data Transfer Object)です。
Input Portはユースケースに必要なデータを提供し、Output Portはユースケースの結果を外部に返します。
Publicメソッドが1つのみになるように設計する
アンクルボブ氏がクリーンアーキテクチャを提唱する際に、明確に「複数のPublicメソッドを持つべきではない」と述べているわけではありません。
しかし、単一責任の原則を遵守するために、UsecaseがPublicメソッドを1つだけ持つように設計することが推奨されます。
Usecaseに複数のPublicメソッドが存在すると責任範囲が広がりすぎ、ビジネスロジックが散逸しやすくなります。
1つのPublicメソッドにすることで、特定のビジネスロジックに集中でき、修正の影響範囲を最小限に抑えることができます。
例:ユーザー登録
package usecases
type IUpdateUserNameInteractor interface {
Execute(ctx context.Context, r *input.UpdateUserNameInput) (*output.UpdateUserNameOutput, error)
}
type UpdateUserNameInteractor struct {
userRepository repositories.IUserRepository
}
func NewUpdateUserNameInteractor(
userRepository repositories.IUserRepository,
) IUpdateUserNameInteractor {
return &UpdateUserNameInteractor{
userRepository: userRepository,
}
}
func (i *UpdateUserNameInteractor) Execute(ctx context.Context, input *input.UpdateUserNameInput) (*output.UpdateUserNameOutput, error) {
user, err := i.userRepository.GetUser(ctx, input.UserID)
if err != nil {
return nil, err
}
user.SetName(input.NewName)
err = i.userRepository.UpdateUser(ctx, user)
if err != nil {
return nil, err
}
output := &output.UpdateUserNameOutput{
User: user,
}
return output, nil
}
エンティティ(Entities)
エンティティは、ビジネスルールやオブジェクトの集合を表します。
これらはシステムの中で最も中心的な存在であり、システムの核となるビジネスロジックを含みます。
ドメインモデルとデータモデルを明確に区別する
データモデルは、データベースのテーブル設計に対応し、永続化されたデータを表します。
ドメインモデルは、ビジネスロジックやルールを反映したものです。
例えば、ユーザーの年齢を扱う場合、年齢は生年月日から計算される派生データであり、ドメインモデルに含まれます。
ドメインモデルは対応するデータモデルがなくても存在することができ、ビジネスロジックを中心に設計されています。
また、ドメインモデルは、不用意なアクセスを防ぐためにフィールドをPrivateにすることを心掛けましょう。
例:データモデル
package models
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Birthdate time.Time `json:"birthdate"`
}
例:ドメインモデル
package entities
type User struct {
id string
name string
age int
}
func NewUser(id int, name string, birthDate time.Time) User {
return User{
id: id,
name: name,
age: calculateAge(birthDate),
}
}
func calculateAge(birthdate time.Time) int {
today := time.Now()
age := today.Year() - birthdate.Year()
if today.YearDay() < birthdate.YearDay() {
age--
}
return age
}
ドメインサービス(Domain Service)
Domainサービスには、複数のドメインにまたがる共通の振る舞いを格納します。
単一のドメインに収まるロジックは、Entitiesに格納するようにします。
これにより、エンティティやユースケースが過度に複雑化するのを防ぐことができ、データ整合性を保ちます。
例:ユーザーサービス
package services
import "errors"
type IUserService interface {
RegisterUser(user *entities.User) error
}
type UserService struct {
userRepository repositories.UserRepository
emailRepository repositories.EmailRepository
}
func NewUserService(
userRepository repositories.UserRepository,
emailRepository repositories.EmailRepository,
) IUserService {
return &UserService{
userRepository: userRepository,
emailRepository: emailRepository,
}
}
func (s *UserService) RegisterUser(user *entities.User) error {
if !user.IsValid() {
return errors.New("invalid user")
}
if err := s.userRepository.Save(user); err != nil {
return err
}
email := &entities.Email{
To: user.Email,
Subject: "Welcome!",
Body: "Thank you for registering!",
}
if err := s.emailRepository.Send(email); err != nil {
return err
}
return nil
}
Serviceはむやみに作成しない
特定のドメインのみに依存し、データを取得するのみの処理の場合は、ユースケースで行い、むやみにService層を作成すべきではありません。
下記のような場面でも、Serviceにメソッドを作成しがちですが、Usecaseで直接処理すべきです。
例:悪い実装方法
func (s *UserService) GetUserByID(id int) (*entities.User, error) {
return s.userRepo.FindByID(id)
}
Service内で別ServiceをDIしない
別のサービスをDIすることは、コードの再利用性を高める一方で、依存関係が複雑になりやすく、循環参照の問題が発生しがちです。個人的には避けることを推奨します。
The Clean Code Blogでは、層の数に制限はなく、必要に応じて層を追加しても良いとされています。
もし層を追加する場合、Facadeのような名前が適しているかもしれません。
リポジトリ(Repository)
例:ユーザーリポジトリ 抽象
domain配下のrepositoriesに格納されています。
package repositories
type IUserRepository interface {
GetUser(ctx context.Context, id string) (*entities.User, error)
UpdateUser(ctx context.Context, user *entities.User) error
}
例:ユーザーリポジトリ 実装
infrastructure配下のrepositoriesに格納されています。
package repositories
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) repositories.IUserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) GetUser(ctx context.Context, id string) (*entities.User, error) {
row := r.db.QueryRowContext(ctx, "SELECT id FROM users WHERE id = ?", id)
user := &models.User{}
err := row.Scan(&user.ID)
if err != nil {
return nil, err
}
return entities.FromDataModel(user), nil
}
func (r *UserRepository) UpdateUser(ctx context.Context, user *entities.User) error {
model := user.ToDataModel()
_, err := r.db.ExecContext(ctx, "UPDATE users SET name = ? WHERE id = ?", model.Name, model.ID)
return err
}
依存性逆転の法則(Dependency Inversion Principle)とは
依存性逆転の法則(DIP: Dependency Inversion Principle)は、ソフトウェア設計の基本原則の一つです。
簡単に言えば、「高レベルのモジュールは低レベルのモジュールに依存しないようにする」という考え方です。
具体的には、以下の2つのルールを含みます
- 高レベルのモジュール(重要なロジックを含む部分)は、低レベルのモジュール(詳細な実装部分)に依存してはいけない。どちらも抽象(インターフェース)に依存するべき。
- 抽象(インターフェース)は具体的な詳細に依存してはいけない。具体的な詳細が抽象に依存するべき。
Repository層での重要性
Repository層では、この依存性逆転の原則が特に重要です。
UsecaseはRepository層に依存しますが、RepositoryはDBの接続をDIしており、Infrastructure層に依存してしまいます。
この問題を解決するために、Usecaseよりも内側の層にRepository層の抽象を置き、実装をInfrastructure層に配置する方法が取られています。
これにより、依存方向が一方向になり、問題が解消されます。
また、他の箇所(UsecaseやServiceなど)でも、抽象を配置していますが、これは各層を疎結合にし、テストを容易にする等の目的があります。
ディレクトリ構成
クリーンアーキテクチャを実装するためのプロジェクト構造は、コードの可読性とメンテナンス性を高めるために重要です。以下は、Golangプロジェクトの一般的なディレクトリ構成の例です。
/your_project
├── main.go
├── domain
│ ├── entities
│ │ └── user.go
│ ├── services
│ │ └── user_service.go
│ └── repositories
│ └── user_repository_interface.go
├── usecases
│ ├── input
│ │ └── update_user_name_input.go
│ ├── output
│ │ └── update_user_name_output.go
│ └── update_user_name_interactor.go
├── infrastructure
│ ├── repositories
│ │ └── user_repository.go
│ ├── models
│ │ └── user_model.go
│ ├── db.go
│ └── router.go
├── controllers
│ └── user_controller.go
├── presenters
│ ├── error_presenter.go
│ └── user_presenter.go
├── api
│ ├── openapi.yaml
│ ├── types.go
│ └── server.go
└── go.mod
ディレクトリの役割
-
main.go
: プロジェクトのエントリーポイント。アプリケーションの初期化と設定を行います。 -
domain
: ビジネスドメインに関するコード。エンティティ、ドメインモデル、ドメインサービス、リポジトリの抽象などが含まれます。-
entities
: ビジネスルールを表すエンティティ。 -
services
: ドメインサービス。 -
repositories
: リポジトリのインターフェース。
-
-
usecases
: アプリケーション固有のビジネスロジック。-
input
: ユースケースへ渡す引数型。 -
output
: ユースケースから返却される型。
-
-
infrastructure
: インフラストラクチャ関連のコード。データベース接続やWebルーターの設定が含まれます。-
models
: データモデル。 -
repositories
: リポジトリの実装。
-
-
controllers
: コントローラー。 -
api
: swaggerからの型生成を想定しています。oapi-codegenによって生成されたtypes.go, server.go)を含みます。
まとめ
クリーンアーキテクチャは、ソフトウェアの設計と開発において多くの利点をもたらします。
特に、変更に強く、テストしやすいシステムを構築するための強力なガイドラインとなります。
この記事を通じて、クリーンアーキテクチャの基本概念とGolangでの実装方法について理解が深まれば幸いです。
以下にサンプルを用意しました。ぜひスターをいただけると嬉しいです。
採用情報
e-dashエンジニアチームは現在一緒にはたらく仲間を募集中です!
同じ夢について語り合える仲間と一緒に、環境問題を解決するプロダクトを作りませんか?
Discussion