Slide 1

Slide 1 text

Jumpei Takiyasu @juntaki M3, Inc. 2019-05-18 Go Conference 2019 Spring

Slide 2

Slide 2 text

name <- "Jumpei Takiyasu" twitter <- "juntaki" company <- "M3, Inc." title <- "Software Engineer/TL/PM" website <- "https://juntaki.com" Me

Slide 3

Slide 3 text

きょうの話 保守性の高いWebアプリを早く作るにはどうしたらよいか 1. アーキテクチャをかんがえる 2. 自動生成をつかって、はやくつくる

Slide 4

Slide 4 text

サンプルアプリ GAE/Goで技術書典むけのダウンロードカードアプリを作った https://www.m3tech.blog/entry/m3techbook-01/qrcode 「電子書籍ダウンロードカードの生成アプリ」 の作り方が書いてある本を↑を使って頒布しました 仕様 ● 各カードに固有のQRコード(追跡可能) ● DLされるPDFの実体は1つ(差し替え可能)

Slide 5

Slide 5 text

アーキテクチャをかんがえる

Slide 6

Slide 6 text

システム構成 SPAとAPIサーバをAppEngineでサクッと開発したい

Slide 7

Slide 7 text

アーキテクチャをかんがえる Web APIの ※フロントエンド・インフラについては、今回の話では割愛します

Slide 8

Slide 8 text

クリーンアーキテクチャを参考にする ● 実装詳細と、ビジネスルールを分ける ● 依存方向を内向きに揃える 自動生成に向いているのは実装詳細 Clean Architecture 達人に学ぶソフトウェアの構造と設計 Robert C. Martin p. 200

Slide 9

Slide 9 text

実装詳細レイヤーの責務を扱う構造で整理 ※用語はDDD(Domain-Driven Design)より

Slide 10

Slide 10 text

各レイヤーの依存関係 外側のインターフェースを元にコードの自動生成が可能になる

Slide 11

Slide 11 text

依存性逆転 上位のレイヤーに下位のレイヤーが依存するという状態をどうやってつくるのか →上位のインターフェースを下位で実装する 依存性逆転の原則 - Wikipedia https://ja.wikipedia.org/wiki/依存性逆転の原則

Slide 12

Slide 12 text

いったんまとめ クリーンアーキテクチャのポイント ● 実装詳細と、ビジネスルールを分ける ● 依存方向を内向きに揃える 実装詳細とビジネスルールのレイヤーを分けて、依存方向を整理した! →外部インターフェースの定義から、実装詳細が自動生成できそう!!

Slide 13

Slide 13 text

自動生成をつかって、はやくつくる

Slide 14

Slide 14 text

どうつくるか? 1. 全体で使えるツールを準備する 2. スキーマファーストで外部インタフェースを整理 3. アプリケーション層で↑の実装をつくる 4. ドメイン層でリポジトリのインタフェースをつくる 5. インフラストラクチャ層で↑の実装を作る 6. DIする 7. テストをなるべく生成する 作る順で説明します

Slide 15

Slide 15 text

1. 全体で使えるツールを準備する 2. スキーマファーストで外部インタフェースを整理 3. アプリケーション層で↑の実装をつくる 4. ドメイン層でリポジトリのインタフェースをつくる 5. インフラストラクチャ層で↑の実装を作る 6. DIする 7. テストをなるべく生成する

Slide 16

Slide 16 text

全体で使えるツール 次の2つがキモだが、めんどう ● interfaceを作って、実装する(依存性逆転のために必要) ● 各レイヤで構造をマップする(データ構造によって暗に依存性を壊さない)

Slide 17

Slide 17 text

impl:interfaceをみたすための関数群を自動生成 https://github.com/josharian/impl 依存性逆転のために各レイヤでinterface定義・実装を分けるとき便利 $ impl 'i *QRCodeReposioryImpl' \ github.com/juntaki/techbook-qrcode/src/domain.QRCodeRepository >> infra/qrcode.go func (i *QRCodeReposioryImpl) GetQRCodes(ctx context.Context) ([]*domain.QRCode, error) { panic("not implemented") } func (i *QRCodeReposioryImpl) IsExistQRCode(ctx context.Context, code *domain.QRCode) bool { panic("not implemented") } ...

Slide 18

Slide 18 text

fillstruct:structを初期値で埋める https://github.com/davidrjenni/reftools/tree/master/cmd/fillstruct レイヤーを分けると値のマッピングが頻繁に必要

Slide 19

Slide 19 text

1. 全体で使えるツールを準備する 2. スキーマファーストで外部インタフェースを整理 3. アプリケーション層で↑の実装をつくる 4. ドメイン層でリポジトリのインタフェースをつくる 5. インフラストラクチャ層で↑の実装を作る 6. DIする 7. テストをなるべく生成する gRPC-Webをつかいます

Slide 20

Slide 20 text

gRPC-Web でAPIエンドポイントを生成する Protocol buffersで外部インターフェースを定義して、自動生成する ● APIエンドポイントのハンドラ ● クライアントライブラリ

Slide 21

Slide 21 text

protoファイル 扱うべき構造とエンドポイントの Request/Responseを定義 syntax = "proto3"; package qrcode; message URL { string url = 1; } message QRCode { string id = 1; string url = 2; bytes image = 3; } message QRCodeList { repeated QRCode QRCodes = 1; } message Empty {} service QRCodeService { rpc GetURL(Empty) returns (URL) {} rpc UpdateURL(URL) returns (Empty) {} rpc GetQRCodes(Empty) returns (QRCodeList) {} rpc AddQRCodes(Empty) returns (Empty) {} }

Slide 22

Slide 22 text

自動生成のコマンドと生成されるもの一覧 ● gRPCサーバのためのGoのinterface ● Goのクライアントライブラリ(今回は使わない) ● JavaScriptのクライアントライブラリ ● TypeScriptの型定義 $ protoc \ -I proto \ --go_out=plugins=grpc:src/lib/qrcode \ --plugin=protoc-gen-ts=front/node_modules/.bin/protoc-gen-ts \ --js_out=import_style=commonjs,binary:./front/src/proto \ --ts_out=service=true:./front/src/proto \ proto/*.proto

Slide 23

Slide 23 text

APIエンドポイントはinterfaceを実装するだけ impl+fillstructでダミーの値をかえすように実装しておく APIサーバとして成立するので、フロントエンドと結合できる // QRCodeServiceServer is the server API for QRCodeService service. type QRCodeServiceServer interface { GetURL(context.Context, *Empty) (*URL, error) UpdateURL(context.Context, *URL) (*Empty, error) GetQRCodes(context.Context, *Empty) (*QRCodeList, error) AddQRCodes(context.Context, *Empty) (*Empty, error) }

Slide 24

Slide 24 text

gRPC-Webとしてマウントする grpcwebのMiddlewareを挟んであげる必要がある https://github.com/improbable-eng/grpc-web/tree/master/go/grpcweb s := grpc.NewServer() qs := application.NewQRCodeServiceServer() qrcode.RegisterQRCodeServiceServer(s, qs) // gRPC-Webのハンドラにする wrappedGrpc := http.StripPrefix("/grpc-web", grpcweb.WrapServer(s))

Slide 25

Slide 25 text

1. 全体で使えるツールを準備する 2. スキーマファーストで外部インタフェースを整理 3. アプリケーション層で↑の実装をつくる 4. ドメイン層でリポジトリのインタフェースをつくる 5. インフラストラクチャ層で↑の実装を作る 6. DIする 7. テストをなるべく生成する

Slide 26

Slide 26 text

ドメイン層で決めること ● 実装詳細と関係ないビジネスルールのための構造を定義 ● 永続化のためのインターフェース定義 type QRCode struct { ID string Index int } func (q *QRCode) GetURL() string { ... } func (q *QRCode) GetQRCode() []byte { ... } type QRCodeRepository interface { GetQRCodes(ctx context.Context) ([]*QRCode, error) ... } これを利用して アプリケーション層が作れる

Slide 27

Slide 27 text

インフラストラクチャ層のつくりかた テーブルの構造を決める→ORMでマップできるstructを生成する ドメイン層のリポジトリのinterfaceを満たすように実装していく

Slide 28

Slide 28 text

xormでDBスキーマからモデルを生成する https://github.com/go-xorm/cmd xorm以外のORM用のモデルも、ある程度はテンプレートの工夫だけでつくれる ちょっと改造すれば何でもいける(例:gorp用→https://s.juntaki.com/hAU $ mysql -uroot -pmysql -h 127.0.0.1 < ddl.sql $ xorm reverse mysql "root:mysql@tcp(127.0.0.1:3306)/mydb" template package models type Qrcode struct { Id string `db:"id"` } CREATE TABLE IF NOT EXISTS `mydb`.`qrcode` ( `id` CHAR(36) NOT NULL COMMENT 'ID', PRIMARY KEY (`id`))

Slide 29

Slide 29 text

wire による Dependency Injection インフラストラクチャ層の実装を、アプリケーション層にDIする wire:Google が開発したDIのためのコードジェネレータ https://github.com/google/wire

Slide 30

Slide 30 text

wireへの入力 //+build wireinject //+build wireinject package main ... func InitializeQRCodeServiceServer(db *gorp.DbMap) *application.QRCodeServiceServer { wire.Build(application.NewQRCodeServiceServer, infra.NewQRCodeRepositoryImpl) return &application.QRCodeServiceServer{} } ビルドタグがwireinjectなので、ビルド対象にならない →Goとしてみても一応問題ないので、コード補完などは効くようになる

Slide 31

Slide 31 text

wireが生成するもの 依存関係に応じた初期化コードが自動生成される // Code generated by Wire. DO NOT EDIT. //go:generate wire //+build !wireinject func InitializeQRCodeServiceServer(db *gorp.DbMap) *application.QRCodeServiceServer {  ... }

Slide 32

Slide 32 text

1. 全体で使えるツールを準備する 2. スキーマファーストで外部インタフェースを整理 3. アプリケーション層で↑の実装をつくる 4. ドメイン層でリポジトリのインタフェースをつくる 5. インフラストラクチャ層で↑の実装を作る 6. DIする 7. テストをなるべく生成する

Slide 33

Slide 33 text

gotestsでテストを生成する func TestQRCode_GetQRCode(t *testing.T) { type fields struct { ID string } tests := []struct { name string fields fields want []byte }{ // TODO: Add test cases. } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { q := &QRCode{ ID: tt.fields.ID, } if got := q.GetQRCode(); !reflect.DeepEqual(got, tt.want) { t.Errorf("QRCode.GetQRCode() = %v, want %v", got, tt.want) } }) } } $ gotests -all -w domain/qrcode.go ここにテスト条件を書く

Slide 34

Slide 34 text

fillstructと組み合わせる テストケース入力もちょっと楽になる

Slide 35

Slide 35 text

juntaki/fixでGolden file testing https://github.com/juntaki/fix テストの正解を「前と変わらないこと」にする func TestQRCodeServiceServer_GetURL(t *testing.T) { ret, err := qrCodeServiceServer.GetQRCodes(ctx, &qrcode.Empty{}) if err != nil { t.Fatal(err) } // 1回目の実行では ret をシリアライズして保存します。 // 2回目からは同様にシリアライズして、保存されたファイルとの一致をチェックします。 err = fix.Fix(ret) if err != nil { t.Fatal(err) } }

Slide 36

Slide 36 text

まとめ 1. 全体で使えるツールを準備する:impl, fillstruct 2. スキーマファーストで外部インタフェースを整理:protoc 3. アプリケーション層で↑の実装をつくる:grpcweb 4. ドメイン層でリポジトリのインタフェースをつくる 5. インフラストラクチャ層で↑の実装を作る:xorm 6. DIする:wire 7. テストをなるべく生成する:gotests, fix 紹介したテクニックを活用してGoでAPI サーバをつくってみてください!!