1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめての Clean Architecture 実践 ーシステム開発の失敗と学び

Posted at

目次

  1. はじめに
  2. 開発における設計の意識ポイント
  3. 試行錯誤フェーズとつまずき
  4. そこから得られた学び
  5. 最終的に定着した設計方法
  6. SOLID原則の適用例
  7. まとめ

はじめに

エンジニアとして初めてクリーンアーキテクチャを意識した設計に挑んだ際、
多くの迷いや失敗を経験しました。
本記事では、実務プロジェクトを例に

  1. 何を意識したか
  2. どこでつまずいたか
  3. 得られた学び
  4. 最終的に定着させた設計方法

を整理し、同じ壁にぶつかった方のヒントにしていただければと思います。

開発における設計の意識ポイント

  • 一方向依存
    各層が「外側から内側へ」だけ依存し、循環参照を絶対に起こさない
  • 責務の明確化 (SRP)
    モジュール/クラスは「変更理由」をひとつに絞る
  • 抽象(ポート)と実装(アダプタ)の分離 (DIP)
    ビジネスロジックはインターフェイスにのみ依存し、具体実装は外部層に閉じ込める
  • YAGNI
    PoC フェーズでは「必要になるまで」層も抽象も増やさない

試行錯誤フェーズとつまずき

フェーズ つまずき/課題 解決策 & 理由
1. レイヤード設計 - usecase ↔ controller が双方向依存
- service 層に I/O が混在
- 依存方向を整理:controller → usecase → domain ← infrastructure に再定義
2. インフラ配置 - SDK や DB モックをどこに置くか迷う - domain/ports に抽象を定義
- infrastructure に具体実装をまとめ、モック注入を容易化
3. ドメイン層の重複 - DTO とドメインモデルの二重定義 - ドメイン層はエンティティ+ポートのみ
- DTO は usecase 層で一元管理
4. CLI エントリ - cmd/clicmd/server を分けるか悩む
- レイヤ数が多くて重い
- CLI 一本化 (cmd/main.go) へ集約
- サーバ起動はオプションで後付け
5. 循環依存防止 - infrastructure が domain を参照しきれず実装が散らばる - DIP 徹底:domain がポートのみ持ち、infra がそれを実装

そこから得られた学び

  1. 依存性逆転 (DIP)
    • Domain 層は「ポート(インターフェイス)」のみ定義
    • Infrastructure 層がそのポートを実装することで、技術的詳細を隔離
  2. 一方向依存の継続確認
    • 実装のたびに 外側 → 内側 をレビューし、逆転を防止
  3. YAGNI の体現
    • PoC 段階では最小限のディレクトリ/抽象のみ用意
    • 永続化やサーバ実装は「要件が発生した時点」で追加
  4. SOLID 原則で責務分離
    • SRP: 各クラス・レイヤはたった一つの理由で変更
    • OCP: インターフェイス経由で後から機能拡張しやすく
    • LSP/ISP: モック/実装を容易に交換可能に

最終的に定着した設計方法

  1. ディレクトリ構成
   project-root/
   ├─ cmd/             ← CLI/サーバ起動
   ├─ internal/
   │   └─ app/
   │       ├─ controller/   ← プレゼンテーション層
   │       ├─ usecase/      ← アプリケーション層 (DTO+ユースケース)
   │       ├─ domain/       ← ドメイン層 (エンティティ+ポート)
   │       └─ infrastructure/ ← インフラ層 (ポート実装)
   └─ configs/         ← 設定読み込み

SOLID原則の適用例

1. 単一責任の原則(SRP)

Domain層はビジネスルールだけを持つ

// domain/planning.go
package domain

type Planning struct {
    Name        string
    ProductMenu ProductMenu
    // ...
}

func (p *Planning) Validate() error {
    // ビジネスルール検証のみ
    if p.ProductMenu != ProductMenuAudienceReach &&
       p.ProductMenu != ProductMenuAIProgramMatch &&
       p.ProductMenu != ProductMenuAudienceReachNonGuaranteed {
           return errors.New("無効なメニュー")
    }
    return nil
}

2.オープン・クローズドの原則(OCP)

インターフェイス経由で拡張可能

// usecase/planning.go
package usecase

import (
    "context"
    "親ディレクトリ/domain"
)

type AIGenerator interface {
    Generate(ctx context.Context, prompt string, schema map[string]interface{}) (PlanningDTO, error)
}

type PlanningUsecase struct {
    aiGenerator AIGenerator  // インターフェイスに依存している
    repository domain.PlanningRepository
}

3.リスコフの置換原則(LSP)

モックと本番実装が同じインターフェイスを満たす

// infrastructure/grpc/planning.go
package grpc

import (
    "context"
    "親ディレクトリ/domain"
)

type PlanningClient struct {
    // gRPCクラインアント設定など
}

func (c *PlanningClient) Save(ctx context.Context, planning *domain.Planning, userUUID, userEmail string) (string, error) {
    if err := c.connect(); err != nil {
        // 接続失敗時はモックでフォールバック
        return c.saveMock(planning)
    }
    // プロダクション実装
    return c.saveToPlanner(ctx, planning, userUUID, userEmail)
}

4.インターフェイス分離の原則(ISP)

必要最小限のメソッドだけを定義

// domain/repository.go
package domain

import "context"

type PlanningRepository interface {
    Save(ctx context.Context, planning *Planning, userUUID, userEmail string) (string, error)
}

5.依存性逆転の原則(DIP)

Domain層がインターフェイスにのみ依存し、Infrastructureが実装

// domain/repository.go
package domain

type PlanningRepository interface {
    Save(ctx context.Context, planning *Planning, userUUID, userEmail string) (string, error)
}
// infrastructure/grpc/planning.go
package grpc

import (
    "context"
    "親ディレクトリ/domain"
)

type PlanningClient struct {
    // ...
}

func (c *PlanningClient) Save(ctx context.Context, planning *domain.Planning, userUUID, userEmail string) (string, error) {
    // gRPC呼び出し実装
    return "", nil
}

最終的に定着した設定方法

1.依存関係のルール

┌─────────────┐
│ Controller  │
└──────┬──────┘
       ↓
┌─────────────┐
│  Usecase    │
└──────┬──────┘
       ↓
┌─────────────┐     ┌──────────────────┐
│   Domain    │ ← ─ │ Infrastructure   │
└─────────────┘     └──────────────────┘

2.開発のフロー

  • usecase/domainのモデル&ポートを先に定義
  • CLIから入力 → controller → usecase → domainでビジネス処理
  • Infrastructureがポートを実装、テスト時はモックを差し替える

まとめ

設計の初期段階で多くの失敗で得たものは、「シンプルな依存関係」と「責務の分離」を徹底することの重要性です。

  • YAGNI:必要になるまで抽象と層を増やさない
  • DIP&一方向依存:Domainを純粋に保ち、技術的詳細を外部層へ
  • SOLID:各原則で責務を切り分け、テストしやすい構造を目指す
    これらを実践すると、保守性・拡張性の高いコードやファイルディレクトリが出来上がると考えました。
1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?