Clean Architectureとは
Clean Architecture(クリーンアーキテクチャ)とは,レイヤに内側と外側の関係性を持たせるアーキテクチャである.
「外側のレイヤは内側のレイヤだけに依存する」というルールを守ることによって,アプリケーションから技術を分離することが目的である.
アプリケーションから技術を分離すると何が嬉しいのか
ここでの「技術」とは,HTTPやcsv,MySQLなどのことを意味している.
アプリケーションから技術を分離すると,技術を容易に変更できたり,テストコードを書くときに容易にモックできたりする.
例えば,出力をHTTPからcsvに変更したくなったときなどに容易に変更が可能である.
各レイヤの責務
Clean Architectureで提案されているレイヤ構造は以下の画像のようなものである.
内側から,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を定義)を表している.
大まかな流れとしては,以下のようなものである.
-
driver
からadapter/controller
を呼び出す -
adapter/controller
は,ポートを全て組み立てて,usecase/port/inputPort
を実行する -
usecase/port/inputPort
はusecase/interactor
が実装しているので,usecase/interactor
のMethodが呼ばれる -
usecase/interactor
では,entity
のドメインロジックを実行する. -
usecase/interactor
では,usecase/port/userRepository
を呼び出し,DBの永続化処理を行う(usecase/port/userRepository
はadapter/gateway
が実装しているので,adapter/gatewayのMethod
が呼ばれる) -
usecase/interactor
では,usecase/port/outputPort
を呼び出し,出力を行う(usecase/port/outputPort
はadapter/presenter
が実装しているので,adapter/presenter
のMethodが呼ばれる)
通常のMVCなどでは,controller
が入力を受け取り,model
を呼び出しドメインロジックを実行し,controller
が出力を行うが,Clean Architectureでは,入力はinputPort
,出力はoutputPort
が担当していることに注意する.それゆえ,adapter/controller
では,単にusecase/port/inputPort
を実行するだけで,返り値を受け取ったり出力を行ったりする必要はない.
サンプルコード
次にサンプルコードを読みながらClean Architectureの流れを理解する.
1. driver
からadapter/controller
を呼び出す
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
を実行する
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/inputPort
はusecase/interactor
が実装しているので,usecase/interactor
のMethodが呼ばれる
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
で実装されている.
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.Render
かport.UserOutputPort.RenderError
が呼ばれている(6).
4. usecase/interactor
では,entity
のドメインロジックを実行する.
今回は,単純にDBから取得したデータを出力しているのでこの部分の実装はない.
5. usecase/interactor
では,usecase/port/userRepository
を呼び出し,DBの永続化処理を行う(usecase/port/userRepository
はadapter/gateway
が実装しているので,adapter/gatewayのMethod
が呼ばれる)
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.GetUserByID
はadapter/gateway/user.go
で実装されている.
adapter/gateway/user.go
では,DB操作を実装している.
6. usecase/interactor
では,usecase/port/outputPort
を呼び出し,出力を行う(usecase/port/outputPort
はadapter/presenter
が実装しているので,adapter/presenter
のMethodが呼ばれる)
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.go
のRender
とRenderError
を変えるだけで良い.
まとめ
Clean Architectureでは,アプリケーションから技術を分離することが重要である.
その観点では,Hexagonal Architecture(ヘキサゴナルアーキテクチャ)でもClean Architectureと同様に,アプリケーションから技術を分離することができる.
それらの違いはレイヤ構造の細分化の程度であり,Clean Architectureの方がHexagonal Architectureよりも細分化されている.
ただし,Hexagonal Architectureを実際に用いることを考えると,レイヤをさらに細かく分割すると思われるので,結局,Clean Architectureに類似していくと考えられる.
また,ここで紹介したパッケージ構成はあくまでも一例である.
例えば,今回はUserRepository
をusecase/port
内に配置したが,UserRepository
をentity
に置くという選択肢などもある.
(Clean Architecture で実装するときに知っておきたかったこと)
参考文献
この記事は以下の情報を参考にして執筆しました.
・pospomeのサーバサイドアーキテクチャ(PDF版)
・Clean Architecture で実装するときに知っておきたかったこと
・The Clean Architecture