blog.syfm

徒然なるままに考えていることなどを書いていくブログ

Clean Architecture で Go 製の API サーバを書く

この記事は Aizu Advent Calendar の 20 日目の記事です。

adventar.org

前日は id:nktafuse の 、

nktafuse.hatenablog.com

明日は id:ywkw1717 です。

ywkw1717.hatenablog.com

きっかけ

最近職場で Clean Architecture を取り入れたアプリケーションを Python で書いていて、Go でも書けないか?という試みではじめました。

作ったものは、サーバにアップロードしたファイル自体の改竄を検出できる簡易ファイルアップローダみたいなものです。

github.com

以下では CA について簡単に解説した後、設計・実装する上で悩んだ点などを挙げています。

Clean Architecture

原文と翻訳版は以下。

8thlight.com

クリーンアーキテクチャ(The Clean Architecture翻訳) | blog.tai2.net

注意書き
ここでの CA の説明はあくまで自分が記事を読んで自分なりに解釈したものです。
ソースとは異なる部分もあるかもしれないので注意してください。

概要

CA は上下のあるレイヤードアーキテクチャ等とは異なり、円になっています。

https://blog.tai2.net/images/CleanArchitecture.jpg

各レイヤーは内側のレイヤーにのみ依存を持つことができ、外側に依存することはできません。
したがって、中央のエンティティ層はいかなるレイヤーにも依存できません。
「依存」という言葉は「使う」という言葉にほぼ同義で置き換えられると思います。

この依存ルールを守ることで、以下のような恩恵が得られます。
原文で上げているいくつかのメリットに自分なりの意見を入れています。

  1. フレームワーク独立 - フレームワークに引っ張られることを防止し、ビジネスロジックが壊されるのを防ぎつつ、効果的にフレームワークを使える
  2. テスタビリティの向上 - 各レイヤーが疎結合になることで容易にテストができる
  3. UI 独立 - UI にビジネスロジックが引っ張られることを防止し、容易に UI を入れ替えることができる
  4. データベース独立 - MySQLPostgreSQL の様な各 DBMS の実装に引っ張られることがなくなる

原文の 5 番目で言及しているとおり、上記の全ては一つの仕組みによって実現されています。
以降の説明で紹介していきます。

エンティティ層

エンティティ層は中央の層で、他のどの層にも依存しません。
このレイヤーには各アプリケーションに依存しないビジネスロジックが属します。
一見何を言っているのかわかりませんが、実際の例で説明します。

ユースケース

エンティティにのみ依存するレイヤーです。
ユースケース層は他のアーキテクチャ等ではアプリケーション層と呼ばれるように、アプリケーション固有のビジネスロジックが入ります。

アダプタ層

原文だとインターフェースアダプターと呼ばれていますが、ちょっと長いのでアダプタ層と略します。
ユースケース層・エンティティ層で使用するデータフォーマットから外部のシステムが使うフォーマットへの変換を担う層です。
ここでは、「外部システム」という言葉が出てきているとおり、各システムの実装が初めて出てきます。
例えば、エンティティから何かしらのデータが渡されて、それを SQL を使っている RDBMS へ保存するというケースを考えてみましょう。

RDBMS で「保存する」と言うのは、つまり永続化のことです。 (これはドメインの仕様によりますがほぼ永続化のことでしょう)
つまり、アダプタ層で行うことというのは、エンティティから返されたデータを加工して、INSERT や UPDATE などの SQL 文を発行するということを指しています。
アプリケーション層やエンティティ層はこれを知るよしもありません。アダプタ層はそれらより外側のレイヤーなので依存できないからです。

ドライバ層

原文だとフレームワーク & ドライバ層と呼ばれています。
SQL DB の場合、Go だと既に database/sql で抽象化されていて、かつ入力も Web Request でしか受け付けないことが前提なので、今回はドライバ層とアダプタ層を一括りにして扱っています。

DIP、DI

各レイヤーをまたがる場合を考えます。
例えば、Web サーバアプリケーションを作っている場合、その controller はアダプタ層に存在します。 リクエストが来た場合、それをアダプタ層の controller が受け取ってユースケース層のメソッドを呼び出します。
ユースケースは、その処理の終わりに結果を返さなければいけませんが、外側に干渉するような操作は依存することになるのでできません。
そこで、DIP を使ってこれを解決します。

DIPDependency Inverse Principal の略で、日本語だと「依存関係逆転の原則」などと呼ばれます。
本来依存する側 (今回はユースケース層) がインターフェースを定義し、それを外側の層が実装します。
これは、インターフェースという抽象型に依存しているので、 「抽象に依存する」 と言います。
レイヤー間を抽象に依存するようにしてあげると、疎結合なソフトウェアを構築することができます。
ユースケースはただ自分が定義したインターフェースを満たした実装を利用することができるようになるので、依存関係を逆転させることができます。

上で説明したとおり、アダプタ層で実装を与え、それをユースケース層・エンティティ層が使うために、何らかの方法で実装を渡さなければいけません。
このために DI を使います。DI は Dependency Injection の略で、「依存性の注入」などとよく呼ばれています。もっとも簡単な方法として、引数の型をインターフェースにし、その関数へインターフェースを満たしている実装を渡してあげる方法があり、今回もこれを利用しています。

type Greeter interface {
    Hello()
}

type English struct {}

func (e *English) Hello() {
    fmt.Println("Hello")
}

type Japanese struct {}

func (j *Japanese) Hello() {
    fmt.Println("こんにちは")
}

func doHello(greeter Greeter) {
    greeter.Hello()
} 

func main() {
    doHello(&English{})  // "Hello"
    doHello(&Japanese{})  // "こんにちは
}

ただの部分型付けポリモーフィズムですね。

実際に適用する

再掲
github.com

ただ、全部を書くと膨大になるので、一部だけ紹介しています。
アプリケーションのシーケンス図は以下の様になっています 👇

f:id:ktr_0731:20171220021439p:plain

エンティティ層

このアプリケーションではエンティティ層は domain という名前のパッケージが相当します。
小さなアプリケーションなので、エンティティは file.go に定義されている File 型しかありません。

また、そのエンティティの永続化を行うリポジトリFileRepository が定義されています。
ここで注目してほしいのが、 FileRepository は interface で定義されている点です。つまり、実装はエンティティ層には存在していません。実装は、CA での説明でも触れたとおり、アダプタ層にあります。

ユースケース

ユースケース層に相当するパッケージは、usecases パッケージです。

usecases/ports には、ユースケースが必要とする port を定義しています。
具体的には、 BlockchainPortCryptoPortStoragePort があります。
また、InputPort / OutputPort が server.go に定義されています。
これは、上で紹介した図の右下の図に書いてあり、サーバへの input とそのレスポンスに相当する output を DIP でどう表現するか?が原文で説明されています。

この図から分かる通り、I/O port はユースケース層で定義され、input port の実装は interactor です。
また、input port に controller が依存しています。
同じように、output port の実装はアダプタ層の presenter で、今度は interactor が output port に依存しています。

interactor は、各ユースケースに DI をする役割を担っています。
interactor のコンストラクタが呼ばれる時に、port の実装である adapter をすべて受け取ります。しかし、ユースケース層は adapter のことを知らない (知ってはいけない) ため、抽象型で受け取ります。

アダプタ層

アダプタ層に相当するパッケージは adapterspresenters です。
二つのパッケージに分けるかどうかは完全に好みです。

presenters は、上のセクションで触れたとおり、output port インターフェースを満たした実装が入っています。
adapters も、各 port の実装が入っています。
ここで注目すべきは、 port のインターフェースを満たしていれば、あらゆる adapter を自由に渡せることです。
例えば、 adapters/repositories には FileRepositoryPort を実装した MockFileRepository がありますが、これは後から MySQLFileRepository などのように、RDBMS をバックエンドにしたものを作り、interactor へこれを渡すようにすれば、アダプタ層より下のレイヤーに全く影響を与えずに実装を入れ替えることができます。

テスト

各レイヤーが疎結合になっているため、簡単にテストを書くことができます。
例えば、 usecases/usecases_test.go を見てみると、repository や storage などの port で定義されている外部パーツは、port を満たすダミー実装に置き換えていることが分かるかと思います。
このように、外部依存に苦しまずにアプリケーションロジックにのみ集中してテストを書くことができます。

まとめ

ざっとですが、Clean Architecture の説明とその実装を紹介しました。
Clean Architecture は、外部システムの仕様変更が予期される場合に対して非常に強固にアプリケーション以下のレイヤーを保護してくれます。
ファイル数は比較的多くなってしまいますが、しっかりとレイヤーを分けられることと比べれば、デメリットとしては小さいのではないかと思っています。

もし、なにか根本的に間違っているようなことがあれば、指摘頂けると助かります 🙇‍♂️

GitHub に草を生やし続けて一年が経った

去年の今頃から毎日 GitHub に commit しようと決心し、最近 365 日を達成しました!

f:id:ktr_0731:20171130052118p:plainf:id:ktr_0731:20171130052142p:plain
いい感じ

なぜかデフォルト表示の方の左上が白くなってますが、ちゃんと一年経っています。

意図

そもそもなぜ始めようとしたかというと、技術の習得には input と output が大事で、input で得たものが output によって定着すると思っていたからです。
output の手段には、ブログや Qiita を書いたり、LT だとかハッカソンでもいいかもしれません。実際、自分も LT でよく発表したりしています。
とはいえ、それらはどのくらい output できているかの可視化ができないので、モチベーションを保つのは少し大変かもしれないと思っていました。
そのため、GitHub の草を output の可視化として利用したらいい感じになるんじゃないかなと思って始めました 🙌

今年の春ごろに LT でこのことについて発表していたので、それも貼っておきます

speakerdeck.com

どう変わったか?

以下は草を生やし始めたころの画像です

f:id:ktr_0731:20171130052957p:plain

この頃と比べて 2.6 倍くらいコミット数が増えました。
また、今年作ったリポジトリの数は、プライベートなものも含めると大体 70 個でした。
ライブラリみたいなものもあれば、コマンドラインツールや、Web アプリのフロント/サーバ、そもそもソフトウェアではないものもあります。
アルバイト先でも GitHub を利用しているので、そのコミットも入っています。

毎日コードを書く時間を意図的に確保することで必然的にコードと向かい合う習慣が付くので、あまり苦に感じたりはしませんでした。
また、当然 OSS を触るので、自分が書いたコードを読む/新しく書く以外にも、OSS の実装を読んだり、バグを踏んだものを fork、修正して Pull Request を送ったりもしていました。
OSS と聞くとハードルが高そうな感じがしますが、実際はあんまりそういうことはなくて、ちょっとしたバグであれば一時間もかからずに直せたりします。

ちなみに、今年パッチを送った最大のソフトウェアは Go でした!(これも一瞬で修正できました ✨)

syfm.hatenablog.com

また、モチベーションを上げるために pocke さんのつくったサービスである、Shibart で草グッズを作ったりもしていましたが、めちゃくちゃ愛着が湧くのでおすすめです

f:id:ktr_0731:20171130055310p:plain

shibart.pocke.me

悪かった点

毎日というと、旅行や病気などで、どうしてもコードを書けないときが出てきます。
そういう時は当然ながら dotfiles をいじったりするだけだったりしてあまり意味のあることはできませんでした。
毎日しっかりコードを書く、というのは正直限界があって、そこをどうするかは結構大切な気がします。
連続 commit 日数が途切れてもいいならば、無理やり commit する必要もないと思います。
自分の場合は、上述のとおり、dotfiles をいじったり、作りたいものを issue / project として管理するリポジトリを用意してそこに新しく登録したりしていました。

改善すべき点もありましたが、これからも続く限りやっていきたいと思っています 💪
モチベーションが上がらない人はぜひ試してみて下さい

より気軽に、簡単にテストができる gRPC クライアントをつくった

先日の A(izu)LT 0x09 で「Evans: more expressive universal gRPC client」 というタイトルで発表してきました。
タイトルの通り、gRPC クライアントをつくった話です 🙌

atnd.org

ALT は、会津大学の有志によって年に二回ほど開催されている LT (Lightning Talk) 会で、次回でついに 0x0a 回目を迎えます!めでたい 🎉

発表したスライドはアップロードしています。大体のことは以下に載っているけど、ちょっと補足をしておきたいと思います。

speakerdeck.com

後日、Go 1.10 Release Party in Tokyo にて、より整理した内容で発表を行いました。

speakerdeck.com

つくったもの

github.com

Evans の目指したもの

Evans は既存の gRPC クライアントより簡単に扱えることを第一目標に作っています。
(gRPC に限らずですが、) gRPC を使っているプロダクト自体の品質を保ちたければ、gRPC を使ったテストも書いて CI で回せばいい話なので、gRPC クライアントはそれ以外のユースケースを補うためにあるべきだと思っています。

では補うべき部分とは一体なんなのかというと、

  • スクリプトによって何かのタスクを自動化したい
  • API を手動で検証したい

この二つだと思っています。
そして、この二つはそれぞれ以下の二つのモードと対応しています。

🖥️ コマンドラインモード

コマンドラインモードは、その名の通り一回のコマンドのみで完結するステートレスなモードで、既存の gRPC クライアントの Polyglot が採用している方式でもあります。

github.com

コマンドラインモードは、単なるコマンドなので、UNIX 哲学に則ってつくっています。
例えば、stdin から入力を受け取ることでフィルターとしての役割を果たすことができますし、その一方で出力は stdout へ JSON 形式で行われます。そのため、jq コマンドなどで適当に加工したりすることが可能です。

$ echo '{ "name": "ktr" }' | evans --package hello --service Greeter --call SayHello hello.proto | jq -r '.message'
# hello, ktr!

また、ファイルへ入力の JSON を保存して、何度も使いたい場合などのケースのために --file (-f) オプションを用意しており、こちらでも同じような動作ができます。

$ cat hello.json
{
  "name": "ktr"
}

$ evans -f hello.json --package hello --service Greeter --call SayHello hello.proto | jq -r '.message'
# hello, ktr!

ちなみに、コマンドラインモードでは事前に使用する package、service、RPC 名を知っている必要があるため、インタラクティブモードとは違い引数を省略できません。しかし、Git プロジェクトルートに .evans.toml がある場合は、デフォルトで使用するものを記述することができます。

[default]
package = "hello"
service = "Greeter"

こうすると以下のようにシンプルにコマンドを指定できます

$ evans -f hello.json --call SayHello hello.proto

🤼 インタラクティブモード

インタラクティブモードは、「API を手動で検証したい」時のためのモードです。
こちらの方は、なにも考えなくても使えるようにつくっています。
補完機能が使えるため、package 名や service 名、RPC 名を覚えていなくても大丈夫です!
このモードは MySQL のクライアントである、 dbcli/mycli を意識しています。

github.com

実際のデモはこんな感じです 👇

demo

読み込んでいる proto ファイル

syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

enum Language {
  ENGLISH = 0;
  JAPANESE = 1;
}

message Person {
  string name = 1;
  repeated string others = 2;
}

message HelloRequest {
  Person person = 1;
  Language language = 2;
}

message HelloResponse {
  string message = 1;
}

サーバの実装

package main

import (
    "fmt"
    "log"
    "net"
    "strings"

    "golang.org/x/net/context"

    helloworld "github.com/ktr0731/evans/tmp"
    "google.golang.org/grpc"
)

type Greeter struct{}

func (t *Greeter) SayHello(ctx context.Context, req *helloworld.HelloRequest) (*helloworld.HelloResponse, error) {
    msg := "Hello, %s also %s!"
    if req.GetLanguage() == helloworld.Language_JAPANESE {
        msg = "こんにちは、%s と %s !"
    }
    return &helloworld.HelloResponse{
        Message: fmt.Sprintf(msg, req.GetPerson().GetName(), strings.Join(req.GetPerson().GetOthers(), ", ")),
    }, nil
}

func main() {
    l, err := net.Listen("tcp", "localhost:50051")
    if err != nil {
        log.Fatal(err)
    }

    server := grpc.NewServer()
    helloworld.RegisterGreeterServer(server, &Greeter{})
    if err := server.Serve(l); err != nil {
        log.Fatal(err)
    }
}

べんりですね。

インストール

macOS / Linux 向けのバイナリが GitHub Releases にあるので、それを使えます。
もちろん、go get で取得することもできます。
macOS であれば、brew を使ってのインストールもできます 🍺

$ brew tap ktr0731/evans
$ brew install evans

Issue / PR

このツールは LT 駆動開発によって誕生したので、テストがありません 😱
なので、一旦新規の機能開発はやめてテストを充実させようと思っています 🙇‍♂️
その後、スライドでも言及したような TODO タスクを消化していきたいと思っています。
もし使っていただける方がいましたら、ぜひ Issue でも PR でもどんどんいただけたらありがたいです!

6 月 20 日追記
ある程度のテストを書き、最初のスライドで TODO として挙げられていた機能もいくつか実装しています。
現在の状況については、ktr0731/evans をご覧ください。