サーバーサイドプログラミング言語としてのSwiftへの追加機能はクライアントとサーバーを同じ言語で使えるようにしただけでなく、APIやコードの再利用を可能としました。
このtry!Swiftの講演では、アプリ開発におけるクライアントとサーバーの新しいインタラクションモデルを紹介し、Swiftで素早くクライアント、サーバーを伴うアプリを作る方法をお見せします。
イントロダクション (0:00)
Chris Baileyです。Robert Dickersonと一緒にSwift@IBMのエンジニアチームで働いています。この発表では、Swiftでエンドツーエンドのアプリケーションを作ることについてお話します。この発表には2つの目的があります。
- なぜSwiftがクライアント同様、サーバーサイドで使うのに優れた言語であるのかを説明します。
- 実際に私たちが作ったアプリを使って、どうやったら自分で作れるようになるかをお見せします。
現代のアプリケーション設計 (0:44)
あるエンドユーザーがいます。彼はSwiftでクライアントサイドのアプリを作ろうとしています。それから何種類かのバックエンドのアプリケーションを作ることになります。例えば、JavaとかNode.jsとかRubyを使います。サードパーティのサービスも次々に使うことになるでしょう。ElasticserachとかTwitterとかTwilio、AWSや他のNoSQLデータベースなどです。オンプレミスなシステムで動いてるDB2とかOracleとかMySQLみたいな古いデータベースかもしれません。
みなさんに勧めたいのは、Swiftをユーザーの使うクライアントアプリだけではなく、むしろサーバーサイドでも使っていこうということです。これは開発者体験の統合、すなわち同じIDEが使えて、クライアントとサーバーサイドで同じコードを共有できるということなんです。
サーバーサイドSwift (1:47)
とてもいいことですよね、でもなぜサーバーサイドでSwiftなんでしょう。 __メリットはなんでしょう?__一つは、性能がとても良いことです。スライドの9枚目にパブリックベンチマークを用意しました。Aliothという団体の”Benchmarks Game“です。たくさんの言語のベンチマークがあって、4つのCPUが搭載されたLinuxのマシン上で計測されています。
これはSpectral Normというベンチマークで、数学的なクランチングによるものです。使用可能なCPUに負荷をかけていて、この場合はディスパッチを使って4つのCPUで実行しています。
Swiftは4秒で終了しました。 Javaは4.3秒と、ほぼ同じぐらいです。このテストにおけるSwiftとJavaの性能はほぼ同じですね。Node.jsを見てみましょう。15.8秒かかっています。SwiftやJavaの約4倍ですね。Rubyはどうでしょう。とても遅いです。Rubyは計算向きの言語ではありません。主な理由としてはNode.jsやJavaのようにJITコンパイラの形式をとっていないからです。
性能面で、Swiftはサーバーで実行するのにあたって、興味深い言語ですね。しかし、メモリの面でもすごいです。RSS(ベンチマークを実行するのに必要な物理メモリ領域)のベンチマークを見てみると、Swiftでは15MBで、Javaでは32MBです。必要なメモリはJavaの半分なのに、同じ性能です。Node.jsはJavaよりは若干少ないですが、Swiftよりずっと多いです。Rubyはたくさん必要ですね。
クラウド上でのSwiftの長所 (4:02)
クラウド上でアプリケーションを実行するとき、ほとんどのクラウドはメモリ使用量に応じてお金がかかります。なので256MBや512MBの容量を買うことになりますね。CPUは無料でついてます。これがどういうことかというと、Swiftがクラウド上で実行するのに理想的な言語だということです。パフォーマンスが���くて、メモリ使用量が少ないからです。同���金額でより容量がたくさんあるのと同じことで、より多くのインスタンスを実行できるということになります。クラウドにもいいし、サーバーにもいいですね。
開発者にとっても良いのが、これが「アイソモーフィックな開発」と言われていることです。この考えによれば、あなたはSwiftプロジェクトの開発者となります。クライアントとサーバーのプロジェクトを作れます。1つのプロジェクトで構成でき、同じコードで書けます。データベースや他のデータストアとの接続を保持することもできます。同時に、中間にSwaggerで定義されたAPIを生成できます。クライアントやAndroidで閲覧したくなるかもしれないからです。ですがあなたの第一級オブジェクトとして、クライアントとサーバーを伴ったSwiftアプリケーションを作っています。
またこれによって、同じツールを使えます。つまり、Xcodeです。同じビルドシステム、同じ構文チェックが使えます。どんな場合でも同じツールや技術が使えるのです。
これはどうやったらできるのでしょうか?
どうやったらできるのか (5:26)
どうしたらサーバーでSwiftを実行できるのでしょうか。まずサーバー上で動く言語を作らなくてはなりません。Swiftはオープンソースとなった昨年12月時点では、Linuxで動作しませんでした。Swiftのランタイムや標準ライブラリ、Foundationのほんの一部が含まれており、GCD(グランドセントラルディスパッチ)も含まれていましたが、コンパイルされてないし、どのテストも通っていませんでした。
今ではFoundationが動いていますし、GCDも完全に動作していますす。しかも両方Swift 3のツールチェインに含まれています。これでクライアントとサーバーの一貫したプラットフォームが提供されていることになります。次に必要なのがWebフレームワークですが、IBMではKitura.というフレームワークを開発しています。他にもたくさんのものが世に出ています。
サーバーサイドのコードが実行できるようにするために、必要なものを追加します。はじめにネットワークです。HTTPSを使うならセキュリティも要るし、セキュアなWebソケットやいくつかのHTTP解析ツールも必要です。
これをするのに3つのライブラリを追加しています。それからKituraを入れています。すると、クライアントアプリを書き始めたら、アプリで使われているアプリケーションライブラリを使えるようになります。そうすると、クライアントサイドでもサーバーサイドでも同じようにライブラリを使えるのです。
我々の試みの一つとして、Kituraでは、標準コンポーネントを使っています。並列処理にGCD
を使っています。なぜこんなことをしているかというと、
- 正しい方法で、良いライブラリだから。
- パフォーマンスの問題が見つかり
GCD
で直すと、他のコミュニティでもそれに習うから。
誰かがGCD
の問題を見つけると、我々もその問題から知見を得ます。なのでコンポーネントやライブラリを提供してくれるユーザーが増えるにつれて、より多くの共有がうまれたり、品質がよくなり、全体の利益となります。
これを進めるにあたっては、この数か月、Appleや他のコミュニティの人々と一緒に取り組んできました。これまでに述べたネットワークやセキュリティ、HTTP解析のコンポーネントを標準規格にするためです。これらをSwiftのランタイムの一部にして、サーバAPIコンポーネントの集合体をつくります。
Swift 3.0 + Kitura (8:12)
サーバーで安定稼働するようになったSwift 3.0とKituraのようなWebフレームワークを組み合わせることによって、Swiftをサーバーで使うというのが、コードを共有できるという利点を持った、iOSアプリのバックエンドの開発における現実的な選択肢となっています。
それでは、同僚のRobertがこれをどのように行うのかをお見せします。
Blitter - SNSのバックエンドの一例 (8:47)
このサンプルでは、Swift 3を使ってSNSのバックエンドを作ってみます。スライドの28枚目でその流れを見ることができます。これは一般的なバックエンドアプリケーションで、HTTPサーバを立ち上げて、送られてきたリクエストを取るルータをつくります。いくつかの正規表現にマッチさせ、適切なハンドラにルーティングします。
また、何種類かのミドルウェアを追加します。例えばFacebookのクレデンシャルミドルウェアをビルドして追加することでリクエストやOAuthトークンの検証を利用できます。さらにユーザー名を返します。いくつかのハンドラがあり、ルータが正規表現に一致するものを見つけるとクロージャを実行します。Cassandraも性能の良いDBなので、使うことにしましょう。
今回お見せしたいのが、以下の項目のやり方です。
- プロジェクトや依存ライブラリの準備
- ルーティングの準備
- Facebook認証の追加
- DBモデルの準備
- リクエストの処理
みなさんのほとんどがまだSwift 3をコマンドラインで使ったことがないでしょうから、Xcode beta 8やSwiftのツールチェインをLinaxでインストールすると何が起こるのか、その一例をお見せしましょう。
プロジェクトの準備
ディレクトリを作ったら、Swiftパッケージを入れるためにSwift Package managerを使いましょう。これは決まり文句みたいなものですが、これで準備が整います。
$ ~/> mkdir Blitter && cd Blitter
$ ~/Blitter/> swift package init
Creating library package: Blitter
Creating Package.swift
Creating .gitignore
Creating Sources/
Creating Sources/Blitter.swift
Creating Tests/
Creating Tests/LinuxMain.swift
Creating Tests/BlitterTests/
Creating Tests/BlitterTests/BlitterTests.swift
2つのソースコードが生成されます。ひとつはHallo Worldアプリで、もうひとつがテストケースを追加できるものです。ほとんどの場合、Xcodeを使って開発したいでしょうから、Swift Package Managerを使ってXcodeのプロジェクトに変換します。
$ ~/Blitter/> swift package generate-xcodeproj
$ ~/Blitter/> open Blitter.xcodeproj
私はバックエンドを開発するときにXcodeを使うのが大好きです。なぜなら、ブレークポイントでステップできて、型チェックがあり、コードカバレッジやプロファイルがあって、それらすべてがバックエンドSwiftで使えるからです。
依存ライブラリの追加
Package.swift
には使いたい依存ライブラリを記載します。s
// Package.swift
import PackageDescription
let package = Package(
name: "TwitterClone",
dependencies: [
.Package(url: "https://github.com/IBM-Swift/Kassandra", majorVersion: 0, minor: 1)
.Package(url: "https://github.com/IBM-Swift/Kitura.git", majorVersion: 0, minor: 28)
.Package(url: "https://github.com/IBM-Swift/SwiftyJSON.git", majorVersion: 0, minor: 14)
]
)
もうCarthageやCocoaPodsを使うことはありません。GitHubのURLやメジャーバージョンやマイナーバージョンを指定すると、Swift Package Managerが自動でダウンロードしてくれます。
ルーティングの準備 (11:44)
次に、ルーティングの準備です。
router.get("/") { request, response, next throws in
// Get my Feed here
}
router.get("/:user") { request, response, next throws in
// Get user bleets here
let user = request.parameters["user"]
}
router.post("/") { request, response, next throws in
// Add a Bleet here
}
Express.jsやSinatraを使ったことがあれば、似たような構文ですが、アップロードリクエストをGETやPOST、PUT、DELETEに登録できます。リクエスト/レスポンスを取得して、次のミドルウェアに引き渡す関数を呼び出せます。例えば、URLパラメータを渡したければ、/:user
を引数にし、ユーザを取得します。
いつも私は、このロジックをコントローラに入れてカプセル化するのでBitterControllerクラスを追加します。
public class BlitterController {
let kassandra = Kassandra()
public let router = Router()
public init() {
router.get("/", handler: getMyFeed)
router.get("/:user", handler: getUserFeed)
router.post("/", handler: bleet)
router.put("/:user", handler: followAuthor)
}
}
DBのドライバ、ルータ、レジスタを作りました。これらルータをコンストラクタの中にいれました。
Facebook認証の追加 (12:35)
Facebook認証を追加しましょう。Credentials
モジュールがありますね。
import Credentials
import CredentialsFacebook
let credentials = Credentials()
credentials.register(CredentialsFacebook())
Credentials()
をつくって先に進んで、Facebookのクレデンシャルをミドルウェアに入れます。GitHubや他に認証が必要なソースコードにも対応しています。
トラッフィクを監視するミドルウェアを入れたいなら、クレデンシャルのミドルウェアをそのルートに登録します。例えば、新しいBleetを投稿したいとき、ルートパスにPOSTします。それからミドルウェアがユーザ名をチェックし、ユーザが使われます。
router.post("/", middleware: credentials)
router.post("/") { request, response, next in}
/// ...
let profile = request.userProfile
let userId = profile.id // "robert dickerson"
let userName = profile.displayName // "Robert F. Dickerson"
/// ...
}
モデルとDBの準備 (13:22)
Bleet
はこんなプロパティを持っています。これは毎回書いているので、皆さんはこれをコピーしてください。
struct Bleet {
var id : UUID?
let author : String
let subscriber : String
let message : String
let postDate : Date
}
これが構成しようとしているデータです。message
とpostDate
です。ポイントはここでFoundaton
を使っていることです。これはいいですよ。お気づきでしょうが、NSはどこにもつける必要はありません。Swift 3でリネームされましたから。DateとUUIDを使うのはLinuxと同じでいいですね。
バックエンドのコードを書いてくのに、JSONで構造を書かなければいけないのは億劫ですよね。なのでその構造を文字キーと値のペアに変換する概念をもっておくのが良いでしょう。このようなプロトコルを定義すると、変換できます。
typealias StringValuePair = [String : Any]
protocol StringValuePairConvertible {
var stringValuePairs: StringValuePair {get}
}
これはオブジェクトがStringに変換可能だったら、コレクションもStringと値のペアになったコレクションであるべきということです。
extension Array where Element : StringValuePairConvertible {
var stringValuePairs: [StringValuePair] {
return self.map { $0.stringValuePairs }
}
}
Bleet
にこのStringと値のペアの変換メソッドを定義することで、簡単にJSONに変換できます。
extension Bleet: StringValuePairConvertible {
var strongValuePairs: StringValuePair {
var result = StringValuePair()
result["id"] = "\(self.id!)"
result["author"] = self.author
result["subscriber"] = self.subscriber
result["message"] = self.message
result["postdate"] = "\(self.postDate)"
return result
}
}
どうやって既存の構造でそれを維持したり、DBに保存できるようにしたらよいでしょうか。我々が開発したCassandaraのドライバを使うことによって、Bleet
にModel
としての機能を拡張することができます。そうすると、Bleetが格納されているテーブル名を指定できますし、自由に永続化できる機能を持つことになります。
import Kassandra
extension Bleet : Model {
static let tableName = "bleet"
// other mapping goes here
}
例えば、新しいBleet
をつくるとき、DBに接続してオブジェクトに対して.save()
を呼ぶことができ、簡単にデータの永続化を行えます。
let bleet = Bleet( id : UUID(),
author : "Robert",
subscriber : "Chris",
message : "I love Swift!",
postDate : Date()
)
try kassandra.connect(with: "blitter") { _ in bleet.save()
}
subscriberの配列があって、それぞれに対して新たにbleetを作り、それをDBに保存するとしましょう。subscriberの配列をbleetのコンストラクタに変換することができ、新たな配列が得られます。それを保存できます。
// Get the subscribers ["Chris", "Ashley", "Emily"]
let newbleets = [Bleet] = subscribers.map {
return Bleet( id : UUID(),
author : userID,
subscriber : $0,
message : message,
postDate : Date())
}
newbleets.forEach { $0.save() { _ in } }
map関数の最後でforEach
でチェインすることでコードを1行にもできますね。
非同期のエラー処理では、このように値かエラーのいずれかを返し、相互排他されているようなAPIをよく見かけますよね。
func doSomething(oncompletion: (Stuff?, Error?) -> Void) {
}
この相互排他を確実に行うために、こんな風にすることをおすすめします。
enum Result<T> {
case success(T)
case error(Error)
var value: T? {
switch self {
case .success (let value): return value
case .error: return nil
}
}
// Do same for error
}
ジェネリックなResultの列挙型で、successに値が、errorにエラーが入ります。値を返したい時は、エラーがあればnilを返し、エラーがなければ値を返します。
Bleetの配列を取得しましょう。DBに接続し、すべてのVleetをフェッチします。エラーがあればonComplete
を呼びます。結果が返せるなら、bleetを作り、success
ハンドラを呼びます。
func getBleets(oncomplete: Result<[Bleet]>) -> Void) {
try kassandra.connect(with: "blitter") { _ in
Bleet.fetch() { bleets, error in
if let error = error {
oncomplete( .error(error))
}
let result = bleets.flatMap() { Bleet.init(withValuePair:)}
oncomplete( .success(result) )
}
}
}
条件文も処理できます。bleetしたユーザ名にマッチするbleetを返したければ、演算子をオーバーライドできるというSwiftの特性を利用して、==
をクエリに適応できる条件文とみなします。
Bleet.fetch(predicate: "author" == user,
limit: 50) { bleets. error in
///
}
リクエストの処理 (17:53)
最後に、リクエストの処理の仕方をお見せしましょう。
getBleets { result in
guard let bleets = result.value else {
response.status(.badRequest).send()
response.next()
return
}
response.status(.OK)
.send(json: JSON(bleets.stringValuePairs))
response.next()
}
}
Bleetを返したら、resultがありますね。そのリクエストが成功かどうかをチェックしました。successでなければ、先に進みレス���ンスにBad Requestをつけます。一般的に500エラーと呼ばれるものです。成功なら、ステータスコードは200ですね。これが.status()
を使ってステータスコードをセットする方法です。
これでJSONをシリアライズする準備ができ、send()
を実行することができます。すでにJSONにはMIMEタイプがセットされており、String-ValueペアをそのDictionaryで返します。SwiftyJSONを使うと、そのDictionaryはJSONに変換されます。Stringとしてシリアライズされています。
多くの場合、Webサーバにメッセージを送ったら、JSON形式になってくれて、そこから情報を抽出できるようにしたいでしょう。
この場合、ルータリクエストを拡張して、ドキュメントにBodyがあるか、BodyにJSONがあるかをチェックし、JSONを返せるようにします。
extension RouterRequest {
var json: Result>JSON> {
guard let body = self.body else {
return .error(requestError("No body in the message"))
}
guard case let .json(json) = body else {
return .error(requestError("Body was not formed as JSON"))
}
return json
}
}
Bleetを保存する用意ができたら、先に進んでJSONからmessageを取得し、保存します。
let userID = authenticate(request: request)
let jsonResult = request.json
guard let json = jsonResult.result else {
response.status(.badRequest)
next()
return
}
let message = json["message"].stringValue
// Save the Bleets with author matching userID
まとめ (19:30)
Kituraを使った他のサンプルとしては、Todoリストアプリがあります。これはMongoDBやRedisなど複数のDBを使っていて、これに対応したiOSアプリも作っています。
もっと拡張してみると、AngularJSでも使えます。BluePicというWebサンプルで、KituraやCourchDB、Object Storage、Watsonを使っています。iOSアプリとAngular JSのフロントエンドがあります。
ありがとうございました。Xcode 8 Betaや最新のツールチェインをダウンロードして、Kituraで遊んでみてください。
参考資料
- IBM-Swift/Kitura
- IBM-Swift/Blitter
- IBM-Swift/BluePic
- Alioth - Benchmarks Game
- Alioth - Benchmarks Game, Spectral Norm Test
- Swagger
- SwiftyJSON
About the content
2016年9月のtry! Swift NYCの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。