この記事はMicroservices Advent Calendarの18日目の記事です。前回は#1: デザインドキュメントを書こう
というお話でした。
なぜインターフェースは重要か
インターネットサービスにおいてユーザーインターフェースの重要性に意を唱える人はあまりいないと思います。ユーザーが内部の実装の都合を推し量らないと使えないようなサービスは、使い勝手の良いサービスとは言えないからです。
だからと言って、ユーザーが何の想定も持たずにサービスを利用しているかと言うとそうではなく、一定のメンタルモデルを持って使っています。ユーザーがそのサービスを上手く使えるためには、メンタルモデルが対象を上手く抽象化したものになっている必要があります。そして、メンタルモデルは多くの場合、ユーザーインターフェースを通じたインタラクションによって形成されるため、概してユーザーインターフェースは重要なものだと認識されています。
これは日常生活におけるユーザーインターフェースの立ち位置の話ですが、マイクロサービスにおいても同じことが言えます。つまり、 良いマイクロサービスは良いインターフェースを備えている ことが必要なのです。
前回は、マイクロサービスが全体として担う責務を定義しました。今回は、より個別事項に進んで、インターフェースを定義する話になります。
LinkTracker の例
それでは簡単な例を見てみましょう。前回登場したマイクロサービス LinkTracker の責務はこうでした。
責務
. 一言で:トラッキング可能な URL を発行し、ハンドリングする。より詳しく:
発行される URL は特定のソースに紐付けることができる。例えば、実際に送信された一通のメールをソース> とみなし、そこに含まれる10個のリンクを全てそのメールと紐付けてトラックできる。
ソースは様々な情報をメタ情報として持ち、API リクエストを通じてそれを保存できる。
ここであらかじめ断っておくと、いきなりこの責務を定義できたわけではありません。実は LinkTracker は当初、MailTracker という名前でした。元々、メーラーという JavaScript の制御が及ばない外部のアクションをトラッキングするために作ったからです(そういう機能が Wantedly の Mother Rails にありました)。しかし、プロトタイプを作ってインターフェースを定義するうちに、「メール」という要素は本質的には不要であることが分かり、LinkTracker としました。結果的にメール以外の箇所でも使われているので、適切な抽象化だったと思います。
この話は、これほどシンプルなマイクロサービスであっても、振る舞いと実装・抽象的な事柄と具体的な事柄の間を行き来することが必要だったという例でした。ちなみに、前回紹介したもう一つ、UsersService の場合は、Wantedly のユーザーというドメインにあらかじめ習熟していたため、完全にトップダウンに定義しました。
話を戻しましょう。仮に実装の影響を受けたとしても、一度定義した責務は尊重し、そこからインターフェースを考える方が良いです。実際に必要な API は3つです。順に見ていきましょう。
1) トラッキングリンクを発行する API
責務から、まずトラッキングリンクを発行する API は必要でしょう。インターフェース定義言語である Protocol Buffer で書くと、以下のようになります(Wantedly では Golang のマイクロサービスは grapi という Protocol Buffer ベースのフレームワークを利用しています)。
rpc CreateTrackingLink (CreateTrackingLinkRequest) returns (TrackingLink) {
option (google.api.http) = {
post: "/sources/{source_id}/tracking_links"
body: "*"
};
}
message CreateTrackingLinkRequest {
string source_id = 1;
string original_url = 2;
}
message TrackingLink {
string url = 1;
}
ここでは入力として、対象となる URL の他に source_id という uuid を受け取れるようになっています。これは責務として書いた「発行される URL は特定のソースに紐付けることができる」に対応します。返却値である TrackingLink はリダイレクト URL を含んでいます(ここでは表現されていませんが、この URL は token というパラメータを持っています)。
2) リダイレクトを行う API
リダイレクトを行う API が、次の定義です(厳密に言えば API ではないですが)。
rpc GetTrackingLink (GetTrackingLinkRequest) returns (Redirection) {
option (google.api.http) = {
get: "/t/**"
};
}
message GetTrackingLinkRequest {
string token = 1;
}
message Redirection {
string x_redirect_url = 1;
}
token を受け取ってリダイレクトするというシンプルな定義です。
3) ソース情報を保存する API
最後に、「ソースに関する様々な情報を保存できる」というのが以下の API で実現できます。ここでは、どういった種類のメールがどういったユーザーに送られているのか、というのを保存できるようにしています。
rpc CreateSource (CreateSourceRequest) returns (Source) {
option (google.api.http) = {
post: "/sources"
body: "*"
};
}
message CreateSourceRequest {
message SourceParams {
google.protobuf.UInt32Value user_id = 1;
google.protobuf.StringValue mailer_class = 2;
google.protobuf.StringValue mailer_action = 3;
google.protobuf.StringValue mailer_variation = 4;
google.protobuf.StringValue post_uuid = 5;
}
google.protobuf.StringValue source_id = 1;
SourceParams params = 2;
}
LinkTracker の実装に必要な API はこれで全てです。
grapi を利用したワークフローでは、この Protocol Buffer の定義を所定の箇所に起き、 grapi protoc を実行して Golang 用の定義などを生成した後、Pull Request にしてレビューします。インターフェースを実装から切り離してレビューすることで、インターフェースが理解しやすいものか、利用する側の要求を満たしているかをチェックしましょう。
UsersService の例
同様に前回紹介した UsersService の例を紹介したいところですが、これは結構大きいので、また余裕があれば書こうと思います。
コラム:インターフェースはディテールが大事
最後に少し強調しておきたいことがあります。それは、インターフェースの良し悪しというのはなにができる API かと言う機能面だけではなく、リクエスト・ボディの型(構造)やちょっとした命名などの詳細にかなり影響を受けるということです。
例えば、Suica の自動改札機はなぜ全国津々浦々、少し角度が付いたものになっているのか。設計した山中先生によれば、初めて利用する人でも間違いなく使えるためには、あの UI が重要だったと言います。
実験では驚くような光景がたくさん見られました。今では考えられないことですが、カードを縦に当てる人、アンテナの上で激しく振る人、有人の改札機のようにカードを機械に見せて通ろうとする人、ともかく光っている所にかざす人…。
いろいろな形のアンテナを試してみると、解決策は意外にシンプルな所にありました。「手前に少し傾いている光るアンテナ面」、それだけで多くの人がちゃんと当ててくれることがわかったのです。
...
それらの結果をふまえて作られた改良型による1999年の実験では、読み取り率は劇的に向上し、5割近かったエラー率が、1%以下に下がりました。
http://lleedd.com/blog/2010/11/25/suica_2/
この「ちょっとしたことで使い勝手が変わる」ということについてはソフトウェアも同じです。たとえ論理的に等価なものであっても、良いインターフェースと悪いインターフェースでは関わる人の生産性に十倍程度の差が出ることは普通にあります。なので頻繁に参照されるインターフェースのディテールは特に大事にした方が良いでしょう。
...ということで、マイクロサービスの重要な側面であるインターフェースについて話しました。