CleanArchitectureについて
CleanArchitectureとは
CleanArchitectureというものはシステムの柔軟性、保守性、テストのしやすさを向上させるアーキテクチャなんだそうで、いくつかのレイヤに分けられ依存関係が一方向になっていることで、いずれかのレイヤに手が入ることがあっても修正をそのレイヤに範囲を限定させたり、置き換えたり、いいかえれば部分的にテスト用コードに置き換えることができるということでテストもしやすくなるよねといった利点がある。
とはいえ、それだけでは甘かったりもするので、SOLID原則も取り入れてできるだけモジュールをシンプルにするよう考えたり依存するものを切り離せるようにするなどして、よりメンテナンスやテストのしやすさを向上させたりもするのだが、あまり分割しすぎるとコード量が増えたり処理速度の低下にもつながるので、システムの規模やテスト内容等を考慮してうまく調整することも大切だったりもする。
この記事について
会社からの要望で急ぎgoを使った例を載せたCleanArchitectureの記事を頼まれたので。参考になりそうな記事を見ながらメモ&実装してみたいと思う。
ポイント
SOLID原則
単一責任の原則、開放閉鎖の原則、リスコフの置換原則、インターフェース分離の原則、依存性逆転の原則といったシステム開発時に気をつけるべきポイント。一つのクラスにあれこれさせないだとか、修正はしなくても拡張はできるようにとかそういったことについて触れられていて、クリーンアーキテクチャでもこの考えを使うので抑えておいたほうがいい。
あの円
引用元: The Clean Code Blog - The Clean Architecture
クリーンアーキテクチャの説明でよく目にするこの図、これはコンセプトを伝えるための例で、このコンポーネントの配置に直接対応するわけではなく、ソフトウェアの構造には意識すべき点が異なる複数の領域があり、各領域は常に一定の方向に依存するといったことを伝えたいようです。
上記ポイントを抑えつつ、各レイヤーを調べながら実装
図にしっかりと依存関係が示されているのでまずは最も内側のEnterprise Business Rulesについて調べながら実装していきます。
ひとまず単一責任の原則に習ってユーザー情報に関して責任を持つユーザーモジュールを実装。
ディレクトリ構造
goでは冗長な名前を避けるようだが、一つの機能に属するコンポーネントはなるべくまとめたいというのと、各コンポーネントが一つとは限らないため、以下のように。
cleanarch
├── go.mod
├── cmd
│ └── main.go
└── internal
└── user
├── controller
│ └── user_controller.go
├── entity
│ └── user.go
├── repository
│ ├── user_repository.go
│ └── user_repository_impl.go
└── service
└── user_service.go
Enterprise Business Rules
クリーンアーキテクチャの最も内側のレイヤーでシステム全体や、他のシステムにも依存しない普遍的なルールを定義します。
今回はユーザー情報を扱うモジュールということで、どこにも依存しないユーザー情報そのものを表すUserエンティティを定義します。
package entity
type User struct {
ID int
Name string
}
Application Business Rules
クリーンアーキテクチャでEnterprise Business Rulesの外側に位置するレイヤーで、特定のアプリケーションの処理(このアプリケーション)を定義します。
今回はユーザー情報を扱うモジュールということで、ユーザー情報をどのように扱うかを定義するUserServiceを定義します。
UserServiceではUserRepositoryImplを使用し、UserRepositoryImplのメンバ上に登録されたユーザー情報を保持します。
サンプルということでこのような処理となっていますが、一般的なシステムではDB等を使用し、データを永続化したいはずなので、あとで変更できるよう、依存性の逆転でUserServiceとUserRepositoryImplの間にUserRepositoryインターフェースを定義しておくことでDB等を使用したUserRepository実装に切り替えられるようにします。
はじめからDBを使用して変更する予定がいない、テストはそのままDBを使って行なう、規模が大きくないなどであればわざわざこういったことをする必要はないとは思います。
まずはリポジトリのインターフェースを定義、今回はUserエンティティを登録して、ID指定で取り出せればよいので以下のように。
package repository
import "cleanarch/internal/user/entity"
type UserRepository interface {
GetByID(id int) (*entity.User, error)
Create(user *entity.User) error
}
つづいてリポジトリの実装、登録されたUserエンティティを保持するメンバ変数と登録用メソッド、取り出し用メソッドを実装
package repository
import (
"cleanarch/internal/user/entity"
"errors")
type userRepositoryImpl struct {
users map[int]*entity.User
}
func NewUserRepository() UserRepository {
return &userRepositoryImpl{
users: make(map[int]*entity.User),
}
}
func (r *userRepositoryImpl) GetByID(id int) (*entity.User, error) {
if user, exists := r.users[id]; exists {
return user, nil
}
return nil, errors.New("user not found")
}
func (r *userRepositoryImpl) Create(user *entity.User) error {
if _, exists := r.users[user.ID]; exists {
return errors.New("user already exists")
}
r.users[user.ID] = user
return nil
}
Application Business RulesのメインとなるUserServiseの実装。今回は特にこのモジュールでの特別なルールがあるわけではなく、リポジトリにそのままユーザーエンティティを渡したり、逆に受け取ったりしてますが、登録したユーザー用のディスクスペースを作る必要があるといった要件があれば、この層で実装するといいと思います。
package service
import (
"cleanarch/internal/user/entity"
"cleanarch/internal/user/repository")
type UserService struct {
repository repository.UserRepository
}
func NewUserService(r repository.UserRepository) *UserService {
return &UserService{repository: r}
}
func (s *UserService) GetUserByID(id int) (*entity.User, error) {
return s.repository.GetByID(id)
}
func (s *UserService) CreateUser(user *entity.User) error {
return s.repository.Create(user)
}
Interface Adapters
クリーンアーキテクチャでApplication Business Rulesの外側に位置するレイヤーで、Application Business Rulesと外部インフラを繋ぐ層で、要はMVC等のコントローラー等。この層の外にあるFrameworks&Driver層からの入力を受け取りApplication Business RulesのUserServiceへ渡す。
package handler
import (
"encoding/json"
"net/http" "strconv"
"cleanarch/internal/user/entity" "cleanarch/internal/user/service")
type UserController struct {
service *service.UserService
}
func NewUserController(s *service.UserService) *UserController {
return &UserController{service: s}
}
func (c *UserController) GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
user, err := c.service.GetUserByID(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(user)
}
func (c *UserController) CreateUser(w http.ResponseWriter, r *http.Request) {
var user entity.User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
if err := c.service.CreateUser(&user); err != nil {
http.Error(w, err.Error(), http.StatusConflict)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
Frameworks&Drivers
クリーンアーキテクチャでInterface Adaptersの外側に位置するレイヤーで、WEBアプリケーションで言うLaravelやNext.js等のフレームワークにあたる層で利用者との直接的なやり取りを行います。今回の実装はWEB APIだが特に複雑なことはしないので特にフレームワークを使わず、go標準のnet/httpで実装
package main
import (
"cleanarch/internal/user/controller"
"cleanarch/internal/user/repository"
"cleanarch/internal/user/service"
"log"
"net/http"
)
func main() {
r := repository.NewUserRepository()
s := service.NewUserService(r)
c := handler.NewUserController(s)
server := http.Server{
Addr: ":8080",
Handler: nil,
}
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
c.GetUser(w, r)
case http.MethodPost:
c.CreateUser(w, r)
}
})
err := server.ListenAndServe()
if err != nil {
log.Fatal(err)
}
}
Discussion