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

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

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

f:id:ngsw_taro:20200410164517p:plain

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

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

Philosophy(理念)

Ubieが掲げるビジョンは「Hello, healthy world」です。 健康が当たり前に存在する世界を実現したいと考えています。 そして、それを実現するため「テクノロジーで人々を適切な医療に案内する」をミッションとしています。 患者はもとより医療従事者をも対象に、適切な医療にアクセスできるようにお手伝いします。

入社当時

創業当時からUbieの理念は一貫していますが、今のビジョンやミッションとはその表現が若干異なりました。 2019年2月に開催された合宿に合わせてビジョンとミッションが決定されました。

f:id:ngsw_taro:20200409170756p:plain
合宿では行動指針となるバリューを全員で決めた

入社当初より世界を変える大仕事という認識はありましたが、ビジョンとミッションとして体裁を整えることで、改めてシャキッとした気持ちになりました。
また僕自身、ライフステージが変化したり年齢を重ねたりする度に健康に対する意識が高まって来たこともあり、会社のビジョンの実現に自分事として取り組めています。

Profession(仕事の内容)

Ubieには、いくつかのプロダクト開発チームが存在しており、主となるのは「AI問診Ubie」という医療機関向けのサービスを開発するチームです。 タブレットで問診を行い、患者の回答を電子カルテへ転記しやすい形に翻訳・整形することで医療現場の業務効率化に寄与します。

入社当時

入社直後に僕に与えられたミッションは「AI問診Ubie」のうち、医師が使う画面への新規機能開発とそれに伴うUIデザインのリニューアルでした。TypeScriptやReactに不慣れではありましたが、ワンパンでした💪 当時はまだ社外からお手伝いとして参画していた、医師画面の新UIの担当デザイナーはたけ氏は、僕の開発スピードに合わせるためにUbie入社を決めたと本人談。

f:id:ngsw_taro:20200409170328p:plain
初めての引越しを終えた直後のオフィスで開発していた

ワクワクしていました!まさに立ち上げ直後のスタートアップで、何も整っていませんでしたが、むしろそういう状況に僕は燃えていました。そして、文字通り医療に貢献している、そんな感覚がはっきりとありました。

2019年4月〜12月

もともと「なんちゃってスクラム」でプロジェクトを回しており、一応は機能しているように見えていたのですが、人数が増えるにつれ、生産性が落ちていることに気付きました。 そこで、ちゃんとしたスクラムをやろうということになったのが2019年の4月頃だったと思います。

「AI問診Ubie」は患者側チーム(タブレット問診)と医師側チーム(PC医師画面)に明確に分かれ、それぞれにプロダクトオーナーやスクラムマスターを配置しました。 僕は医師側チームで、カルテ記載のための補助やそれに付随する機能を担当しました。 このチームの構成は、医師兼プロダクトオーナー、デザイナー兼スクラムマスター、エンジニア4名です。 POの医師やデザイナー、あるいはカスタマーサクセスが精力的に医療現場でのヒアリングを行い、ニーズを拾い上げ仕様案へと落とし込みます。 それを毎日のプロダクトバックログ・リファインメントの時間にメンバーで議論を交わし洗練させ、週次のスプリント・プランニングで実装の計画を立てます。

f:id:ngsw_taro:20200409172220p:plain
医師チーム(突発的に開催された社外ミーティング)

医師側チームは大きな夢に向かって全速前進して好調でした。しかし、後半は失速。手広くやりすぎた、悪く言えばリソースが分散、中途半端。また「AI問診Ubie」というプロダクトを医師側という一側面からしか見ていませんでした。そして…

2020年1月〜3月

「AI問診Ubie」という同じプロダクトを扱うチームが患者側と医師側で分かれることによって、施策の優先順位の見極め精度が曇るという課題が顕在化してきました。 そこでLeSS(Large Scale Scrum)という手法の採用に踏み切りました。 患者側と医師側をガッチャンコさせ「AI問診Ubie」でONE TEAMだと。プロダクトオーナーも、プロダクトバックログもひとつ。 LeSSにより「AI問診Ubie」は全員同じ目線で動けるようになったと思います。

現実を直視し、ときには泥臭く開発を進めました。 プログラマーの美学とでも言うべき感覚にこだわりを持っていた僕ですが、必要ならば負債を積むことも厭わない、そういうバランス感覚が養われたと思います。

2020年4月(今)

「AI問診Ubie」は比較的規模の大きい病院に受け入れられ、全国の病院で導入が進んでいます。 この春から、街の診療所のような規模の小さい施設にもUbieを使っていただくため、僕は「AI問診Ubie」チームからクリニック特化チームへと移籍しました。 大病院とクリニックとでは求められる機能が異なるため、また我々の解像度が低いため、別チームとして切り出し、仮説検証を高速に回していくことが狙いです。

めっちゃアツイ!未踏の地に足跡を残すのが好きなんだ!

People(人)

人が文化を創り、文化が人を育てる(名言風)

個人的には4Pのうち一番重要なPだと思います。

入社当時と今

6人目のメンバーとして入社。 月並みな表現ですが、みんな優秀で個性的で尊敬しかありませんでした。 もっとシンプルに、好きと、そう言い切れるメンバーです。

f:id:ngsw_taro:20200410141231p:plain
エンジニア採用LPのために撮った写真

今、社員は40人程度で、そのうちエンジニア(ソフトウェアエンジニア、データサイエンティスト、SRE等)は20人くらい在籍しています。 いろんな人がいます。 やはりみんな優秀で、僕も負けないように日々研鑚しています。 どのような人がいるか、ここに入社エントリやUbieでの働き方についてそれぞれが書いた記事がまとめられているので、見てみてください。

組織運営

Ubieという組織を表現するキーワードとして「オープン」「フラット」「透明」「率直」というものが挙げられます。共同代表の二人を除けば、その他のメンバー間に上下関係や階層構造は一切なく、それゆえに情報や知識の流れを阻害するものはありません。あらゆる情報にアクセス可能です。招待されていない会議にも興味さえあれば参加可能です。意見や提案も自由です。

内緒話もなければ根回し、社内政治もありません。 これが心理的安全性を支え、チャレンジ精神を育んでいるのかもしれません。

しかし、これがうまく機能しているのは人数の少なさゆえかもしれません。人が増え、情報が増えると、情報へアクセスするコストも増え、スピード感が損なわれるおそれがあります。 そういう状況に危機感を覚えたメンバーが中心となり「透明性プロジェクト」が立ち上がりました。そもそもどういう状態が透明であるのかという定義から、透明である状態を貫くためのhowの検討までを担当しています。 その一環としてホラクラシー組織の実装・運用も視野に入れています。

OKR

Ubieでは目標管理の手法としてOKRを採用しています。 今では珍しくないものですが、取り組みの度合いとしては本気です。 全社員にMeasure What Mattersが配られ、これを読んでOKRに臨むぞと、去年の1月からスタートしました。 それでも最初のうちは、なかなか運用が難しく、試行錯誤を繰り返して、徐々に慣れていきました。

f:id:ngsw_taro:20200410145050p:plain
四半期に一度行われるOKRオフサイトミーティング

業務外でも遊ぶよ

そりゃあ仲良くなれば一緒に遊ぶよね、人間だもの。

f:id:ngsw_taro:20200410145852p:plainf:id:ngsw_taro:20191001213854j:plainf:id:ngsw_taro:20190928202200j:plainf:id:ngsw_taro:20200410150227j:plain
キャンプ、HADO、釣りBBQ、スノボ

体を動かして汗をかいた後に飲むビールの美味さよ

Privilege(待遇)

正直に、給料は悪くないです。というか納得している。でないと転職していません。 加えて、ストックオプションをもらっています。 求人票には年俸600万〜1200万円と提示されています。

コアタイムなしのフルフレックスでリモートワークもOKです。 出社するにしても自宅で仕事するにしても、満員電車を避けられるのは嬉しいです。 もちろん、オフライン推奨ミーティングや出張のときなんかはこの限りではありませんが。

オフィスのデスクに、パーテーションはありません。コミュニケーションを取りやすくするためでしょう。 僕はでっかいディスプレイ2枚をもらって作業しています。一枚はターミナルを表示して、もう一枚はSlackとTwitterを表示しています。そしてノートPCにはIntelliJ IDEAを表示してます。

椅子はエルゴヒューマンのこれだと思います。

オフィスには無料のドリンクとフードが盛り沢山です。 フードはお菓子だけではなく、OFFICE DE YASAIによるサラダや果物や豆腐のヘルシーそうなやつもあります。

f:id:ngsw_taro:20200410155617p:plain
食糧には困らない

卓球台もあります。 単なる飾りで終わり、ではなく、ちゃんと使われています。 卓球の取り組みについて取材を受けました。 また、Gunosyさんと企業対抗団体戦をやったりもしました。 ここ最近は、新型コロナウイルスの影響でリモートワークにシフトし、出社していないので卓球が恋しいです。

f:id:ngsw_taro:20200410154756p:plain
ドライブを打つときの顔

オフィス奥には芝生スペースがあるのですが、ここでミーティングをよく行います。 大きいディスプレイにはNintendo Switchがつながってるのでゲームやカラオケを楽しむことができます。 芝生側にある冷蔵庫にはビールが入っており、またその隣にはワインセラーがあります。 ソファに座って、あるいはバーカウンターでお酒を楽しむことができます。

f:id:ngsw_taro:20200410160258p:plain
みんなの憩いの場 芝生スペース

よさそう

おわりに

以上、Ubieにおける「組織のエンゲージメントの4P」でした。

僕は「テクノロジーで人々を適切な医療に案内する」をミッションに、日々コードを書いています。 「AI問診Ubie」の医師画面を担当していましたが、今月からクリニック向けプロジェクトが始まり、そちらに注力します。 開業されている先生方にお話を伺い、仮説に肉付けをし、持ち前の爆速開発力でじゃんじゃんリリースし、価値を証明していきます。

思い切り仕事ができるのも、周りの人々や組織文化、働きやすさのおかげだと思います。

最後に、新型コロナウイルス感染症が世界中で猛威を振い、多方面に影響が広がっていることに関して一個人として心苦しく思っております。 この状況にUbieだからこそ貢献できることもあると考え、社内でも色々と議論を進めております。一刻も早い事態の収束を望むばかりです。


Ubieについてもっと詳しく知りたい方は👇をご覧ください。

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を叩くというよりもページ遷移 + レンダリングなアプリケーションなので、今回の例とはそこらへんが異なりますが本質的な問題ではないでしょう。

Kotlin Fest 2019 開催のお知らせ #kotlinfest #jkug

f:id:ngsw_taro:20190512081350j:plain

Kotlin技術カンファレンス、今年も開催!

日本Kotlinユーザグループ(JKUG)主催のKotlin技術カンファレンス「Kotlin Fest 2019」を開催します。 前回のKotlin Fest 2018のご好評を受け、今回は規模を大きくします!

日程
2019年8月24日 土曜日
会場
東京コンファレンスセンター品川
主催
日本Kotlinユーザグループ
コンテンツ
3トラック 15セッション、企業展示ブース(予定)
参加人数
500名程度

チケットは7月上旬に販売開始予定です。 詳細は下記サイトをご覧ください。

kotlin.connpass.com

Kotlin Fest は一貫して「Kotlinを愛でる」というテーマで企画・運営しています。 ご来場のみなさまに、Kotlinに関する知見の共有と、ほかのKotlinファンとの交流を楽しんでいただければ幸いです。

今回はご講演者さまを公募するという試みを行います。 ぜひ、お持ちのKotlinの知識や経験を、Kotlin Festでのご登壇という形で共有いただければと思います! 下記リンクのフォームから、2019年6月23日 23:59までにお申し込みください。

お問い合わせはJKUG窓口 japan-kotlin-user-group@googlegroups.com までお願いいたします。

Have a nice Kotlin!