北裕介と申します。メルカリというサンフランシスコに拠点を置く日本企業で働いています。eBayのようなマーケットプレイスアプリを作っていて、アメリカのユーザーに向けて国際化対応をしています。
今日は大きなメリットを享受できるProtocol Bufferによって得られた知見を共有したいと思います。
Web APIs
TwitterやGitHubクライアントのように、多くのアプリではネットワーク通信が行われています。たいていの場合、APIを呼ぶと、サーバーからJSONが返ってきます。これはデータをシリアライズするのに当たり前な方法だと思われがちですが、これは Swiftらしくないな と思いながらSwiftのコードを書いています。
次のように JSONオブジェクトをData型 に変換して、HTTPリクエストを送信する例を見てみましょう。
let json = ["id": 1]
let data = try? JSONSerialization.data(
withJSONObject: json,
options: []
)
var request = URLRequest(url: url)
request.httpBody = data
このリクエストは、userIdを送ると、サーバーからユーザ情報を取得するものとなっています。keyはString型で、ID自身はInt型です。これをData型に変換し、httpBodyにセットします。
レスポンスはURLSessionを介してData型からJSONオブジェクトへ変換されます。
{"user": {"name": "Yusuke"}}
URLSession.shared.dataTask(with: request) { (data, _, _) in
let json = (try? JSONSerialization.jsonObject(
with: data!,
options: []
)) as? [String: Any]
let user = json?["user"] as? [String: Any]
let name = user?["name"] as? String
}
ここで、IDをミスタイプしたり、間違うと失敗します。Swiftはすべてが型の世界です。JSONには Any
型を使われてしまうので、Swiftらしくないと思っています。
Protocol Buffers
Protocol Buffer(Protobuf)は、2008年に開発されたシリアライゼーション用のフォーマットです。
Example
先程の例をProtobufで見てみましょう。Protobufでは自分で型をカスタムして定義することができます。
custom type
let userRequest = UserRequest.with {
$0.id = 1
}
let body = try? userRequest.serializedData()
var request = URLRequest(url: url)
request.httpBody = body
keyの名前をStringリテラルで書く必要はありません。Protobufには serializedData()
というシリアライズメソッドがあります。これを呼ぶことで、httpBodyにセットできます。
レスポンスを見てみると、Protobufを使うことで、 UserResponse
型を用いることができます。UserResponse型は私が定義したものです。UserResponse型には name
プロパティを持った User
型を持たせています。Data型をUserResponse型にデシリアライズするだけで、プロパティにアクセスできるようになります。
UserResponse(user: User(name: "Yusuke"))
URLSession.shared.dataTask(with: request) { (data, _, _) in
// custom type
guard let response = try? UserResponse(serializedData: data!) else {
// error
return
}
let user = response.user
let name = user.name
}
Protobufには型があるので、よりSwiftらしく書けます。
使い方
Protobufを使うには、Protobufのコンパイラ(最新版は3.3)をインストールします。Appleから公式にリリースされているSwift用のプラグインも必要です。
ProtobufはObjective-CとSwift両方をサポートしています。両方同時に使うこともできますよ!
Protobufの実装にはやることが3つあります。
- Message型の定義
- コード生成
- シリアライズ/デシリアライズ
Message型は .proto ファイル内のデータ構造を定義したkey-valueベースのコード
iOSConのプロフィールページを例に、 .protoファイルを作ってみましょう。このファイルには、ユーザーのID、名前、紹介文へのURL、ユーザータイプがあります。どのフィールドにも型(intやstring, enum)があります。
talk.proto syntax = "proto3";
import "user.proto";
message Talk {
int32 id = 1;
string title = 2;
string desc = 3;
User speaker = 4;
repeated string tags = 5; // Array
}
talk.protoファイルもあります。 これにはID、タイトル、説明、ユーザータイプ、およびタグを持っています。これは似ていますが、User型を使っているので、user.protoを一番上でインポートし、Protobuf自体のバージョンを指定しなければなりません(最新バージョンは無料です)。なので、proto3と書きます。
Protobufコンパイラは .proto
ファイルからすべてのコードを生成する
Protobufは .proto
ファイルからすべてのコードを生成します。
サポートされている基本的な型には、int, Bool, string, Dictionaryがあります。C++, JavaScript, Ruby, Rustのような他の言語もサポートしています。
以下はAppleによって公式にサポートされているSwift版の特徴です。
- structとなっており、データ構造を持つ
-
struct
であり、class
ではない。 - enumを定義できるが、
RawValue
は常にint
型となる。
どのプロパティにもデフォルト値があります。これはプロパティ自体はOptional型を持てないことになります。usersのnameプロパティをチェックする際、それが空のStringかそうでないかを確認する必要があります。Optionalではなく、空のStringがデフォルト値となるからです。
シリアライズ/デシリアライズ
シリアライズはProtobuf自体に実装されていますし、JSONやtextを受け取れます。seralizedData();
を呼ぶだけでいいのです。
バイナリエンコーディング
message Talk {
int32 id = 1; ← // Field number
string title = 2;
string desc = 3;
User speaker = 4;
repeated string tags = 5;
}
ワイヤータイプという型のグループがあり、intなら0、stringなら2となります。
次のstructを考えてみましょう。
message Test1 {
int32 a = 1;
}
test1.a = 300
// encoded message
08 96 01
08 // field number and wire type
96 01 // value which is 300
300について見てみましょう。 a
はint型で、エンコードした結果は 08 96 01
となっています。最初の2桁はフィールド番号とワイヤータイプの組み合わせを表しています.
バイナリエンコーディングによって、JSONに比べサイズが小さくなり、すべて数値となります。その結果、ネットワークのパフォーマンスがよくなります。
使用上の注意
バージョン管理
Protobufには、後方互換性があります。レスポンスにnon-fieldがある場合、レスポンスは無視されます。または、レスポンスに何か足りないものがある場合には、デフォルト値になります。サーバー側から新しいプロパティーを簡単に追加したり、クライアント側にある使っていないプロパティーを削除したりすることができます。
protobufとJSONの共存
HTTPのヘッダにあるcontent typeをみることで、JSONとProtobufをどちらも持たせることができます。返り値をJSONかProtobufにするかを設定できます。
デメリットは何か
Protobufはバイナリデータなので、人間が読める形式ではありません。コンソールで生データを見るには、カスタム型にデシリアライズする必要があります。
Protobufを使い始めると、最初は時間がかかります。Protobufの全てのことを知っている必要があることに加えて、iOSプロジェクトだけでなく、他のプラットフォームでも使われるからです。
Codable
つい先日、SwiftにCodableが追加されることが発表されました。これによりシリアライズに型安全性が担保されます。Protocolになっていて、Protobufと同じようにCodableを扱うことができます。SwiftのFoundationライブラリに公式に含まれます。
Q & A
Q: ProtobufはRubyでも使えますか? Yusuke: はい、Rubyでも使えますよ。
About the content
This talk was delivered live in June 2017 at AltConf. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.