算譜王におれはなる!!!!

偏りはあると思うけど情報技術全般についてマイペースに書くよ。

入社して2年が経ったUbieについて理念・仕事内容・人・待遇の観点から語る

f:id:ngsw_taro:20200410164517p:plain

なんと早いもので!AI医療スタートアップのUbie(ユビー)株式会社に入社して、2年が経ちました。あっと言う間ですね〜。 Ubieは来月に4期目を迎える、まだまだ若いスタートアップ企業ですので、そういう意味で2年という期間は長く、今と入社当時とでは状況がまるで違います。

このエントリでは、僕にとってのUbieを「THE TEAM」の中で語られている組織のエンゲージメントの4Pという観点で、入社当時と現在を比較しながら考えていきたいと思います。

続きを読む

KtorのカスタムFeatureをつくる

Ktorの面白い特徴のひとつとしては「Webアプリケーションに必要な機能を"install"していくスタイル」を採っていることだと思います。 その典型的な例はルーティングです。よく見るルーティング設定のコードは

routing {
    get("/") {
        call.respondText("Hello, world!")
    }
}

のように routing を使うものが多いと思いますが、これは

install(Routing) {
    get("/") {
        call.respondText("Hello, world!")
    }
}

と記述するのとだいたい同じです。

その他にも標準で提供されているFeatureはたくさんあって、例えばContentNegotiationCompressionのようなものがあります。

install(ContentNegotiation) {
    jackson()
}
install(Compression) {
    gzip {
        priority = 1.0
    }
}
install(Routing) {
    get("/") {
        call.respondText("Hello, world!")
    }
}

今回は、このFeatureを自作してみます。 有用なものを作って配布したら、いろんな人にinstallして使ってもらえるかもしれませんね。

参考

とは言っても役に立ちそうなアイデアがパッと浮かばないので、単純なロギングFeatureを実装したいと思います。 ポイントは

  • 大元となるFeatureクラスを定義する(今回はMyLoggingクラス)
  • そこに設定用クラスをネストする(名前は何でもいいけどConfigurationクラスとします)
  • ApplicationFeatureインタフェースを実装したcompanion objectを定義する

です。 必ずしもこれに従うことはないとは思いますが、上記参考URLの公式ドキュメントではこうなっていました。 ざっとこんな感じになります(importは割愛)。

class MyLogging(configuration: Configuration) {

    val decoration: String = configuration.decoration

    class Configuration {
        var decoration: String = "✨"
    }

    companion object : ApplicationFeature<ApplicationCallPipeline, Configuration, MyLogging> {
        override val key: AttributeKey<MyLogging> = AttributeKey("MyLogging")

        override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): MyLogging {
            val configuration = Configuration().apply(configure)
            val feature = MyLogging(configuration)
            /* TODO */
            return feature
        }
    }

    // あとで使う
    private fun log(message: String) {
        println("$decoration $message $decoration".trim())
    }
}

このコードで一旦はFeatureとしての体を成しています。が、現時点では何も仕事はしていません。 メソッドinstallの引数configureが、独自定義の設定用クラスConfigurationの拡張関数になっていることに注目してください。 このFeatureのユーザは、ラムダ式の中でConfigurationのプロパティにアクセスすることで設定を組み立てていきます。

install(MyLogging) {
    decoration = "🌟"
}
routing {
    get("/") {
        call.respondText("Hello, world!")
    }
}

さて、まだ何の面白いこともしていないMyLoggingですが、リクエストの前後でログを出力したいと思います。 2つ前のコードのメソッドinstallに再び注目してください。第1引数pipelineを扱います。 PipelineもKtorの面白い特徴のひとつなんですが、ドキュメントコメントの言葉を借りればこれは「非同期の拡張可能な計算のための実行パイプラインを表す」ものです。 Pipelineにはフェーズがあって、そのフェーズをインターセプトすることができます。

override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): MyLogging {
    val configuration = Configuration().apply(configure)
    val feature = MyLogging(configuration)

    pipeline.intercept(ApplicationCallPipeline.Features) {
        feature.log("${call.request.path()}へのリクエストが始まるよ!")
        proceed()
        feature.log("${call.request.path()}へのリクエストが終わったよ!")
    }
    return feature
}

余談ですが、メインのApplicationCallPipelineには5つのフェーズがあって、今回は"Features"というフェーズをインターセプトしました。 本来であれば"Monitoring"でやるべきだったのかなと思いつつCallLoggingという標準のロギングFeatureのコードを読んでみたら、新たに"Logging"というフェーズを作り、"Monitoring"の前に挿入していました。そういうプレイングもあるのか…。

なお、今回つくってみたMyLoggingの使ってみた結果はこんな感じです。 CallLoggingも併用しています。

f:id:ngsw_taro:20191217183951p:plain

Ktorのルーティングとコントローラについて考える

アドベントカレンダーの季節なので、KtorでWebアプリケーションを作るときのことを考えたメモを残します。

いわゆるコントローラ、リクエストを受けてごにょごにょしてレスポンスを返すところを担うやつ。 そしてルーティング、パスとコントローラを紐づけるやつ。 Ktorでこの両者をどう扱うかを考えてみました。

HelloWorldの延長線上でズラズラとコードを並べるとこんな感じになります(完全なコードではありません)。 コントローラの役割とルーティングが同居する世界です。 これを基本形として、いくつかの工夫を見ていきたいと思います。

val userRepository = MyUserRepository()
routing {
    route("users") {
        get {
            val users = userRepository.findUsers()
            call.respond(users)
        }
        post {
            ...
        }
        route("{id}") {
            get {
                val id = call.parameters["id"]?.toLongOrNull()
                val user = id?.let { userRepository.findUser(it) }
                if (user == null) {
                    call.response.status(HttpStatusCode.NotFound)
                    return@get
                }
                call.respond(user)
            }
            delete {
                ...
            }
        }
    }
}

公式サンプルではどうやっているか

Ktorはサンプルプログラムを豊富に提供していて、その中にはそこそこ本格的なアプリケーションも含まれています。 例えばTwitterみたいなKweetというサンプルです。 このサンプルで、今回注目したいのはLocationsというfeatureと Route の拡張関数を使っているところです。 Locationsはパスを表現するクラスをいい感じに扱えるようにしてくれます。 詳細は公式ドキュメントに譲りますが、Locationsがどのように使われているのかざっくり説明します。 単純のためKweetのコードを引用せず、上記のようなUser APIについて考えます*1

まず @Location にパスを指定して、クラスにくっつけます。

@Location("users")
class Users {
    @Location("{id}")
    class Member(val id: Long)
}

@Location がついたクラスを入れ子にすることで user/{id} のようなパスを表現することが可能です。 ルーティングをこのように書き直すことができます。

routing {
    get<Users> {
        val users = userRepository.findUsers()
        call.respond(users)
    }
    post<Users> {
        ...
    }
    get<Users.Member> {
        val user = userRepository.findUser(it.id)
        if (user == null) {
            call.response.status(HttpStatusCode.NotFound)
            return@get
        }
        call.respond(user)
    }
    delete<Users.Member> {
        ...
    }
}

users/{id} で指定したパスパラメータが Users.Member オブジェクトのプロパティ id にセットされるので、これを簡単にアクセスすることができます。

Kweetではさらに、パスとHTTPメソッドの組みそれぞれに対して Route の拡張関数を別ファイルに定義しています。

fun Route.showUser(userRepository: UserRepository) {
    get<Users.Member> {
        val user = userRepository.findUser(it.id)
        if (user == null) {
            call.response.status(HttpStatusCode.NotFound)
            return@get
        }
        call.respond(user)
    }
}

このような拡張関数を定義して、 routingラムダ式の中身をこのように書き換えます。

routing {
    listUsers(userRepository)
    createUser(userRepository)
    showUser(userRepository)
    deleteUser(userRepository)
}

ファイルを分割し、見通しはよくなった気がしますが、実質的にはコントローラの役割とルーティングが合わさっているように思えます。 拡張関数 showUser は、パスパラメータ id をより抽象的な概念として捉えることができますが、自分のパスは Users.Member クラスと対応していることを知っています。

コントローラクラスを導入する

個人的にこれを改善しうるのって、いつものコントローラクラスだと思っています。 リソース1種類につき、ひとつのコントローラクラス。 素直にコーディングすると、今回の場合はこのような感じになりそうです。

class UserController(private val userRepository: UserRepository) {

    suspend fun index(call: ApplicationCall) {
        val users = userRepository.findUsers()
        call.respond(users)
    }

    suspend fun create(call: ApplicationCall) {
        ...
    }

    suspend fun show(call: ApplicationCall, id: Long) {
        val user = userRepository.findUser(id)
        if (user == null) {
            call.response.status(HttpStatusCode.NotFound)
            return
        }
        call.respond(user)
    }

    suspend fun delete(call: ApplicationCall, id: Long) {
        ...
    }
}

そしてルーティング側はこうなります。 もはやLocationsはなくていいかもしれませんね。

val userRepository = DummyUserRepository()
val userController = UserController(userRepository)
routing {
    get<Users> { userController.index(call) }
    post<Users> { userController.create(call) }
    get<Users.Member> { userController.show(call, it.id) }
    delete<Users.Member> { userController.delete(call, it.id) }
}

という感じで、一旦の僕の中での結論としてはパスとHTTPメソッドを知らないコントローラを定義して、その各メソッドを routing の中でパスとHTTPメソッドに紐づける方式がよいのではなかろうかと。 で、Locationsも要らない、ややこしくしているだけ説。 UbieではKtorアプリケーションが爆誕しつつあるんですが、リリースされて本格的に運用が始まったらこの仮説は崩れるのだろうか。。 ベスプラ知りたい。

*1:KweetはSPAがREST APIを叩くというよりもページ遷移 + レンダリングなアプリケーションなので、今回の例とはそこらへんが異なりますが本質的な問題ではないでしょう。