はじめに
こんにちは!
2012年にGoがリリースされてから、2024年時点で12年。
基礎となるクリーンアーキテクチャについて、簡単に振り返ってみます。
1. なぜクリーンアーキテクチャが必要なの?
これを疑問に感じた方は、少なくないはず。
構造をそこまで意識する必要があるのか?
比較的小規模なプロダクトであれば、そんなに意識しなくてもいいと思います。
マイクロサービスとして活用している場合など、なおさら感じるのではないでしょうか。
しかし、比較的大規模なサービスなどで機能が増えて大きくなっていくと、変更が難しく、新しい機能を追加するのも大変になってきますね。
そこで登場するのが「クリーンアーキテクチャ」です。
クリーンアーキテクチャは、プログラムを役割ごとに整理することで、
- 変更に強くなる
- テストしやすくなる
- チーム開発がスムーズになる
などのメリットがあります。
機能が増えていく前提で考えると、分かりやすいかもしれませんね。
2. 玉ねぎ🧅で理解するクリーンアーキテクチャ
クリーンアーキテクチャは、まるで玉ねぎ🧅のように層になっていて、それぞれの層が役割を持っています。
今回は、以下の4つの層で中心から順に構成を考えてみましょう。
-
Entities (エンティティ):
アプリケーションの中心となるデータ構造やビジネスルールを表現する。
どの層にも依存しない。 -
Use Cases (ユースケース):
アプリケーションの具体的な処理を行う。
エンティティを操作するが、DBや外部APIには依存しない。 -
Interface Adapters (インターフェースアダプター):
DBや外部APIとのやり取りを担当。
ユースケースからの指示を受けて、具体的な処理を行う。 -
Frameworks & Drivers (フレームワークとドライバ):
DBやWebフレームワークなど。
具体的な技術に依存する部分を扱う。
ポイントは、「依存関係の方向」。
外側の層は内側の層に依存しますが、内側の層は外側の層に依存しません。
3. 実際にコードを書いてみる
簡単なタスク管理アプリを例に、クリーンアーキテクチャを適用してみます。
3.1 エンティティ (Entities):
アプリケーションの中心となるデータ構造やビジネスルールを表現する。
どの層にも依存しない。
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には依存しない。
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とのやり取りを担当。
ユースケースからの指示を受けて、具体的な処理を行う。
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フレームワークなど。
具体的な技術に依存する部分を扱う。
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言語でクリーンアーキテクチャを始めるための基本的な考え方と、簡単な例をまとめました。
最初は少し難しく感じるかもしれませんが、クリーンアーキテクチャを導入することで、より保守性・拡張性の高いアプリケーションを開発できるようになります。
将来的な拡張性を意識して、より保守性・拡張性の高いプロダクトにしていきましょう!