0
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?

はじめに

こんにちは!

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言語でクリーンアーキテクチャを始めるための基本的な考え方と、簡単な例をまとめました。

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

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

0
0
2

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
0
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?