7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クリーンアーキテクチャの各パーツをコードで理解する

7
Last updated at Posted at 2026-03-01

ゴール

本記事の目的は、クリーンアーキテクチャを構成する以下の各パーツについて、その役割を自分の言葉で説明できるようになることである。
image.png

本記事で使うプロジェクトの構成図
image.png

事前知識

ドメイン駆動設計とは

本記事では以下を定義とする。

対象領域(ドメイン)をモデル化し、そのモデル(ドメインオブジェクト)を使ってシステムを実現する考え方

image.png

クリーンアーキテクチャとは

本記事では以下を定義とする。

ドメイン駆動設計により定義されたドメインオブジェクトを中心に据え、外部要素(DB、UI、フレームワーク等)から独立させるソフトウェア設計思想

一言で言えば、「流行りのフレームワークやDBの都合に、システムの核心であるビジネスルールを振り回されないようにする」ための整理術である。
image.png

各パーツの解説

1. ドメイン層

値オブジェクト

image.png
「名前」「メールアドレス」「金額」など、それ自体が意味を持つ値である。システムの中で「正しい形式であること」を保証し、単なる文字列や数字以上の意味を持たせる。

項目 内容
役割 不正な値(空文字、不正な形式のメールアドレスなど)がシステムに入り込むのを防ぎ、値の比較を容易にする。
具体例(ユーザー名) 文字数制限や使用可能文字のルールを値オブジェクト自体に持たせ、生成時にバリデーションを行うことで「常に正しい名前」であることを担保する。
具体例(メールアドレス) 「@」が含まれているかなどの形式チェックを内包する。一度生成された後は中身が変更されない(不変)ため、安心して使い回せる。

コードによる解説

値オブジェクトは、単なるデータの入れ物(構造体)ではない。それ自体がビジネスルールを内包し、守るべき「規律」を持ったオブジェクトである。

では、どのようにしてオブジェクトにルールを持たせるのだろうか。その鍵は、生成過程にある。

例えば、以下の id.go を見てほしい。単に type ID int と定義しただけで終わらせてしまえば、それはただの数値型に過ぎない。この型を「値オブジェクト」たらしめているのは、コンストラクタとなる NewID 関数の存在である。

値オブジェクトは、必ずこの NewID 関数を経由して生成される。そして、この生成の瞬間に厳格なルールを課すのである。id.go の場合、以下のような制約を設けている。

if value < 0 {
    return 0, errors.New("ID must be non-negative")
}
return ID(value), nil

このように、不正な値での生成を断固として拒否することで、システム内に「負のID」が紛れ込む隙を失わせる。

また、username という値オブジェクトであれば、以下のようなルールを課して生成を行う。

if utf8.RuneCountInString(value) < 3 || utf8.RuneCountInString(value) > 32 {
    return "", errors.New("username must be 3-32 chars")
}
return Username(value), nil

「3文字以上32文字以内」というビジネス上の制約を、型そのものが背負う形となる。

このように、生成時にバリデーションを強制し、一度作られたらその正しさが保証され続ける。これこそが、値オブジェクトが単なる構造体を超えて「ルールを背負ったオブジェクト」である理由である。

エンティティ

image.png
「ユーザー」や「注文」など、一意の識別子(ID)を持つオブジェクト。システムの中で「誰が」「何が」を特定し、その状態を管理するための中心的な存在である。

項目 内容
役割 属性(名前や住所など)が変化しても、特定の個体として一貫して追跡し続ける必要がある概念を表現する。
具体例(ユーザー) ユーザーが「名前」を変更したとしても、システム上では「同じ人物(同一ID)」として認識し続け、過去の注文履歴などを紐付けたままで管理する。
具体例(注文) 「未発送」から「発送済み」へステータスが変化しても、その「注文そのもの(注文番号)」は変わらず、一つの取引として成立し続ける。

コードによる解説

エンティティも値オブジェクトと同様、構造体を直接インスタンス化することはせず、必ず NewUser 関数のような生成用関数を経由させる。この関数内で、構成要素となる各値オブジェクトの生成(バリデーション)を強制するのである。

func NewUser(id int, username, email, hashedPassword string) (*User, error) {
    // 各値オブジェクトの生成時にルールを課し、不正な値があればその時点で拒否する
    uid, err := value_objects.NewID(id)
    if err != nil {
        return nil, err
    }
    uname, err := value_objects.NewUsername(username)
    if err != nil {
        return nil, err
    }
    // ...中略...
    return &User{
        ID:             uid,
        Username:       uname,
        Email:          emailVO,
        HashedPassword: hashVO,
    }, nil
}

このように、エンティティは「ルールを通過した値オブジェクト」のみを受け入れることで、オブジェクト自身の正当性を担保している。

エンティティ最大の特徴は、システムが終了しても消えないよう「永続化」される必要がある点である。そのため、エンティティには必ず「リポジトリ」という、データの保存・取得を担う部品がセットで必要になる。

しかし、ここで一つ問題が生じる。リポジトリの正体はデータベース(MySQLやPostgreSQLなど)であり、これらは具体的な技術詳細(外側の層)である。クリーンアーキテクチャの原則に従えば、中心部であるドメイン層に具体的な技術を持ち込むことはできない。

そこで、ドメイン層ではインターフェース(約束事)だけを定義する。

type UserRepository interface {
    Create(user *User) (*User, error)
    FindByID(id value_objects.ID) (*User, error)
    // ...中略...
    Update(user *User) error
}

このように、ドメイン層では「何ができるか(メソッドの形)」だけを決めておき、「どうやるか(DBへの書き込み)」の実装は必ず外側の層で行うよう約束させるのである。

ドメインサービス

image.png
値オブジェクトやエンティティに持たせると不自然なロジックの受け皿。イメージとしては、エンティティたちがある特定の目的(認証や重複確認など)のために、外部の知恵を借りるための「専門サービス」である。

項目 内容
役割 個々のエンティティが自分自身の情報だけでは完結できない、システム全体やルールを跨ぐ計算・照合を行う。
具体例(認証) ユーザー(エンティティ)が、入力されたパスワードが正しいかを判定するために「認証サービス(AuthDomainService)」を使用し、その結果を受け取る。
具体例(重複) 新規ユーザーが、自分の希望する名前が既に使われていないかを「ユーザー重複確認サービス」を使用してチェックする。

コードによる解説

2. ユースケース層

ユースケース

image.png
「ユーザーを登録する」「商品を注文する」といった、システムの具体的な「機能」を実現する場所である。ドメイン層が「ルール」なら、ユースケース層はそれらを組み合わせて目的を果たす「手順(シナリオ)」を記述する。

項目 内容
役割 ドメインオブジェクト(エンティティやサービス)を呼び出し、一つの業務目的を達成するための一連の処理の流れをコントロールする。
具体例(ユーザー登録) 1. 入力値のバリデーション、2. ドメインサービスによる重複チェック、3. エンティティの生成、4. リポジトリによる保存、という手順を指揮する。
具体例(注文処理) 在庫の確認、注文エンティティの作成、支払処理の実行、配送予約といった、複数のドメイン知識を跨ぐ「一連のフロー」を完結させる。

コードによる解説

ユースケースオブジェクトは、ドメインオブジェクト(値オブジェクト、エンティティ、ドメインサービス)とインターフェース(リポジトリ、プレゼンター)を駆使しながら、そのユースケースが実現したいシナリオ(今回ならユーザー作成)を組み立てる。

プレゼンターはユースケースの結果を「外の世界」へ伝える役割を持つが、ユースケース自体が特定の技術(JSONやHTMLなど)に依存しないよう、以下のようにインターフェースとして定義する。

type UserSignupPresenter interface {
	Output(user *entities.User, token string) *UserSignupOutput
}

ユースケースのコードにおいて最も難解なインタラクター(今回におけるuserSignupInteractor)については、以下の記事で解説しているため、ここでは省略する。

コード内の以下のユースケース入出力の定義を見て、疑問を抱いたかもしれない。

type UserSignupInput struct {
	Username string
	Email    string
	Password string
}

type UserSignupOutput struct {
	ID    int
	Token string
}

先ほど値オブジェクトとして Username や Email、ID などをすべて定義したにもかかわらず、なぜ改めて string 型や int 型で定義し直しているのかと、違和感を覚えるはずだ。

その理由を理解するには、まずユースケースオブジェクトの役割を把握する必要がある。

以下はユースケースオブジェクトのイメージ図だ。

image.png

ユースケースオブジェクトの本来の役割は、ドメインオブジェクト(値オブジェクト、エンティティ、ドメインサービス)を組み合わせて、システムとしての具体的な機能(ユーザー登録など)を実行することにある。

しかし、ここで思い出してほしいのは、ユースケースオブジェクトが「ドメイン世界」と「現実世界」のちょうど境界線に位置しているという点だ。

コンピュータの中の概念であるドメインオブジェクトは、ビジネスルールを守るために「特殊な型」をしているが、私たち人間やブラウザがやり取りできるのは、もっと一般的で扱いやすい 「数値(int)」や「文字列(string)」などのプリミティブ型 だけである。

つまり、ユースケースオブジェクトという機能を外から実行しようとしたとき、その入り口(入力)と出口(出力)だけは、現実世界でも話が通じる言葉で用意されていなければならないのだ。

以下の Input は、現実世界側の入力、つまりユースケースオブジェクトが機能を開始するために受け取れる「現実世界の言葉」である。

type UserSignupInput struct {
	Username string
	Email    string
	Password string
}

そして、以下の Output は、機能が完了したあとに返される「現実世界側の出力」である。

type UserSignupOutput struct {
	ID    int
	Token string
}

「なぜドメインオブジェクトを使わずに、わざわざ string や int に戻しているのか?」

その問いの答えは、現実世界からユースケースを呼び出し、その結果を私たちが正しく受け取るためには、入り口と出口が「共通言語」であるプリミティブ型で定義されている必要があるから

image.png

3. アダプタ層

この層は、現実世界(HTTPリクエストなど)と内部のビジネスロジック(ユースケース)の間で、データの形式を翻訳する役割を担う。

コントローラー

image.png
外部(ブラウザやAPIクライアント)からのリクエストを一番に受け取る窓口である。

項目 内容
役割 HTTPリクエストなどの外部入力を受け取り、ユースケースが理解できるデータ形式(Input DTO)に整理して橋渡しを行う。
具体例(入力) JSON形式で送られてきたユーザー情報を解析(パース)し、UserSignupInput 構造体に詰め替えてユースケースの実行関数へ渡す。
具体例(交通整理) 認証トークンの有無を確認したり、リクエストが正しい形式でない場合に早期にエラーを返したりする「受付室」のような振る舞いをする。

コードによる解説

コントローラーは、後ほど解説する「ルーター」というオブジェクトから送られてくる情報を、ユースケースオブジェクトが要求する形式(先述のUserSignupInput)に変換した上で、ユースケースを実行し、その結果をルーターへと返却する役割を担う。

「そのままルーターに返してしまって問題ないのか」と疑問に思うかもしれない。しかし、先ほどのユースケースオブジェクトの内部において、プレゼンターを通じてすでに現実世界の形式へと変換されているため、そのまま返却しても支障はないのである。

プレゼンター

image.png
ユースケースの実行結果を受け取り、クライアントが表示しやすい形式に変換する担当である。

項目 内容
役割 ユースケースから返された出力(Output DTO)を、最終的なレスポンス形式(JSON、HTML、XMLなど)に整形して返す。
具体例(JSON変換) 登録完了したユーザーのIDやトークンを、「成功ステータス」と共に綺麗なJSONフォーマットに整えてレスポンスとして出力する。
具体例(多言語・形式対応) 同じ実行結果であっても、スマホアプリ向けにはJSON、ブラウザ向けにはHTMLといったように、出力先に応じた「見せ方」を調整する。

コードによる解説

プレゼンターはドメイン世界の情報を現実世界の情報(ユースケースのコード先述のUserSignupOutput)に変換するオブジェクトである。

以下は先ほどのイメージ図。
image.png

4. インフラストラクチャ層

この層は、データベースやWebフレームワーク、外部APIといった「具体的な技術」が配置される、最も外側の層である。内側の層(ドメイン層やユースケース層)が決めた「ルール」や「約束(インターフェース)」を、現実の技術を使って実現する役割を担う。

ルーター

image.png
ルーターは単なるURLの分岐点ではない。クリーンアーキテクチャにおいて、バラバラだった各パーツに命を吹き込み、一つのシステムとして連結する(DI:依存性注入)最も重要な場所である。

項目 内容
役割(組み立て) 各層のパーツを生成し、インターフェースという「穴」に具体的な実装を流し込む(DI)。
役割(配線) HTTPメソッドやURLパスを確認し、組み立て済みのコントローラーを呼び出す。
技術依存 EchoGin といった具体的なフレームワークを使い、通信の入り口を管理する。

コードによる解説

ルーターは単なるパスの分岐点ではない。クリーンアーキテクチャにおいて、各層でバラバラに定義されたパーツを結合し、システムとして命を吹き込む「組み立て工場」の役割を担う。

具体的には、ドメイン層やユースケース層で定義したインターフェース(リポジトリ、ドメインサービス、プレゼンター)に対して、インフラ層やアダプタ層で作った「具体的な実装」をDI(依存性注入)していく場所である。

ここで、「DI(依存性注入)とは何か」という疑問が浮かぶはずだ。

クリーンアーキテクチャにおける「依存性」の正体は、MySQLやPostgreSQL、あるいは外部APIといった「具体的な技術」そのものである。
ドメイン層では「データを保存する」というインターフェース(メソッドの約束事)だけを決めておき、中身は空っぽにしておく。その空っぽの約束事に対して、「今回はPostgreSQLを使ってこう実現する」という具体的な技術を流し込み、実体を持たせる。

これこそが技術(依存性)の注入である。

以下はイメージ図
image.png

コードでは以下の部分がわかりやすい。

// 1. インフラ層(Repository / Domain Impl)の初期化
spotRepo := postgres.NewSpotRepository(db)
postRepo := postgres.NewPostRepository(db)
userRepo := postgres.NewUserRepository(db)

各パーツを実装後、それらを使用してユースケースを初期化し、さらにそのユースケースをコントローラーに渡し、最終的にHTTPパスへと紐付ける。

リポジトリ

image.png
データの保存や取得の実務を担当する「裏方」である。

項目 内容
役割 ドメイン層で定義されたインターフェース(UserRepository等)を実装し、具体的な保存処理を行う。
具体例(DB操作) SQLを発行してMySQLにデータを保存したり、Redisからキャッシュを取得したりといった「生臭い」処理を一手に引き受ける。
具体例(隠蔽) ユースケース層に対しては「データがどこにあるか」を意識させず、まるでメモリ上にデータがあるかのように振る舞う。

コードによる解説

先ほどエンティティの層でインターフェースとして定義したリポジトリを、ここで具体的に実装している。今回はPostgreSQLを用いて実装しているが、他のどのようなデータベースであっても同様に実装が可能である。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?