13
12

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 3 years have passed since last update.

Go言語でTODOアプリwithクリーンアーキテクチャ

Last updated at Posted at 2020-09-15

この記事ではクリーンアーキテクチャを学びながらGoで簡単なTODOアプリを作成する記事です。勉強のまとめをした物なので間違っている点があればご指摘をお願いします。

クリーンアーキテクチャ

クリーンアーキテクチャとは

ソフトウェアをレイターごとに役割を切り分けることにより関心を分離させることができるという考え方です。
翻訳版はこちら

採用する目的

  1. フレームワーク独立
    フレームワークを道具として使うことでアプリケーションがフレームワークに依存せず、制約を受けない

  2. テスト可能
    レイヤー同士が独立しているのでテストがしやすい。

  3. UI独立
    ビジネスルールを変えずにUIを変更できる。

  4. データベース独立
    postgresやMySqlなどのデータベースの変更が容易である。

  5. 外部機能独立
    ビジネスルールは外部レイヤーに依存しない。

レイヤー

説明するのは4つだが4つでなくても良いです。

  1. Entities

ビジネスルールのためのデータ構造や関数

  1. Use Case

アプリケーション固有のビジネスルールを定義する。エンティティとのデータをやり取りする。

  1. Interfce Adapters

アダプターの集合。内側にある層から外側の層にデータを変換して受け渡す。逆に外側から内側にデータを受け渡す。

  1. 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はビジネスルールのためのデータ構造なのでここで定義します。また、今回は追加していないユーザーの名前の文字数制限などはここで定義する必要があります。UserTodoは次のようになります。

/domain/model/user.go
package model

type User struct {
	ID        int    `json:"id"`
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
}

type Users []User
/domain/model/todo.go
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との接続を行います。

/infrastructure/sqlhandler.go
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レイヤ

このレイヤにはUserRepositoryTodoRepositoryを宣言します。
例としてuserRepository.goを示します。SQLを叩いていることがわかります。

/interfaces/database/userRepository.go
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
}
... 

UserRepositoryinfrastructureレイヤで定義したSqlhandlerを埋め込んでいます。
しかし、先ほど説明したように内側の層は外側の層の物を使用してはいけないので新しくSqlhandlerinterfaceレイヤに定義します。

/interfaces/database/sqlhandler.go
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レイヤのSqlhandlerExecute(string, ...interface{}) (Result, error) Query(string, ...interface{}) (Row, error)をもっているのでこれを満たすことができます。
interfaceレイヤにSqlhandler Result Rowを定義することでinterfaceレイヤはinfrastructureレイヤに依存することはなくなるのです。(Goのインターフェースについてはここ)

Usecaseレイヤ

このレイヤではinterfaceレイヤからのInput portの役割、interfaces/controllersへのGatewayの役割をしている。

/usecase/userInteractor.go
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を定義する。Repositoryinterfaces/databaseレイヤに存在するので、この層でも先ほどと同じようにインターフェースを定義することで依存関係を解決する。そのためUsecaseレイヤでもRepositoryを定義する。

/usecase/userRepository.go
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レイヤ

ここのレイヤではリクエストに対するルーティングを実装します。

/interfaces/controllers/userController.go
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)
}
...

NewUserControllerdatabase.Sqlhandlerを引数に持つのでInteractorを返してUsecaseレイヤとinfrastructure/databaseを紐づけられます。
次にルーティングを実装します。

infrastructureレイヤ

リクエストは外部から来るので最外殻のinfrastructureレイヤにルーティングを実装します。

/infrastructure/router.go
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

/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/

13
12
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
13
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?