🚀

クリーンアーキテクチャのお話

10 min read

概要

個人開発でクリーンアーキテクチャを実装してみました。理由は、夏のインターンでクリーンアーキテクチャを採用したプロダクトに参加させていただいたため、また、復習を込めてアウトプットしようと思いました。

初投稿で荒削りかもしれませんが、この記事を訪れた方々の役に少しでも立つことができれば幸いです。

はじめに

クリーンアーキテクチャといえば下図を思い浮かべる人が多いと思います。
具体的な説明は様々な人がしているため、今回は簡単に説明しようと思います。
clean.jpg

クリーンアーキテクチャについて詳しく知りたい方へ
Uncle Bob: The Clean Code Blog
Qiita: 実装クリーンアーキテクチャ

クリーンアーキテクチャの特徴

ボブおじさんの記事を読んでみると以下のように記述されています。

  1. Independent of Frameworks. The architecture does not depend on the existence of some library of feature laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints.
  2. Testable. The business rules can be tested without the UI, Database, Web Server, or any other external element.
  3. Independent of UI. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules.
  4. Independent of Database. You can swap out Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database.
  5. Independent of any external agency. In fact your business rules simply don’t know anything at all about the outside world.

Uncle Bob: The Clean Code Blog

簡単に要約すると以下のようになります。

  1. フレームワーク(ソフトウェアのライブラリ)に依存しない。
  2. UI、データベース、ウェブサーバーなどの外部要素を使用せずにテスト可能。
  3. UIに依存しない。システムの他の部分を変更することなく、UIを簡単に変更することができる。
  4. データベースからの独立(データベースに縛られない)。OracleやSQL Serverではなく、MongoやBigTable、CouchDBなどに変更することができる。
  5. 外部のレイヤーに依存しない。内部のレイヤーは外部について何も知らない。

クリーンアーキテクチャを実装して感じたこと

レイヤーごとに責務が分離されているため、テストが明確化する(インターン中)

テストはレイヤーの機能ごとに実装するため、どういうテストを書けばいいか明確にできました。また、責務が分離されている(あるレイヤーの機能が特定の目的を持って実装されている)ので、他のレイヤーに依存せず、最小限のテストを記述することで済みました。

コード量が多くなる

依存方向を一方向にするために、インターフェースや依存性逆転の法則を使用することでコード量が多くなる印象がありました。

実装

今回以下のようなディレクトリ構成で実装しました。ユーザ情報をPOSTで書き換えた後、全Userの情報を返すという実装です。

ディレクトリ構造
src/
├ application/
|   └ main.go
├ domain/
|   └ user.go
├ infrastructure/
|   └ firebase.go
├ interface/
|   ├ controllers/
|   |   └ user_controller.go
|   ├ presenters/
|   |   └ user_presenter.go
|   └ repositories/
|       └ user_repository.go
└ usecase/
    ├ services/
    |   └ user_service.go
    └ usecases/
       ├ user_input_port.go
       ├ user_interactor.go
       └ user_output_port.go

フレームワーク&ドライバー層

この層は一般的に、内部のレイヤーが外部の機能と通信するために必要なコードを記述します。今回、Firestoreを使用したため、Firestoreのアカウントに接続するために必要な処理を記述しました。

infrastructure/firebase.go
package infrastructure

import (
	"context"
	"log"

	"cloud.google.com/go/firestore"
	firebase "firebase.google.com/go"
	"google.golang.org/api/option"
)

func FirebaseInit(ctx context.Context) (*firestore.Client, error) {
	serviceAccount := option.WithCredentialsFile("path/to/serviceAccount.json")
	app, err := firebase.NewApp(ctx, nil, serviceAccount)
	if err != nil {
		log.Fatalln(err)
		return nil, err
	}

	client, err := app.Firestore(ctx)
	if err != nil {
		log.Fatalln(err)
		return nil, err
	}

	return client, nil
}

インターフェース層

この層は、データをユースケースやエンティティに最も適したフォーマットから、データベースやウェブなどの外部に最も適したフォーマットに変換する機能を持ちます。また、その逆も然りで、外部サービスなどの外部形式からユースケースやエンティティが使用する内部形式にデータを変換するために必要な機能も含まれます。

コントローラ

コントローラから直接リポジトリを呼び出さずに、インプットポートを介して(依存性逆転の原則を用いて)依存関係を一方向に限定しています。

interface/controllers/user_controller.go
package controllers

import (
	"example/interface/presenters"
	"example/interface/repositories"
	"example/usecase/usecases"

	"github.com/labstack/echo"
)

type UsersController struct {
	UsersInputPort usecases.UsersInputPort
}

func NewUsersController(e *echo.Echo) *UsersController {
	return &UsersController{
		UsersInputPort: usecases.NewUsersInteractor(
			presenters.NewUsersPresenters(e),
			repositories.NewUsersRepository(e),
		),
	}
}

func (c *UsersController) POST(ec echo.Context) error {
	return c.UsersInputPort.AddUsers(ec)
}

プレゼンター

ここではユースケースのアウトプットポートを汎化した処理を記述しています。

interface/presenters/user_presenter.go
package presenters

import (
	"net/http"

	"example/domain"
	"example/usecase/usecases"

	"github.com/labstack/echo"
)

type UsersPresenters struct {
	echo *echo.Echo
	usecases.UsersOutputPort
}

func NewUsersPresenters(echo *echo.Echo) *UsersPresenters {
	return &UsersPresenters{
		echo: echo,
	}
}

func (p *UsersPresenters) AddUsers(ec echo.Context, user []*domain.User) error {
	return ec.JSON(http.StatusOK, user)
}

リポジトリ

ここでは簡潔に言うと実処理を書く場所です。今回はFirestoreとのやり取りを記述しました。

interface/repositories/user_repository.go
package repositories

import (
	"context"
	"encoding/json"
	"log"

	"example/domain"
	"example/infrastructure"
	"example/usecase/services"

	"github.com/labstack/echo"
)

type UsersRepository struct {
	echo *echo.Echo
	usersService services.UsersService
}

func NewUsersRepository(
	echo *echo.Echo,
) *UsersRepository {
	return &UsersRepository{
		echo: echo,
	}
}

func (r *UsersRepository) AddUsers(user *domain.User) ([]*domain.User, error) {
	ctx := context.Background()
	client, err := infrastructure.FirebaseInit(ctx)
	if err != nil {
		log.Fatal(err)
	}

	// データ追加
	_, err = client.Collection("users").Doc(user.Name).Set(ctx, map[string]interface{}{
		"age":     user.Age,
		"address": user.Address,
	})
	if err != nil {
		log.Fatalf("Failed adding alovelace: %v", err)
	}
	// データ読み込み
	allData := client.Collection("users").Documents(ctx)
	// 全ドキュメント取得
	docs, err := allData.GetAll()
	if err != nil {
		log.Fatalf("Failed adding getAll: %v", err)
	}
	// 配列の初期化
	users := make([]*domain.User, 0)
	for _, doc := range docs {
		// 構造体の初期化
		u := new(domain.User)
		// 構造体にFireStoreのデータをセット
		mapToStruct(doc.Data(), &u)
		// ドキュメント名を取得してnameにセット
		u.Name = doc.Ref.ID
		// 配列に構造体をセット
		users = append(users, u)
	}

	// 切断
	defer client.Close()

	// 成功していればusersに値が、失敗の場合はerrに値が入る
	return users, err
}

func mapToStruct(m map[string]interface{}, val interface{}) error {
	tmp, err := json.Marshal(m)
	if err != nil {
		return err
	}
	err = json.Unmarshal(tmp, val)
	if err != nil {
		return err
	}
	return nil
}

ユースケース層

この層には、アプリケーション固有のビジネスルールが記述されています。つまり、どのような入力をして、どのようなフローで、どのような値が出力されるかを記述しています。しかし、ここで注目してほしいのは、それらの詳細を記述せずに(カプセル化して)実装しているところです。

ユースケース

以下の画像はコントローラとプレゼンターが、内部レイヤーのユースケースと通信している様子を示しています。制御の流れに注目すると、コントローラで始まり、ユースケースを経て、プレゼンターで実行されています。また、依存関係にも注目すると、それぞれがユースケースに向かって内向きになっています。ここで、制御の流れと、依存関係が内向きになっていることの矛盾を解決するには、「依存性逆転の原則」を用います。例えば、内側のユースケースインタラクターがインターフェース(ユースケースアウトプットポート)を呼び出し、外側でプレゼンターがそれを実装しています。
clean_migisita.JPG

usecase/usecases/user_input_port.go
package usecases

import "github.com/labstack/echo"

type UsersInputPort interface {
	AddUsers(ec echo.Context) error
}
usecase/usecases/user_interactor.go
package usecases

import (
	"log"

	"example/domain"
	"example/usecase/services"

	"github.com/labstack/echo"
)

type UsersInteractor struct {
	UsersOutputPort UsersOutputPort
	UsersService services.UsersService
}

func NewUsersInteractor(
	usersOutputPort UsersOutputPort,
	usersService services.UsersService,
) *UsersInteractor {
	return &UsersInteractor{
		UsersOutputPort: usersOutputPort,
		UsersService: usersService,
	}
}

func (i *UsersInteractor) AddUsers(ec echo.Context) error {
	u := new(domain.User)
	if err := ec.Bind(u); err != nil {
		return err
	}

	res, err := i.UsersService.AddUsers(u)
	if err != nil {
		log.Fatal(err)
	}

	return i.UsersOutputPort.AddUsers(ec, res)
}
usecase/usecases/user_output_port.go
package usecases

import (
	"example/domain"

	"github.com/labstack/echo"
)

type UsersOutputPort interface {
	AddUsers(ec echo.Context, user []*domain.User) error
}

サービス

ユースケースインタラクターがリポジトリを直接呼び出してしまうと、依存方向と逆になってしまいます。よって、「依存性逆転の原則」を用いるため、サービスをインターフェースとして定義しています。

usecase/services/user_service.go
package services

import (
	"example/domain"
)

type UsersService interface {
	AddUsers(user *domain.User) ([]*domain.User, error)
}

ドメイン(エンティティ)層

この層は、アプリケーション全体のビジネスルールをカプセル化したものです。メソッドを持つオブジェクトやデータ構造、関数のセットなどが記述されます。ここで注目してほしいのは、機能の詳細については一切記述されないことです。

domain/user.go
package domain

type User struct {
	Name    string `json:"name"`
	Age     string `json:"age"`
	Address string `json:"address"`
}

まとめ

このアーキテクチャは、「依存関係が一方向」であることによって成立しています。内側の層は、外側の層について何も知ることができません。
また、あくまで図の円は模式的なもので、常にこの4つだけを作成しなければならないというルールはありません。しかし、「依存関係が内側に一方向」であるルールは常に適用されます。一番内側の円は、最も一般的なもの(抽象度が高いもの)です。また、一番外側の円は、低レベルの具体的な詳細です。
ソフトウェアをレイヤーに分け、「依存関係のルール」に従うことで、本質的にテスト可能なシステムを作ることができ、それによってもたらされるあらゆるメリットがあります。また、データベースやウェブフレームワークなど、システムの外部パーツが古くなった場合には、最小限の手間で古い要素を置き換えることができます。その一方で、コードが冗長になってしまうデメリットが存在します。ここで重要なのは、あくまで手法論ではなく目的に沿って導入を検討する必要があります。

最後に

最後まで読んでくださってありがとうございました。
今回はサンプルのため、サービスではありませんが、これからクリーンアーキテクチャを使用してサーバーサイドを構築する際、もっとよりよいものにしていければと思います。

https://github.com/daiki328/clean-architecture-api

参考

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
https://qiita.com/nrslib/items/a5f902c4defc83bd46b8
https://zenn.dev/maru44/articles/b9e07e91a0ea77
https://tech-blog.optim.co.jp/entry/2019/01/29/173000
https://rightcode.co.jp/blog/information-technology/golang-introduction-rest-api-implementation

Discussion

ログインするとコメントできます