86
48

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Go言語とClean ArchitectureでAPIサーバを構築する

Last updated at Posted at 2021-02-20

Clean Architectureとは

Clean Architecture(クリーンアーキテクチャ)とは,レイヤに内側と外側の関係性を持たせるアーキテクチャである.
「外側のレイヤは内側のレイヤだけに依存する」というルールを守ることによって,アプリケーションから技術を分離することが目的である.

アプリケーションから技術を分離すると何が嬉しいのか

ここでの「技術」とは,HTTPやcsv,MySQLなどのことを意味している.
アプリケーションから技術を分離すると,技術を容易に変更できたり,テストコードを書くときに容易にモックできたりする.
例えば,出力をHTTPからcsvに変更したくなったときなどに容易に変更が可能である.

各レイヤの責務

Clean Architectureで提案されているレイヤ構造は以下の画像のようなものである.
CleanArchitecture.jpg
内側から,Entitiesレイヤ,Use Casesレイヤ,Interface Adaptersレイヤ,Frameworkd & Driversレイヤの4つのレイヤから構成される.
「外側のレイヤは内側のレイヤだけに依存する」というルールが存在し,例えば,Use CasesレイヤがExternal Interfacesレイヤに依存するようなことがあってはならない.
また,技術に依存しているコードを置いていいのはInterface Adaptersレイヤ,Frameworkd & Driversレイヤの外側2層だけで,Entitiesレイヤ,Use Casesレイヤには技術に依存したコードをおいてはならない.

各レイヤの責務を大まかに説明すると次のようなものである.

Entitiesレイヤ

ドメインロジックを実装する責務を持つ.
DB操作などの技術的な実装を持ってはならない.
また,他のどのレイヤにも依存してはならない.

Use Casesレイヤ

Entitiesレイヤのオブジェクトを操作してビジネスロジックを実行する責務を持つ.
さらに,このレイヤにはポートを定義する.ここで,ポートとは,アダプターで実装を差し替えることができる対象のことである.
Go言語の場合,ポートはInterfaceにあたる.
InputPort・OutputPortはそれぞれ,入力・出力に関するポート,

Interface Adaptersレイヤ

Use Casesレイヤで定義したポートに対する実装を提供する.すなわち,InterfaceのMethodを定義する(実態を作ると考えるとよい).
それゆえ,このレイヤでDB操作やHTTP入出力などの技術的な実装を定義する.
Controllersは入力に関するアダプター,Presentersは出力に関するアダプター,Gatewaysは永続化に関するアダプターである.

Frameworks & Driversレイヤ

DBのconnection生成やroutingなどの技術的な実装をおく.

Go言語を用いてAPIサーバを構築する.

ここで作成するAPIはPathParameterからuserIDを受け取り,そのuserIDをもつuserの名前をDBから取得し,出力するものである.

GET /user/:id
input: userID string
output: userName string

以下で出てくるコードは全て サンプルコード においてある.

package構成

大まかなpackage構成が以下である.

.
├── adapter
│   ├── controller
│   │   └── user.go
│   ├── gateway
│   │   └── user.go
│   └── presenter
│       └── user.go
├── driver
│   └── user.go
├── entity
│   └── user.go
└── usecase
    ├── interactor
    │   └── user.go
    └── port
        └── user.go

package同士の関係

package同士の関係は以下の画像のとおりである.ただし,重要な部分だけを抜き出している.
実線は依存(使用していると読み替えても良い),点線は実装(Interfaceを満たすようにMethodを定義)を表している.
architecture.png

大まかな流れとしては,以下のようなものである.

  1. driverからadapter/controllerを呼び出す
  2. adapter/controllerは,ポートを全て組み立てて,usecase/port/inputPortを実行する
  3. usecase/port/inputPortusecase/interactorが実装しているので,usecase/interactorのMethodが呼ばれる
  4. usecase/interactorでは,entityのドメインロジックを実行する.
  5. usecase/interactorでは,usecase/port/userRepositoryを呼び出し,DBの永続化処理を行う(usecase/port/userRepositoryadapter/gatewayが実装しているので,adapter/gatewayのMethodが呼ばれる)
  6. usecase/interactorでは,usecase/port/outputPortを呼び出し,出力を行う(usecase/port/outputPortadapter/presenterが実装しているので,adapter/presenterのMethodが呼ばれる)

通常のMVCなどでは,controllerが入力を受け取り,modelを呼び出しドメインロジックを実行し,controllerが出力を行うが,Clean Architectureでは,入力はinputPort,出力はoutputPortが担当していることに注意する.それゆえ,adapter/controllerでは,単にusecase/port/inputPortを実行するだけで,返り値を受け取ったり出力を行ったりする必要はない.

サンプルコード

次にサンプルコードを読みながらClean Architectureの流れを理解する.

1. driverからadapter/controllerを呼び出す

driver/user.go
package driver

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"

	// blank import for MySQL driver
	"github.com/arkuchy/clean-architecture-sample-sample/adapter/controller"
	"github.com/arkuchy/clean-architecture-sample-sample/adapter/gateway"
	"github.com/arkuchy/clean-architecture-sample-sample/adapter/presenter"
	"github.com/arkuchy/clean-architecture-sample-sample/usecase/interactor"
	_ "github.com/go-sql-driver/mysql"
)

// Serve はserverを起動させます.
func Serve(addr string) {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DATABASE"))
	conn, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Println(err)
		return
	}
	user := controller.User{
		OutputFactory: presenter.NewUserOutputPort,
		InputFactory:  interactor.NewUserInputPort,
		RepoFactory:   gateway.NewUserRepository,
		Conn:          conn,
	}
	http.HandleFunc("/user/", user.GetUserByID)
	err = http.ListenAndServe(addr, nil)
	if err != nil {
		log.Fatalf("Listen and serve failed. %+v", err)
	}
}

driver/user.goではDBのconnectionを生成し,routingの設定を行なっている.
adapter/controller/user.goで定義されているcontroller.Userを作成し,http.HandleFunc()controller.User.GetUserByIDを渡している


2. adapter/controllerは,ポートを全て組み立てて,usecase/port/inputPortを実行する

adapter/controller/user.go
package controller

import (
	"database/sql"
	"net/http"
	"strings"

	"github.com/arkuchy/clean-architecture-sample-sample/usecase/port"
)

type User struct {
	OutputFactory func(w http.ResponseWriter) port.UserOutputPort
	// -> presenter.NewUserOutputPort
	InputFactory func(o port.UserOutputPort, u port.UserRepository) port.UserInputPort
	// -> interactor.NewUserInputPort
	RepoFactory func(c *sql.DB) port.UserRepository
	// -> gateway.NewUserRepository
	Conn *sql.DB
}

// GetUserByID は,httpを受け取り,portを組み立てて,inputPort.GetUserByIDを呼び出します.
func (u *User) GetUserByID(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	userID := strings.TrimPrefix(r.URL.Path, "/user/")
	outputPort := u.OutputFactory(w)
	repository := u.RepoFactory(u.Conn)
	inputPort := u.InputFactory(outputPort, repository)
	inputPort.GetUserByID(ctx, userID)
}

1で呼ばれたcontroller.User.GetUserByID内では,入力を受け取りPathParmeterを取得した後,全てのポート(UserInputPort, UserOutputPort, UserRepository)を組み立てて,inputPort.GetUserByIDを呼び出す.


3. usecase/port/inputPortusecase/interactorが実装しているので,usecase/interactorのMethodが呼ばれる

usecase/port/user.go
package port

import (
	"context"

	"github.com/arkuchy/clean-architecture-sample-sample/entity"
)

type UserInputPort interface {
	GetUserByID(ctx context.Context, userID string)
}

type UserOutputPort interface {
	Render(*entity.User)
	RenderError(error)
}

// userのCRUDに対するDB用のポート
type UserRepository interface {
	GetUserByID(ctx context.Context, userID string) (*entity.User, error)
}

usecase/port/user.goに定義されているUserInputPortはInterfaceなので,このInterfaceを実装しているコードが呼ばれることになる.
UserInputPortは,usecase/interactor/user.goで実装されている.

usecase/interactor/user.go
package interactor

import (
	"context"

	"github.com/arkuchy/clean-architecture-sample-sample/usecase/port"
)

type User struct {
	OutputPort port.UserOutputPort
	UserRepo   port.UserRepository
}

// NewUserInputPort はUserInputPortを取得します.
func NewUserInputPort(outputPort port.UserOutputPort, userRepository port.UserRepository) port.UserInputPort {
	return &User{
		OutputPort: outputPort,
		UserRepo:   userRepository,
	}
}

// usecase.UserInputPortを実装している
// GetUserByID は,UserRepo.GetUserByIDを呼び出し,その結果をOutputPort.Render or OutputPort.RenderErrorに渡します.
func (u *User) GetUserByID(ctx context.Context, userID string) {
	user, err := u.UserRepo.GetUserByID(ctx, userID)
	if err != nil {
		u.OutputPort.RenderError(err)
		return
	}
	u.OutputPort.Render(user)
}

usecase/interactor/user.goに定義されているInputPortの実装(GetUserByID)内では,まずport.UserRepository.GetUserByIDが呼ばれている(5).
その後,errの有無により,port.UserOutputPort.Renderport.UserOutputPort.RenderErrorが呼ばれている(6).


4. usecase/interactorでは,entityのドメインロジックを実行する.
今回は,単純にDBから取得したデータを出力しているのでこの部分の実装はない.


5. usecase/interactorでは,usecase/port/userRepositoryを呼び出し,DBの永続化処理を行う(usecase/port/userRepositoryadapter/gatewayが実装しているので,adapter/gatewayのMethodが呼ばれる)

adapter/gateway/user.go
package gateway

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"log"

	"github.com/arkuchy/clean-architecture-sample-sample/entity"
	"github.com/arkuchy/clean-architecture-sample-sample/usecase/port"
)

type UserRepository struct {
	conn *sql.DB
}

// NewUserRepository はUserRepositoryを返します.
func NewUserRepository(conn *sql.DB) port.UserRepository {
	return &UserRepository{
		conn: conn,
	}
}

// GetUserByID はDBからデータを取得します.
func (u *UserRepository) GetUserByID(ctx context.Context, userID string) (*entity.User, error) {
	conn := u.GetDBConn()
	row := conn.QueryRowContext(ctx, "SELECT * FROM `user` WHERE id=?", userID)
	user := entity.User{}
	err := row.Scan(&user.ID, &user.Name)
	if err != nil {
		if err == sql.ErrNoRows {
			return nil, fmt.Errorf("User Not Found. UserID = %s", userID)
		}
		log.Println(err)
		return nil, errors.New("Internal Server Error. adapter/gateway/GetUserByID")
	}
	return &user, nil
}

// GetDBConn はconnectionを取得します.
func (u *UserRepository) GetDBConn() *sql.DB {
	return u.conn
}

3のusecase/interactor/user.goで呼び出されたUserRepository.GetUserByIDadapter/gateway/user.goで実装されている.
adapter/gateway/user.goでは,DB操作を実装している.


6. usecase/interactorでは,usecase/port/outputPortを呼び出し,出力を行う(usecase/port/outputPortadapter/presenterが実装しているので,adapter/presenterのMethodが呼ばれる)

adapter/presenter/user.go
package presenter

import (
	"fmt"
	"net/http"

	"github.com/arkuchy/clean-architecture-sample-sample/entity"
	"github.com/arkuchy/clean-architecture-sample-sample/usecase/port"
)

type User struct {
	w http.ResponseWriter
}

// NewUserOutputPort はUserOutputPortを取得します.
func NewUserOutputPort(w http.ResponseWriter) port.UserOutputPort {
	return &User{
		w: w,
	}
}

// usecase.UserOutputPortを実装している
// Render はNameを出力します.
func (u *User) Render(user *entity.User) {
	u.w.WriteHeader(http.StatusOK)
	// httpでentity.User.Nameを出力
	fmt.Fprint(u.w, user.Name)
}

// RenderError はErrorを出力します.
func (u *User) RenderError(err error) {
	u.w.WriteHeader(http.StatusInternalServerError)
	fmt.Fprint(u.w, err)
}

3のusecase/interactor/user.goで呼び出されたUserOutputPort.Render(RenderError)はadapter/presenter/user.goで実装されている.
adapter/presenter/user.goでは,Headerを付与して出力を行っている.

アダプターの差し替え

上のサンプルコードのように,技術的な実装は全てInterface AdaptersレイヤとFrameworkd & Driversレイヤで行っている.それよりも内側の層は,技術が何を使われているかを知ることがない.
したがって,HTTP出力ではなく,ファイル出力に変えたければ,adapter/presenter/user.goRenderRenderErrorを変えるだけで良い.

まとめ

Clean Architectureでは,アプリケーションから技術を分離することが重要である.
その観点では,Hexagonal Architecture(ヘキサゴナルアーキテクチャ)でもClean Architectureと同様に,アプリケーションから技術を分離することができる.
それらの違いはレイヤ構造の細分化の程度であり,Clean Architectureの方がHexagonal Architectureよりも細分化されている.
ただし,Hexagonal Architectureを実際に用いることを考えると,レイヤをさらに細かく分割すると思われるので,結局,Clean Architectureに類似していくと考えられる.

また,ここで紹介したパッケージ構成はあくまでも一例である.
例えば,今回はUserRepositoryusecase/port内に配置したが,UserRepositoryentityに置くという選択肢などもある.
(Clean Architecture で実装するときに知っておきたかったこと)

参考文献

この記事は以下の情報を参考にして執筆しました.
pospomeのサーバサイドアーキテクチャ(PDF版)
Clean Architecture で実装するときに知っておきたかったこと
The Clean Architecture

86
48
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
86
48

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?