post Image
Goでクリーンアーキテクチャを使ってみた話

 本記事では、GolangでClean Architectureを使ってAPI Serverを構築したことについてお話しします。
 Clean Architectureについての詳細は、こちらをクリックしていただくと詳しい解説があるかと思います。

各レイヤーの役割

 ここでは、私が設計した各レイヤーの説明を行います。

adapter

 ルーティングで呼び出すためのControllerの処理を記述している。

db

 マイグレーションのための層でDDL文やマイグレーションファイルが置いてある。今回は、gooseを使ってマイグレーションを行っている。

entity

 ビジネスルールを記述したデータ構造。

external

 APIのルーティングを記述する層でControllerを呼び出す。

infrastructure

 DBにアクセスするためのHandler、Model層(impl)、Modelで扱うインターフェースの定義。

usecase

アプリケーション固有のビジネスルールを含む。システムのユースケースすべてをカプセル化および実装する。

実装するアプリケーション

 今回はユーザー名のみを扱った単純なCRUDを実装します。全てのソースコードはこちらにありますのでよければみてください。
コード量の兼ね合いで本記事ではPOSTのみの実装を紹介します。実装した全てのエンドポイントを以下に示します。
POST: /users
GET: /users
PUT: /users/:id
DELETE: /users/:id

entity

src/entity/user.go
package entity

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

 まずはentityの実装です。dbタグでは、database上でのどのカラム名を指しているかを明示しています。Userは、IDFirstNameLastNameを持っています。

db

db/migration/20181010221318_createUsers.sql
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE `users`(
    `id` INT (11) NOT NULL AUTO_INCREMENT,
    `first_name` VARCHAR(256) NOT NULL,
    `last_name` VARCHAR(256) NOT NULL,
    PRIMARY KEY(`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE `users`;
db/dbconf.yml
development:
    driver: mysql
    open: root:@/clean_go?parseTime=true&charset=utf8mb4&interpolateParams=true

 dbの実装です。gooseを使用するためにdbの下にdbconf.ymlを定義します。sqlファイルには、goose Upの下にmigrate upしたいDDL文を記述して、goose Downの下にmigrate downしたいDDL文を記述します。

infrastructure

src/infrastructure/interfaces/sql_handler_repository.go
package interfaces

import (
    "database/sql"
)

// SQLHandler ...
type SQLHandler interface {
    Prepare(string) (*sql.Stmt, error)
}
src/infrastructure/interfaces/user_repository.go
package interfaces

import (
    "github.com/rikiya/go-clean/src/entity"
)

// UserRepository ...
type UserRepository interface {
    Store(entity.User) error
}

 interfaces層で、SQLのHandlerの振る舞いと、usersテーブルに対するSQL問い合わせの振る舞いをインターフェースとして定義しています。

src/infrastructure/database/sql_handler.go
package database

import (
    "database/sql"

    _ "github.com/go-sql-driver/mysql"
    "github.com/jmoiron/sqlx"
)

// SQLHandler ...
type SQLHandler struct {
    Conn *sqlx.DB
}

// NewSQLHandler ...
func NewSQLHandler() *SQLHandler {
    ctx, err := sqlx.Open("mysql", "root:@/clean_go?parseTime=true&charset=utf8mb4&interpolateParams=true")
    if err != nil {
        panic(err)
    }
    sqlHandler := new(SQLHandler)
    sqlHandler.Conn = ctx
    return sqlHandler
}

// Prepare ...
func (s *SQLHandler) Prepare(query string) (*sql.Stmt, error) {
    result, err := s.Conn.Prepare(query)
    if err != nil {
        return nil, err
    }
    return result, nil
}

 ここでは、インターフェースで定義したことに乗っ取って実装をしています。

src/infrastructure/user/user_impl.go
package user

import (
    "github.com/rikiya/go-clean/src/entity"
    "github.com/rikiya/go-clean/src/infrastructure/database"
)

// UserImpl ...
type UserImpl struct {
    database.SQLHandler
}

// Store ...
func (ui *UserImpl) Store(u entity.User) error {
    ctx := database.NewSQLHandler()
    res, err := ctx.Prepare(
        `INSERT INTO users (first_name, last_name) VALUES(?, ?)`,
    )
    if err != nil {
        return err
    }
    defer res.Close()

    _, err = res.Exec(u.FirstName, u.LastName)
    return err
}

 ここでは、usersテーブルに対してDBの問い合わせを行っています。Prepareメソッドは先ほど実装したsql_handler.goのメソッドを使用しています。問い合わせが終わったらきちんとClose()を行いましょう。

usecase

src/usecase/user_interactor.go
package usecase

import (
    "github.com/rikiya/go-clean/src/entity"
    "github.com/rikiya/go-clean/src/infrastructure/interfaces"
)

// UserInteractor ...
type UserInteractor struct {
    UserRepository interfaces.UserRepository
}

// Store ...
func (ui *UserInteractor) Store(u entity.User) error {
    err := ui.UserRepository.Store(u)
    if err != nil {
        return err
    }
    return nil
}

 ここでは、controllerに対するGatewayのような役割を果たしています。なので、controllerではここで定義したメソッドを呼び出します。

adapter

src/errorlog/error.go
package errorlog

import (
    "log"
    "net/http"
)

// ErrorStatus ...
func ErrorStatus(w http.ResponseWriter, err error, status int) {
    if err != nil {
        log.Println("Error: ", err)
        w.WriteHeader(status)
        return
    }
}

 エラー処理を行って適切なステータスコードをHeaderに載せて終了させるメソッドです。if err != nil{}という処理が増えるのが私自身好きでないのでよくこのようなやり方をしてステータスコードを埋め込んだりしています。これに関しては自分でもどうなのだろうと感じているのでもっと良いやり方があれば教えてください笑。

src/userscontroller/users_controller.go
package userscontroller

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"

    "github.com/gorilla/mux"
    "github.com/rikiya/go-clean/src/adapter/errorlog"
    "github.com/rikiya/go-clean/src/entity"
    "github.com/rikiya/go-clean/src/infrastructure/database"
    "github.com/rikiya/go-clean/src/infrastructure/user"
    "github.com/rikiya/go-clean/src/usecase"
)

// UserController ...
type UserController struct {
    Interactor usecase.UserInteractor
}

// NewUserController ...
func NewUserController(sqlHandler database.SQLHandler) *UserController {
    return &UserController{
        Interactor: usecase.UserInteractor{
            UserRepository: &user.UserImpl{
                SQLHandler: sqlHandler,
            },
        },
    }
}

// Create ...
func (c *UserController) Create(w http.ResponseWriter, r *http.Request) {
    u := entity.User{}
    err := json.NewDecoder(r.Body).Decode(&u)
    errorlog.ErrorStatus(w, err, http.StatusBadRequest)
    err = c.Interactor.Store(u)
    errorlog.ErrorStatus(w, err, http.StatusInternalServerError)
    log.Println("Created User!!")
}

 UserControllerは、usecase.UserInteractorを内包しています。そして、NewUserControllerでインスタンスを作る際に、database.SqlHandlerを引数に取っている。
 Createメソッドでは、request bodyをDecodeしてentity.User{}に対して埋め込みます。そして、受け取ったBodyの値をStoreメソッドに対して渡しています。

external

src/external/router.go
package external

import (
    "github.com/gorilla/mux"
    "github.com/rikiya/go-clean/src/adapter/userscontroller"
    "github.com/rikiya/go-clean/src/infrastructure/database"
)

// Router ...
func Router(r *mux.Router) {
    usersController := userscontroller.NewUserController(*database.NewSQLHandler())
    r.HandleFunc("/users", usersController.Create).Methods("POST")
}

 ここでは、Routingの定義を行ってHandlerで、Controllerのメソッドを呼び出しています。

main

src/server.go
package main

import (
    "net/http"

    "github.com/gorilla/mux"
    "github.com/rikiya/go-clean/src/external"
)

func main() {
    r := mux.NewRouter()
    external.Router(r)
    http.ListenAndServe("localhost:8080", r)
}

 最後にmainで新しいRouterのインスタンスを生成してexternalに対して渡してあげます。最後に8080番をListenして終了です。

まとめ

 Clean Architectureのルールに則ることで各層の役割がはっきりしてコードが疎結合になることで再利用性が上がったり、テストがしやすくなったりすると感じました。最初書き始めるとコード量が多くなるような印象は持つと思いますが、一度書いてしまえば拡張性が高く開発効率は上がるのではないのでしょうか。
 最後になりましたが、全ソースコードをこちらに載せていますのでよければみてください!!(よかったらスター⭐️お願いします笑)
 最後まで見てくださりありがとうございました。


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。