Tryswift chris bailey robert dickerson facebook

Swift、Java、Node.js、Ruby、どれを使いますか?サーバーサイドSwiftの優れた点について

サーバーサイドプログラミング言語としてのSwiftへの追加機能はクライアントとサーバーを同じ言語で使えるようにしただけでなく、APIやコードの再利用を可能としました。

このtry!Swiftの講演では、アプリ開発におけるクライアントとサーバーの新しいインタラクションモデルを紹介し、Swiftで素早くクライアント、サーバーを伴うアプリを作る方法をお見せします。


イントロダクション (0:00)

Chris Baileyです。Robert Dickersonと一緒に[email protected]のエンジニアチームで働いています。この発表では、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

}

これが構成しようとしているデータです。messagepostDateです。ポイントはここで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のドライバを使うことによって、BleetModelとしての機能を拡張することができます。そうすると、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で遊んでみてください。

参考資料

About the content

2016年9月のtry! Swift NYCの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。

Chris Bailey

Chris Bailey is a developer and technical leader in the Runtime Technologies team at IBM. Chris has spent over 15 years working on runtimes, working with the open source communities for Java, Node.js and most recently, Swift. He has contributed to the Swift Language, Foundation and Dispatch projects, and is currently working on making more “server” focused APIs available to the community.

Robert Dickerson

Robert F. Dickerson is a lead software engineer in [email protected] at Austin, TX. He is focused on enriching the “Swift on the server” community by being a developer for the web framework “Kitura”, Swift server libraries and SDKs, and also sample applications. He has taught computer science courses at the University of Texas (Austin) and the College of William and Mary and has written numerous research papers about mobile computing, Internet of Things, and virtual reality. When not busy writing code, he is busy swing dancing at nights.

4 design patterns for a RESTless mobile integration »

close