Goのクリーンアーキテクチャ振り返り

# はじめに
こんにちは！

2012年にGoがリリースされてから、2024年時点で12年。
基礎となるクリーンアーキテクチャについて、簡単に振り返ってみます。

### 1. なぜクリーンアーキテクチャが必要なの？

これを疑問に感じた方は、少なくないはず。

構造をそこまで意識する必要があるのか？

比較的小規模なプロダクトであれば、そんなに意識しなくてもいいと思います。
マイクロサービスとして活用している場合など、なおさら感じるのではないでしょうか。

しかし、比較的大規模なサービスなどで機能が増えて大きくなっていくと、変更が難しく、新しい機能を追加するのも大変になってきますね。

そこで登場するのが「クリーンアーキテクチャ」です。

クリーンアーキテクチャは、プログラムを役割ごとに整理することで、

* **変更に強くなる**
* **テストしやすくなる**
* **チーム開発がスムーズになる**

などのメリットがあります。

機能が増えていく前提で考えると、分かりやすいかもしれませんね。

### 2. 玉ねぎ🧅で理解するクリーンアーキテクチャ

クリーンアーキテクチャは、まるで玉ねぎ🧅のように層になっていて、それぞれの層が役割を持っています。

今回は、以下の4つの層で中心から順に構成を考えてみましょう。

* **Entities (エンティティ):** 
アプリケーションの中心となるデータ構造やビジネスルールを表現する。
どの層にも依存しない。
* **Use Cases (ユースケース):** 
アプリケーションの具体的な処理を行う。
エンティティを操作するが、DBや外部APIには依存しない。
* **Interface Adapters (インターフェースアダプター):** 
DBや外部APIとのやり取りを担当。
ユースケースからの指示を受けて、具体的な処理を行う。
* **Frameworks & Drivers (フレームワークとドライバ):** 
DBやWebフレームワークなど。
具体的な技術に依存する部分を扱う。

![クリーンアーキテクチャの層](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407770/ed2dfb78-da7c-b50d-3aec-650ad941ba47.png)


ポイントは、「依存関係の方向」。
外側の層は内側の層に依存しますが、内側の層は外側の層に依存しません。 

### 3. 実際にコードを書いてみる

簡単なタスク管理アプリを例に、クリーンアーキテクチャを適用してみます。

**3.1 エンティティ (Entities):**
アプリケーションの中心となるデータ構造やビジネスルールを表現する。
どの層にも依存しない。
```Go
package entity

// Task はタスクを表す構造体
type Task struct {
	ID          int    `json:"id"`
	Title       string `json:"title"`
	Description string `json:"description"`
	Completed   bool   `json:"completed"`
}
```

**3.2 ユースケース (Use Cases):**
アプリケーションの具体的な処理を行う。
エンティティを操作するが、DBや外部APIには依存しない。
```Go
package usecase

import "github.com/myname/myproject/entity"

// タスクに関するビジネスロジックを定義するインターフェース
type TaskUseCase interface {
	GetTask(id int) (*entity.Task, error)
	CreateTask(title, description string) (*entity.Task, error)
	// 他のタスク操作
}

// TaskUseCase インターフェースを実装する構造体
type taskUseCase struct {
	// DB 操作などを担当するインターフェース
	TaskRepository TaskRepository
}

// TaskUseCase のインスタンスを生成
func NewTaskUseCase(repo TaskRepository) TaskUseCase {
	return &taskUseCase{TaskRepository: repo}
}

// GetTask は ID を元にタスクを取得する
func (tu *taskUseCase) GetTask(id int) (*entity.Task, error) {
	// タスクの取得を TaskRepository に依頼する
	return tu.TaskRepository.GetTask(id)
}

// CreateTask は新しいタスクを作成する
func (tu *taskUseCase) CreateTask(title, description string) (*entity.Task, error) {
	// タスクの作成を TaskRepository に依頼する
	task := &entity.Task{
		Title:       title,
		Description: description,
	}
	return tu.TaskRepository.CreateTask(task)
}
```

**3.3 インターフェースアダプター (Interface Adapters):**
DBや外部APIとのやり取りを担当。
ユースケースからの指示を受けて、具体的な処理を行う。
```Go
package repository

import (
	"database/sql"

	"github.com/myname/myproject/entity"
)

// TaskRepository はタスクの永続化を抽象化するインターフェース
type TaskRepository interface {
	GetTask(id int) (*entity.Task, error)
	CreateTask(task *entity.Task) (*entity.Task, error)
	// 他の DB 操作
}

// TaskRepository インターフェースを実装する構造体
// DB 接続を保持し、実際の DB 操作を行う
type dbTaskRepository struct {
	DB *sql.DB
}

// dbTaskRepository のインスタンスを生成
func NewDBTaskRepository(db *sql.DB) TaskRepository {
	return &dbTaskRepository{DB: db}
}

// GDB から ID を元にタスクを取得する
func (d *dbTaskRepository) GetTask(id int) (*entity.Task, error) {
	// SQL クエリの実行
	row := d.DB.QueryRow("SELECT * FROM tasks WHERE id = ?", id)

	task := &entity.Task{}
	err := row.Scan(&task.ID, &task.Title, &task.Description, &task.Completed)
	if err != nil {
		return nil, err
	}

	return task, nil
}

// DB に新しいタスクを作成する
func (d *dbTaskRepository) CreateTask(task *entity.Task) (*entity.Task, error) {
	// SQL クエリの実行
	result, err := d.DB.Exec("INSERT INTO tasks (title, description) VALUES (?, ?)", task.Title, task.Description)
	if err != nil {
		return nil, err
	}

	// 作成したタスクの ID を取得
	lastInsertID, err := result.LastInsertId()
	if err != nil {
		return nil, err
	}
	task.ID = int(lastInsertID)

	return task, nil
}
```

**3.4 フレームワークとドライバ (Frameworks & Drivers):**
DBやWebフレームワークなど。
具体的な技術に依存する部分を扱う。
```Go
func main() {
	// DB 接続情報
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		"ユーザー名",
		"パスワード",
		"ホスト名",
		"ポート番号",
		"データベース名",
	)

	// DB 接続
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		panic(err.Error())
	}
	defer db.Close()

	// リポジトリ、ユースケースを初期化
	taskRepo := repository.NewDBTaskRepository(db)
	taskUseCase := usecase.NewTaskUseCase(taskRepo)

	router := gin.Default()

	// タスク取得 API
	router.GET("/tasks/:id", func(c *gin.Context) {
		// パラメータからタスク ID を取得
		id, err := strconv.Atoi(c.Param("id"))
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid task ID"})
			return
		}

		// ユースケースを使ってタスクを取得
		task, err := taskUseCase.GetTask(id)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusOK, task)
	})

	// タスク作成 API
	router.POST("/tasks", func(c *gin.Context) {
		// リクエストボディからタスク情報を読み込み
		var task entity.Task
		if err := c.ShouldBindJSON(&task); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// ユースケースを使ってタスクを作成
		createdTask, err := taskUseCase.CreateTask(task.Title, task.Description)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusCreated, createdTask)
	})

	// サーバー起動
	router.Run(":8080")
}
```

### 4. まとめ

今回は、Go言語でクリーンアーキテクチャを始めるための基本的な考え方と、簡単な例をまとめました。

最初は少し難しく感じるかもしれませんが、クリーンアーキテクチャを導入することで、より保守性・拡張性の高いアプリケーションを開発できるようになります。

将来的な拡張性を意識して、より保守性・拡張性の高いプロダクトにしていきましょう！