レイヤードアーキテクチャとは
APIサーバを実装するときはだいたい以下の流れだと思います.
データを受け取って -> 処理して -> データベースに保存
レイヤードアーキテクチャは簡単に言うと,この流れを責務に乗っ取り分割することで,依存関係を少なくし,メンテナンスのしやすい構成にしようと言う考え方になります.
構成
- UI(Presentation)層: クライアントのことを指します. APIサーバでは考えません.
- Application層: Domain層を用いて,クライアントが欲しいデータを返すのが責務です.
- Domain層: クライアントからのデータを処理するのが責務です.
- Infrastructure層: DBとの通信が責務です.
題材
https://github.com/tozastation/gRPC-Training-Golang
を使って説明していきます
構成
├── domain: ドメイン層
│ ├── repository: 依存性逆転の原則
│ └── service: ロジック
├── idl: Protocol Bufferの定義ファイル
├── implements: アプリケーション層
├── infrastructure: インフラ層
│ └── persistence
│ ├── model: DBモデル
│ └── mssql: mssqlサーバ用Repository
├── interfaces: その他
│ ├── auth: 認証
│ ├── di: 依存性の注入
│ ├── handler: ハンドラー
│ └── rpc: gRPC生成ファイル
├── main.go
├── protoc.sh
└── vendor: Goのパッケージ
ドメイン層
repository
repositoryでは,インフラ層におけるrepositoryのインターフェースを作成します.
依存性逆転の原則(抽象に依存せよ)
先ほど説明したレイヤードアーキテクチャだけでは困ることがあります.それは,上位レイヤが下位レイヤの実体を持っている必要があることです.
下位レイヤの実装が変更されると,上位レイヤは影響を受けることや,上位レイヤは下位レイヤの実装後でないと参照できないなどの不便さが残ります.
そのため,interfaceを用いて,抽象に依存します.これにより,上位レイヤは下位レイヤの詳細は知らずとも良くなります.
実装
package repository
// IUserRepository is ...
type IUserRepository interface {
FindUserByUserToken(ctx context.Context, token string) (*db.User, error)
CreateUser(user *db.User) (string, error)
Login(uID string, password []byte) (string, error)
}
service
実装
- アプリケーション層がサービス層を参照するためにインターフェースを定義しています.
- インフラ層のインターフェースに依存した構造体を定義しています.
- 構造体を生成するメソッドを定義しています(のちに解説しますがこれがDIと呼ばれるものです.)
- 定義したインターフェースに合わせてメソッドを実装します.
package service
// IUserService ...
type IUserService interface {
GetMe(ctx context.Context, token string) (*rpc_user.GetUser, error)
SignIn(ctx context.Context, uID, password string) (string, error)
SignUp(ctx context.Context, user *rpc_user.PostUser) (string, error)
}
type userService struct {
irepo.IUserRepository
}
// NewUserService is ...
func NewUserService(repo irepo.IUserRepository) IUserService {
return &userService{repo}
}
func (srv *userService) GetMe(ctx context.Context, token string) (*rpc_user.GetUser, error) {
user, err := srv.IUserRepository.FindUserByUserToken(ctx, token)
if err != nil {
return nil, err
}
return dbToPostUser(user), nil
}
func dbToPostUser(user *db.User) *rpc_user.GetUser {
return &rpc_user.GetUser{
Name: user.Name,
CityName: user.CityName,
}
}
アプリケーション層
implement
実装
Application層が依存するもの無くない?と思われると思います.
これはGoの特徴なのですが,DIを行うメソッドを定義した際に,インターフェースを返り値として実装しています.
このようにすることで,定義したインターフェースの内容を満たさないと関数が実装されたとコンパイラが認めないため,この構造体は定義したメソッドを全て含んでいるということを担保することができます.
package implements
// IUserImplement is ...
type IUserImplement interface {
Get(ctx context.Context, p *rpc_user.GetRequest) (*rpc_user.GetResponse, error)
Login(ctx context.Context, p *rpc_user.LoginRequest) (*rpc_user.LoginResponse, error)
Post(ctx context.Context, p *rpc_user.PostRequest) (*rpc_user.PostResponse, error)
}
type userImplement struct {
isrv.IUserService
*logrus.Logger
}
// NewUserImplement is ...
func NewUserImplement(s isrv.IUserService, l *logrus.Logger) IUserImplement {
return &userImplement{s, l}
}
func (imp *userImplement) Get(ctx context.Context, p *rpc_user.GetRequest) (*rpc_user.GetResponse, error) {
imp.Logger.Infoln("[START] GetMyBoughtVegetablesRPC is Called from Client")
token := p.GetToken()
user, err := imp.IUserService.GetMe(ctx, token)
if err != nil {
return nil, err
}
res := rpc_user.GetResponse{
User: user,
}
imp.Logger.Infoln("[END] GetMyBoughtVegetablesRPC is Called from Client")
return &res, nil
}
###インフラ層
persistence
責務はデータ永続化です
model
実装
DBに受け渡すデータを構造体として定義します
package db
// User is ...
type User struct {
BaseModel
Name string
CityName string
Password []byte
AccessToken string
}
mssql
実装
特徴としては,database/sqlを用いて,SQL文を直書きしています.
理由としては,orm
は独自の構文を使用するため他ライブラリの以降にコストがかかりますが,SQL文だと汎用性があるためです.
package mssql
// UserRepository is
type UserRepository struct {
*sql.DB
}
// NewUserRepository is ...
func NewUserRepository(Conn *sql.DB) irepo.IUserRepository {
return &UserRepository{Conn}
}
// FindUserByUserToken is ...
func (repo *UserRepository) FindUserByUserToken(ctx context.Context, token string) (*db.User, error) {
dbUser := db.User{}
if err := repo.DB.QueryRow("SELECT CityName FROM [Weather].[dbo].[Users] WHERE AccessToken = " + token).Scan(&dbUser.CityName); err != nil {
return nil, err
}
return &dbUser, nil
}
その他
auth
JWT(Json Web Token)という技術を用いています.興味ある方はこちらを参照
di
依存性注入(Dependency Injection)
先ほどからDIDIと言ってきましたが,DIとは外部からクラスを射し込むことを言います.
実装
テストを書くときを考えてみてください.
サービス層のテストをするとなると,DBの情報が欲しいとなります.
となるとリポジトリが必要そうです.しかしこれでは,DBに依存してしまい切り分けた意味がなくなってしまいます.
DIをすることにより,決まった値を返すモックのリポジトリを注入することができ,晴れて依存性を解消できるという訳です.
package di
var logger = logrus.New()
// InitializeUser is ...
func InitializeUser() implements.IUserImplement {
repo := mssql.NewUserRepository(handler.OpenDBConnection())
srv := service.NewUserService(repo)
imp := implements.NewUserImplement(srv, logger)
return imp
}
handler
DBなどのコネクションを生成するメソッドを集めています.
rpc
gRPCの説明は後に回します