Edited at

Golangで書くレイヤードアーキテクチャ 簡易APIサーバーを構築


はじめに & この記事を書いた理由

この記事は、レイヤードアーキテクチャをちょっと理解した学生が残したものです。コードに自信がないので、改善点やコメントなどを残して頂けると勉強になるので助かります。

コードはGolang、MySQLで書いています。(サンプルコード)

クリーンアーキテクチャについてもそのうち記事にします。

バカな僕だけかも知れませんがアーキテクチャについての記事を読んで、「層って何?どういう構造になってるの?どう実装するの?」って思いませんか?

アーキテクチャを勉強中の僕はいろんな記事を読んで、バカなりに理解したことを伝えたくて今回の記事を書きました。


レイヤードアーキテクチャ とは ①

レイヤードアーキテクチャについて を読んでいただけると理解できると思います。

今回はAPIサーバーを想定しているので、UI(Presentation)層は実装しません。


各層の役割


Application層

クライアントのほしいデータを返すのが責務。(Requestを受け付け、Responseする部分)


Domain層

情報を処理するのが責務。(ごちゃごちゃ処理を行う部分)


Infrastructure層

DBと通信することが責務。(DBを触る処理を書く部分)


アーキテクチャを採用することでのメリット


  • 単体テストが書ける

  • スパゲティコードになる可能性が下がる

  • データベースを簡単に変更することができる


パッケージ構成

├── application    // Request、Responseを行うだけ

│   └── server
│   ├── handler
│   ├── response
│   └── server.go
├── domain // データを加工する。必要とあらば infra を通じてDB情報を使う
│   ├── model // structの塊
│   ├── repository // Interfaceの塊(あとで説明します)
│   └── service // 処理をごちゃごちゃ書くこところ
├── infrastructure // DBと通信する
├── cmd
│   └── main.go
└── MySQL // 今回使うDBはDockerに乗せました
├── docker
└── docker-compose.yml


レイヤードアーキテクチャ とは ②

正直こんな説明じゃわかりませんよね。僕だったらわかりません。

レイヤードアーキテクチャの層を説明されても、「だから何?どうすればいいの?」ってなりますよね。

そんなあなたにこの図だ!!



こちらも矢印は依存関係を表します。

「え?Interfaceって?」となったら

Golang速習: interface{}型とinterface

【Go言語】埋め込みでinterfaceを簡単に満たす

Go言語のInterfaceの考え方、Accept interfaces,return structs

Interfaceを噛ませる利点

 - 単体テストができる

 - どこにコードを書けばいいのかわかりやすい

この設計にも問題があります。それはレイヤー化アーキテクチャとDIPです。

説明すると長くなるので読んできてください。

「依存性を逆にするって?」ってなりますよね。

難しそうに思えますよね? 



これで終わりです。

Infrastructure層にあったInterfaceをDomain層に移動しただけです。層ベースでみると依存が逆になっているのがわかります。


サンプルコード

サンプルコード

簡単なユーザー登録、データ取得、ユーザー削除機能を持ったサンプルコードを作成しました。

その一部分をここに添付します。データ取得のためのコードを抜粋して張り出しました。


Application

Requestを受け付け、Responseするだけの責務。

DIについて知りたい方は DIパターンをInterfaceを使って抽象化して実装する


application/server/handler/getUser.go

package handler

import (
"encoding/json"
"io/ioutil"
"log"
"net/http"

"github.com/hmarf/sample_layer/application/server/response"
)

type getUserRequest struct {
UserID string `json:"userId"`
}

type getUserResponse struct {
UserID string `json:"userId"`
Name string `json:"name"`
CreatedAt string `json:"createdAt"`
}

// HandleGetUser はDBからユーザー情報を取得
func HandleGetUser() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {

body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println(err)
response.BadRequest(w, "Invalid Request Body")
return
}

var requestBody getUserRequest
json.Unmarshal(body, &requestBody)

if requestBody.UserID == "" {
response.BadRequest(w, "Invalid Request Body")
return
}

//Domain層を呼び出す。参照先はInterface
user, err := userService.GetUserService(requestBody.UserID)
response.Success(w, getUserResponse{UserID: user.UserID, Name: user.Name, CreatedAt: user.CreatedAt})
}
}



application/server/handler/setUpHandler.go

package handler

import (
"github.com/hmarf/sample_layer/domain/service"
"github.com/hmarf/sample_layer/infrastructure"
)

var userService service.UserServiceInterface

func DI() {
userRepo := infrastructure.NewUserDB(infrastructure.DB)
userService = service.NewUserService(userRepo)
}



Domain

情報を処理することが責務。


domain/service/user.go

package service

import (
"github.com/hmarf/sample_layer/domain/model"
"github.com/hmarf/sample_layer/domain/repository"
)

type userServiceStruct struct {
userRepo repository.UserRepository
}

// Application層はこのInterfaceに依存
type UserServiceInterface interface {
GetUserService(string) (model.User, error)
InsertUserService(string) (model.User, error)
DeleteUserService(string) error
}
// DIのための関数
func NewUserService(u repository.UserRepository) UserServiceInterface {
return &userServiceStruct{userRepo: u}
}



domain/service/getUser.go

package service

import (
"log"

"github.com/hmarf/sample_layer/domain/model"
)

func (u *userServiceStruct) GetUserService(userID string) (model.User, error) {

user, err := u.userRepo.Select(userID)
if err != nil {
log.Println(err)
}
return user, err
}



domain/model/user.go

package model

type User struct {
UserID string `json:"UserID"`
Name string `json:"Name"`
CreatedAt string `json:"CreatedAt"`
}



domain/repository/user.go

package repository

import (
"time"

"github.com/hmarf/sample_layer/domain/model"
)

// Infra層からDomain層に移動させたInterface
type UserRepository interface {
Insert(string, string, time.Time) error
Select(string) (model.User, error)
Delete(string) error
}



Infrastructure

DBと通信することが責務。


infrastructure/user.go

package infrastructure

import (
"database/sql"
"log"
"time"

"github.com/hmarf/sample_layer/domain/model"
"github.com/hmarf/sample_layer/domain/repository"
)

type userInfraStruct struct {
db *sql.DB
}

// DIのための関数
func NewUserDB(db *sql.DB) repository.UserRepository {
return &userInfraStruct{db: db}
}

func (u *userInfraStruct) Insert(userID string, name string, createdAt time.Time) (err error) {
ins, err := u.db.Prepare("INSERT INTO user(user_id,name,createdAt) VALUES(?,?,?)")
if err != nil {
log.Println(err)
}
ins.Exec(userID, name, createdAt)
return
}

func (u *userInfraStruct) Select(userID string) (user model.User, err error) {
if err := u.db.QueryRow("SELECT * FROM user WHERE user_id = ?", userID).
Scan(&user.UserID, &user.Name, &user.CreatedAt); err != nil {
}
return
}

func (u *userInfraStruct) Delete(userID string) (err error) {
ins, err := u.db.Prepare("DELETE FROM user WHERE user_id=?")
if err != nil {
log.Println(err)
}
ins.Exec(userID)
return
}



終わりに

最後まで読んでいただきありがとうございました。まだアーキテクチャを勉強中なので間違いなどがあると思います。そのミスにお気づきの際はコメントお願いします。またコードの改善点などがありましたら指摘お願いします。

DDD設計を勉強中ですが、いまいち掴みきれません。良本や良記事がありましたら教えてください。