この記事ではクリーンアーキテクチャを学びながらGoで簡単なTODOアプリを作成する記事です。勉強のまとめをした物なので間違っている点があればご指摘をお願いします。
クリーンアーキテクチャ
クリーンアーキテクチャとは
ソフトウェアをレイターごとに役割を切り分けることにより関心を分離させることができるという考え方です。
翻訳版はこちら
採用する目的
-
フレームワーク独立
フレームワークを道具として使うことでアプリケーションがフレームワークに依存せず、制約を受けない -
テスト可能
レイヤー同士が独立しているのでテストがしやすい。 -
UI独立
ビジネスルールを変えずにUIを変更できる。 -
データベース独立
postgresやMySqlなどのデータベースの変更が容易である。 -
外部機能独立
ビジネスルールは外部レイヤーに依存しない。
レイヤー
説明するのは4つだが4つでなくても良いです。
- Entities
ビジネスルールのためのデータ構造や関数
- Use Case
アプリケーション固有のビジネスルールを定義する。エンティティとのデータをやり取りする。
- Interfce Adapters
アダプターの集合。内側にある層から外側の層にデータを変換して受け渡す。逆に外側から内側にデータを受け渡す。
- Frameworks & Drivers
フレームワークやツールから構成される。
レイヤー間の規則
依存ルール。各レイヤーは内側のレイヤーのことしか知らず、外側のレイヤーについては知らない。
内側の円は、外側の円についてなにも知ることはない。とくに、外側の円で宣言されたものの名前を、内側の円から言及してはならない。これは、関数、クラス、変数、あるいはその他、名前が付けられたソフトウェアのエンティティすべてに言える。
同様に、外側の円で使われているデータフォーマットを内側の円で使うべきではない。とくに、それらのフォーマットが、外側の円でフレームワークによって生成されているのであれば。外側の円のどんなものも、内側の円に影響を与えるべきではないのだ。
実装するアプリケーション
作成したアプリケーションのコードはこちらです。
TODOとユーザーを使った単純なアプリケーションです。WebAPIを提供します。
ユーザーのCRUD, TODOのCRUDを行います。
開発環境
Docker 19.03.12
docker-compose 1.26.2
Go 1.13
PostgreSQL
github.com/lib/pq 1.8.0
ディレクトリ構成
├── domain
│ └── model
│ ├── todo.go
│ └── user.go
├── infrastructure
│ ├── router.go
│ ├── sqlhandler.go
│ └── sqlhandler_test.go
├── interfaces
│ ├── controllers
│ │ ├── todoController.go
│ │ └── userController.go
│ └── database
│ ├── sqlhandler.go
│ ├── todoRepository.go
│ └── userRepository.go
├── main.go
└── usecase
├── todoInteractor.go
├── todoRepository.go
├── userInteractor.go
└── userRepository.go
とします。アーキテクチャ図とディレクト構成の関係は次のようになっています。
依存の方向 | レイヤー | ディレクトリ名(パッケージ名) |
---|---|---|
- | Entities | domain(model) |
↑ | Use cases | usecase(usecase) |
↑ | interfaces | interfaces(controllers, database) |
↑ | Frameworks & Drivers | infrastracture(infrastructure) |
データの流れ
-
DB操作
Use case(Interactor)
→interface(Repository)
→infrastructure(Sqlhandler)
-
リクエスト処理
アクセス→infrastructure(handler)
→interfaces(controller)
→httpリスポンス
Entitiesレイヤ
User
, Todo
はビジネスルールのためのデータ構造なのでここで定義します。また、今回は追加していないユーザーの名前の文字数制限などはここで定義する必要があります。User
とTodo
は次のようになります。
package model
type User struct {
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
type Users []User
package model
import "time"
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Note string `json:"note"`
DueDate time.Time `json:"due_date"`
UserID int `json:"user_id"`
}
type Todos []Todo
infrastructureレイヤ
このレイヤでは外部のツールであるPostgresとの接続を行います。
package infrastructure
import (
"database/sql"
"fmt"
"github.com/kikils/golang-todo/interfaces/database"
_ "github.com/lib/pq" // postres driver
)
type Sqlhandler struct {
DB *sql.DB
}
func NewSqlhandler() *Sqlhandler {
connStr := "postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil
}
err = db.Ping()
if err != nil {
fmt.Println("here")
return nil
}
return &Sqlhandler{db}
}
func (handler *Sqlhandler) Execute(statement string, args ...interface{}) (database.Result, error) {
res := SqlResult{}
result, err := handler.DB.Exec(statement, args...)
if err != nil {
return res, err
}
res.Result = result
return res, nil
}
func (handler *Sqlhandler) Query(statement string, args ...interface{}) (database.Row, error) {
rows, err := handler.DB.Query(statement, args...)
if err != nil {
return new(SqlRow), err
}
row := new(SqlRow)
row.Rows = rows
return row, nil
}
type SqlResult struct {
Result sql.Result
}
func (r SqlResult) LastInsertId() (int64, error) {
return r.Result.LastInsertId()
}
func (r SqlResult) RowsAffected() (int64, error) {
return r.Result.RowsAffected()
}
type SqlRow struct {
Rows *sql.Rows
}
func (r SqlRow) Scan(dest ...interface{}) error {
return r.Rows.Scan(dest...)
}
func (r SqlRow) Next() bool {
return r.Rows.Next()
}
func (r SqlRow) Close() error {
return r.Rows.Close()
}
次に実際にSQlとのデータのやり取りを行う処理をinterfaces/database
に書きます。
interfaces/databaseレイヤ
このレイヤにはUserRepository
とTodoRepository
を宣言します。
例としてuserRepository.go
を示します。SQL
を叩いていることがわかります。
package database
import (
"github.com/kikils/golang-todo/domain/model"
)
type UserRepository struct {
Sqlhandler
}
func (repo *UserRepository) Store(u model.User) (id int, err error) {
row, err := repo.Sqlhandler.Query(
"INSERT INTO users (FirstName, LastName) VALUES ($1,$2) RETURNING id;", u.FirstName, u.LastName,
)
if err != nil {
return
}
for row.Next() {
if err := row.Scan(&id); err != nil {
return -1, err
}
}
return
}
... 略
UserRepository
にinfrastructure
レイヤで定義したSqlhandler
を埋め込んでいます。
しかし、先ほど説明したように内側の層は外側の層の物を使用してはいけないので新しくSqlhandler
をinterface
レイヤに定義します。
package database
type Sqlhandler interface {
Execute(string, ...interface{}) (Result, error)
Query(string, ...interface{}) (Row, error)
}
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
type Row interface {
Scan(...interface{}) error
Next() bool
Close() error
}
infrastructure
レイヤのSqlhandler
はExecute(string, ...interface{}) (Result, error)
Query(string, ...interface{}) (Row, error)
をもっているのでこれを満たすことができます。
interface
レイヤにSqlhandler
Result
Row
を定義することでinterface
レイヤはinfrastructure
レイヤに依存することはなくなるのです。(Goのインターフェースについてはここ)
Usecaseレイヤ
このレイヤではinterface
レイヤからのInput portの役割、interfaces/controllers
へのGatewayの役割をしている。
package usecase
import "github.com/kikils/golang-todo/domain/model"
type UserInteractor struct {
UserRepository UserRepository
}
func (interactor *UserInteractor) Add(u model.User) (id int, err error) {
id, err = interactor.UserRepository.Store(u)
return
}
...略
このようにRepository
をもつInteractor
を定義する。Repository
はinterfaces/database
レイヤに存在するので、この層でも先ほどと同じようにインターフェースを定義することで依存関係を解決する。そのためUsecase
レイヤでもRepository
を定義する。
package usecase
import "github.com/kikils/golang-todo/domain/model"
type UserRepository interface {
Store(model.User) (int, error)
Update(user model.User) (id int, err error)
Delete(userID int) (err error)
FindById(int) (model.User, error)
FindAll() (model.Users, error)
}
interfaces/controllersレイヤ
ここのレイヤではリクエストに対するルーティングを実装します。
package controllers
import (
"encoding/json"
"io/ioutil"
"net/http"
"github.com/kikils/golang-todo/domain/model"
"github.com/kikils/golang-todo/interfaces/database"
"github.com/kikils/golang-todo/usecase"
)
type UserController struct {
Interactor usecase.UserInteractor
}
func NewUserController(sqlHandler database.Sqlhandler) *UserController {
return &UserController{
Interactor: usecase.UserInteractor{
UserRepository: &database.UserRepository{
Sqlhandler: sqlHandler,
},
},
}
}
func (controller *UserController) Create(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
ResponseError(w, http.StatusInternalServerError, err.Error())
return
}
var user model.User
if err := json.Unmarshal(b, &user); err != nil {
ResponseError(w, http.StatusBadRequest, err.Error())
return
}
id, err := controller.Interactor.Add(user)
if err != nil {
ResponseError(w, http.StatusBadRequest, err.Error())
return
}
ResponseOk(w, id)
}
...略
NewUserController
でdatabase.Sqlhandler
を引数に持つのでInteractor
を返してUsecase
レイヤとinfrastructure/database
を紐づけられます。
次にルーティングを実装します。
infrastructureレイヤ
リクエストは外部から来るので最外殻のinfrastructure
レイヤにルーティングを実装します。
package infrastructure
import (
"encoding/json"
"log"
"net/http"
"github.com/kikils/golang-todo/interfaces/controllers"
)
func SetUpRouting() *http.ServeMux {
mux := http.NewServeMux()
sqlhandler := NewSqlhandler()
userController := controllers.NewUserController(sqlhandler)
todoController := controllers.NewTodoController(sqlhandler)
mux.HandleFunc("/user/create", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
userController.Create(w, r)
default:
ResponseError(w, http.StatusNotFound, "")
}
})
mux.HandleFunc("/user/get", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
userController.Show(w, r)
default:
ResponseError(w, http.StatusNotFound, "")
}
})
...略
SetUpRouting()
でSqlhandler
を定義しController
に渡してあげることで各レイヤの依存関係を保つことができます。
その後、main.go
に
package main
import (
"log"
"net/http"
"github.com/kikils/golang-todo/infrastructure"
)
func main() {
mux := infrastructure.SetUpRouting()
log.Fatal(http.ListenAndServe(":8080", mux))
}
としてSetUpRouting()
を呼び出して実装は終了です。
最後に
今回はクリーンアーキテクチャを用いてTODOアプリを作成してみました。最初はクリーンアーキテクチャがとっつきにくかったです。実装してみて思ったのは、クリーンアーキテクチャにすることで同じようなコードを複数書く必要があることや、コード補完がきかないので実装のしにくさに気づきました。けれども、DBなどから独立しているので簡単に変更可能だと実感できました。
参考にしたもの
https://qiita.com/ogady/items/34aae1b2af3080e0fec4
https://qiita.com/hirotakan/items/698c1f5773a3cca6193e
https://github.com/eminetto/clean-architecture-go
https://github.com/cohhei/go-to-the-handson/tree/master/04
https://yyh-gl.github.io/tech-blog/blog/go_web_api/