Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

スキーマ駆動開発のススメ

こんにちは,For School事業部のid:atomiyamaです.

現在Studyplus for Schoolはサービスのフルリニューアルを行っています.
弊サービスはこれまでRailsでslimを使いViewを提供してきましたが,今後より良い体験をユーザーへ届けるためにリニューアルを行いサーバーサイドとクライアントサイドを分離しました.
リニューアルに向けて現在サーバーサイドはRailsでJSON APIサーバーの開発を行っており,その中で導入したスキーマ駆動開発の話をします.

TL;DR

  • 技術スタックはOpenAPI3.0, swaggerUI,committee
  • クライアントサイド開発者と連携してJSONスキーマを仮決定する.
  • サーバーサイドの開発者はRailsコントローラにそのJSONをべた書きした仮実装を行う.
  • クライアントサイド,サーバーサイドの開発者が互いにフィードバックしながら並行して実装を進める.

スキーマ駆動開発とは

スキーマ駆動開発とは,
「スキーマをはじめに定義し,スキーマを元にAPI開発者,利用者が同時に開発を進めていくこと」です.

スキーマ駆動開発を導入するモチベーション

今回スキーマ駆動開発を導入した背景は以下の2つの問題により思ったようなスケジュールで開発が進行してしなかったことです.

  • サーバーサイドの実装が完了するまでクライアントサイド開発者が実装に着手できない.
  • 一度実装したAPIにクライアントサイドから変更の依頼が入り手戻りが頻発する.

今回のフルリニューアルのような長期にわたるプロジェクトでは特に既に開発を終えたタスクに対してな手戻りが頻発することは当初のスケジュール通りにプロジェクトを進めていく上で大きな問題になります.
またクライアントサイドにおいてはAPIで提供されるデータの仕様が不明確なままでは開発を進めることができませんからサーバーサイドの進捗から強く影響を受けてしまいます.

そこでサーバーサイド,クライアントサイドが並行して独立して開発できるようにスキーマ駆動開発を導入しました.

スキーマ駆動開発

私達のチームではスキーマ駆動開発を進める上で以下のようなステップにそって開発を進行しています.

  1. APIのスキーマを利用者の合意のもと決定する
  2. APIスタブサーバを提供する
  3. 本実装を行う

APIドキュメンテーション

APIのスキーマを決定しドキュメント化するために弊社ではOpenAPI3.0を使っています. OpenAPI3.0に従ってyamlで定義を書くんですがサービスがある程度の規模になってくると一つのyamlファイルにすべてのエンドポイントの仕様を書くのは辛くなってきます😭
そこで以下の用にエンドポイントURIのパスと対応するレベルでファイルを分割し,Rakeタスクでファイルを結合してswaggerUIが読み込み可能な形に吐き出しています.

├── app
├── bin
├── config
├── db
├── dockerfiles
├── lib
├── log
├── public
├── spec
├── swagger
│   └── v1
│       ├── main.yaml
│       ├── paths.yaml
│       ├── components
│       │   └─ user.yml
│       └── paths
│           └─ users.yml
├── tmp
└── vendor
# swagger/main.yml
openapi: 3.0.0
info:
  title: SampleAPIApp
  version: 0.0.1
servers:
  - url: https://example.com/v1
paths:
  $ref: ./paths.yaml
components:
  schemas:
    $ref: ./components/schemas.yaml

# swagger/paths.yaml
/v1/users:
  $ref: ./paths/users.yaml

# swagger/path/users.yaml
get:
  summary: ユーザー一覧
  tags:
    - ユーザー
  responses:
    200:
      description: OK
      content:
        application/json:
          schema:
            type: object
            required:
              - users
            properties:
              users:
                type: array
                items:
                  type: object
                  required:
                    - name
                    - age
                    - email
                  properties:
                    name:
                      type: string
                      description: 名前
                      example: スタプラタロウ
                    age:
                      type: integer
                      description: 年齢
                      example: 20
                    email:
                      type: string
                      description: メールアドレス
                      example: sutapura.tarou@example.com

APIスタブサーバの提供

前項でスキーマが決定しswaggerUIリーダブルな形に変換できたのでswaggerUIの公式イメージを立ち上げ出力したファイルを読み込むことで下のようにAPIのスキーマが閲覧可能になりました.
f:id:atomiyama:20190612170745p:plain ここから実際にRailsの実装を行っていくのですが,いきなり本実装をはじめてしまうと完了するまでクライアント開発者は開発に着手できなくなってしまうので最低限の実装でAPIスタブを実装します.

実際にはswaggerUIのResponsesのExample valueにあるオブジェクトをコピーして,Railsのコントローラにそのまま貼り付けます.

# app/controllers/v1/users_controller.rb
class V1::UsersController < ApplicationController
  def index
    return json: {
      "users": [
        {
          "name": "スタプラタロウ",
          "age": 20,
          "email": "sutapura.tarou@example.com"
        }
      ]
    }
  end
end

テストも最低限書いておきます.テストではcommitteeを使っています.
committeeを使ってテストをすることでOpenAPI3.0に則って定義したスキーマと差分の無いレスポンスが返却されることをテストすることができます.
committeeの導入についてはこちらを確認してください.

require 'rails_helper'

RSpec.describe V1::UsersController, type: :request do
  describe "GET /v1/users" do
    subject { get "/v1/users" }

    # 最低限のテスト
    it "200" do
      subject
      expect(response).to have_http_status(200)
      assert_schema_conform
    end
  end
end

ここまでをやったら一度PullRequestを作成してデプロイしてしまいます.
これらの作業は2,3時間もあれば終わってしまう作業ですし,ここまで完了すればサーバーから仮のデータが提供されるようになっているためクライアントサイドの開発者も開発を進めることができます.

本実装

残すはクライアント,サーバーともに前項までにやった仮の実装を本実装していくだけです.
ひたすらにスキーマ通りにデータを返せるようにコーディングします.

まとめ

スキーマ駆動開発を導入することにより早い段階で決定されたAPIスキーマ(APIスタブも含む)を元に,クライアントサイド,サーバーサイド互いが自身の作業に専念できるようになりました.
個人的にいいなと思った点は,最初のスキーマを決定する際にクライアントサイド開発者のフィードバックを受けるので互いの仕様への理解をすり合わせる空気感が自然とできることです.
またほぼ同時に本実装が進むため,もしどちらかの実装の都合によりスキーマに仕様変更が入っても完成前なので大きな手戻りにはなりにくいためスケジュール遅延リスクも最小限に抑えることができます.

是非これからAPI開発に取り組む方はスキーマ駆動開発取り入れてみてはどうでしょう.

Android Architecture Componentsに感動した話

初めまして、Studyplus開発部のAndroidエンジニアの隅山です。今年5月からスタディプラスでAndroidアプリ開発を行っています。

本格的にAndroid開発を始めてまだ日が浅いので、初中級エンジニア目線で学んだことをまとめていきたいと思います。

背景

初めてAndroid開発を行った時感じたことが「Androidの画面表示関連の仕様の複雑さ」でした。

以前開発してた時もAndroidのライフサイクルや画面回転あたりで非常に苦しめられていました。しかし今回、Android Architecture Components(AAC)を学ぶ+導入することで、ライフサイクルや画面回転問題が簡単に解消されたのでAAC実装例を紹介します。

やったこと

今回、ユーザが入力する文字列に対応した検索結果を、リアルタイムでリスト表示する機能を実装しました。

実装としては画面にEditTextとListViewを用意し、EditTextの文字の変更通知に合わせてListViewのアイテムを更新するという仕組みとなります。AACを使わない場合と使う場合で大きく変わった点が「EditTextの文字変更通知」部分なのでそこに着目していきます。

AAC使わない場合

まずはAACを使わないやり方、TextWatcherで文字変更通知を受ける場合です。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val itemList = listOf("くろ", "あか", "あお", "しろ")
        val editText = findViewById<EditText>(R.id.edit_text)
        val listView = findViewById<ListView>(R.id.list_view)
        val arrayAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, itemList)
        listView.adapter = arrayAdapter

        editText.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {
                // 文字変更通知を受けて、ListViewの中身更新
                val list = if (s.isNullOrEmpty()) itemList else itemList.filter { it.contains(s) }
                arrayAdapter.clear()
                arrayAdapter.addAll(list)
            }
            override fun beforeTextChanged(s: CharSequence?, s: Int, c: Int, a: Int) {}
            override fun onTextChanged(s: CharSequence?, s: Int, b: Int, c: Int) {}
        })
    }
}

自分が感じたこの実装のメリット、デメリットは下記となります。

  • メリット
    • Androidの標準APIのみで書ける
  • デメリット
    • 画面回転考慮する必要がある
    • アクティビティの役割が大きすぎる
    • モダンな実装ではない

特に、画面回転問題を考慮する場合、onSaveInstanceStateなどでデータを保持する必要があります。 また、サーバーから検索結果を取得し表示するなどの機能拡張を行うと、アクティビティクラスのコード量も役割もどんどん増えてしまいます。

AAC使う場合

つぎにAACのViewModel+LiveDataで文字変更通知を受ける場合です。

class SampleViewModel : ViewModel() {
    private val itemList = listOf("くろ", "あか", "あお", "しろ")

    // serchTextは双方向BindingによりEditTextと接続
    val searchText = MutableLiveData<String>()
    val items: LiveData<List<String>> = Transformations.map(searchText) { text ->
        // ここで文字変更通知を受け、リストに表示するアイテム作成
        if (text.isNullOrEmpty()) itemList else itemList.filter { it.contains(text) }
    }
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val viewModel = ViewModelProviders.of(this).get(SampleViewModel::class.java)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1)

        binding.lifecycleOwner = this
        binding.viewModel = viewModel
        binding.listView.adapter = adapter

        viewModel.items.observe(this, Observer { items ->
            // リスト表示アイテムを受け、ListViewに表示
            adapter.clear()
            items?.let { adapter.addAll(it) }
        })
    }
}

自分が感じたこの実装のメリット、デメリットは下記となります。

  • メリット
    • ViewModelがViewModelProviderで管理されているため画面回転されても情報が保持される
    • SampleViewModelで文字変更通知を受け、リストに表示するアイテムを作成するため、アクティビティは流れてきたアイテムを表示する役割のみ
    • モダンな実装のため、Google公式の最新アップデートに追随しやすい
  • デメリット
    • AACに関して勉強する必要がある

文字変更通知もLiveDataを使うと簡単に受けることができ、リストに表示するアイテムを自由にViewModel側でカスタマイズすることができます。今回は簡単な文字列リストを表示するだけですが、実際は検索文字を用いてサーバアクセスしたため、SampleViewModelでKotlin Coroutinesなどを使って実装しました。

まとめ

Androidでつまづきやすい画面回転によって情報が落ちてしまう問題や、クラスの役割が膨大になる問題をAACを導入することで解決できたのは感動しました。

今回は簡単な例を示すためリストに文字列表示するだけでしたが、実際の利用シーンでは検索文字列をサーバに投げてレスポンス結果をリストでリアルタイム表示しています。この際、Kotlin Coroutinesによる非同期処理や入力待ちの遅延処理を行なっています。AACを使わない場合だとアクティビティが肥大化して運用できない形になっていたため、今後の運用面も考慮して開発することができたのはよかったと思います。

AndroidのことやAACのことは同じチームメンバーの若宮さんや中島さんに教わりつつ開発することができたため、大きくつまづくことはほとんどなく非常に助かりました。Androidの画面回転やライフサイクルなどで詰まっている方は是非Android Architecture Componentsを導入してみてください。

AndroidのCI環境を移行しました!

こんにちは、Androidチームの若宮(id:D_R_1009)です。

2019年5月よりAndroidチームのCI環境をBitriseからCircleCIに移行いたしました。 今回は移行の経緯や結果などをまとめたいと思います。

CI環境変更経緯

昨年の6月ごろからBitriseを利用していました。 それ以前はWorckerを使っていたようです。

tech.studyplus.co.jp

BitriseはGUIによって容易にステップの追加/削除が可能なため、自動化したい処理の管理が非常に簡単です。 ステップにはコミュニティ主導で作られているものもあり、機能追加やメンテナンスが活発に行われています。 弊社のアプリに関して言えば、build.gradleファイルの整理時にgradle-play-publisherをBitriseのステップに置き換えるなどの様々な試作を行うことができました。

これらのことから、Bitriseを利用することになんら不満はありませんでした。一方で弊社全体のCI環境を見ると、少し違和感のある状況になっていました。

チーム 環境
Android(Kotlin/Java) Bitrise
iOS(Swift/Obj-C) CircleCI
Server(Ruby) CircleCI

上記の通りCI環境がチームによって分断されているため、CI環境に対する投資とそのリターンの効率が悪くなっています。 また、CI環境構築や改善についての知見がそれぞれのチームに偏ってしまうため、CI構築のノウハウが育ちにくい状況になっていました。

これらの状況を踏まえ、AndroidチームもCircleCIを利用することを決めた次第です。

CI移行ステップ

Androidのビルドについて立ち止まって考えてみると、一般的なCIサービスで利用できないものは特にないことに気づきます。

そのため何らかの方針を決めてCIで自動化する処理を決める必要があると考えました。 今回の移行ではCIで実行したいことは何かを整理すること、CIでは何をシェルスクリプトで書くべき何かを整理すること、この2点を重視しています。

なおCIについて何を考えていくかについては、darumaさんのLTを参考に考えていきました。

おすすめです。

スタディプラスAndroidチームのCI方針

"Ruby"を活用していく点がポイントだと考え、fastlaneによるビルド環境構築を選択しました。

fastlane.tools

fastlaneはiOSアプリの様々な自動化においてデファクトスタンダードとなっているツールです。 Androidではあまりなじみはありませんが、様々な対応がなされています。

実際にステップ構築にあたり考えた方針は下記の通りです。

  • fastlaneで可能なことは、全てfastlaneで実行する
  • fastlaneで実行できないがGradleプラグインで実行できるものは、Gradleプラグインで実行する
  • fastlaneとGradleプラグインで実行できないことだけ、シェルで実行する

出来上がったconfigファイルを見返してみるとCrashlytics Beta用NoteにGit logを記載する箇所だけシェルによる実行となっており、この方針は満たせているかなと感じています。

BitriseからCircleCIに移行で苦労した点

CircleCI上に各リリース用のkeyを設定する以外には、特にありませんでした。 というのも、CircleCI 2.1のconfiguration機能を使うことで、CircleCIの各処理を小さくまとめて記載することができたためです。

たとえば、Dangerを走らせる場合には

commands:
  setup_bundle:
    steps:
      - restore_cache:
          key: v1-danger-cache-{{ checksum "Gemfile.lock" }}
      - run:
          name: Bundle install
          command: bundle check --path ./vendor/bundle || bundle install --jobs=4 --retry=3 --path ./vendor/bundle --clean
      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-danger-cache-{{ checksum "Gemfile.lock" }}
  run_danger:
    steps:
      - run:
          name: Run danger
          command: bundle exec fastlane android check_pull_request

とcommandsを定義します。これを

jobs:
  check_pull_request:
    executor:
      name: android_defaults
    steps:
      - checkout
      - setup_android
      - setup_bundle
      - run_danger

のようにjobとすることで、Bitriseと同じようにステップとして扱うことができます。

移行を始めた当初は書き慣れないYAMLファイルやRubyの環境構築(Bundlerとは、Gemfileとはなど)に手間取りました。 しかし学習が進むについれ、手元の環境で構文チェックをしながら構築することができ、生産性が加速度的に高まっていったように感じます。

CircleCI上にAndroidリリース用のファイルを設定する方法は、下記ブログが一番実用的だと感じました。

medium.com

弊社の状況に合わせて試行錯誤した結果、ほぼ同一の対応をすることになっていました。 一度このブログの手順に合わせて構築し、各環境に合わせてカスタマイズすると良いのではないかと感じています。

移行結果

移行した結果、実行時間の削減が見られました。 以下の2ファイルはキャッシュがない状態で、ほぼ同一の処理を行っています。

CircleCI Bitrise
f:id:D_R_1009:20190524162647p:plain
CircleCI stageビルド
f:id:D_R_1009:20190524162341p:plain
Bitirise stageビルド

2019年6月現在ではdebug/stage/releaseの全てのビルドをCircleCIに移行していますが、ほぼ同じか半分程度の実行時間になっています。 Bitriseではビルド待ちの問題が発生しなかった点を差し引く必要がありますが、概ね実行時間は短縮されたと言えます。

まとめ

BitriseとCircleCI、どちらも利便性に優れたCI環境です。 特にCIで実行するステップの管理に対して、アプローチは異なりますがどちらも簡潔だなと感じました。 簡潔な処理を適切に組み合わせることとなるので、だんだんとステップを編集するコストを軽く感じるようになりました。

一部キャッシュの扱いについて違いがあるため個別のCIについての学習は発生しますが、利用しながら改善していけば十分な範疇だと思います。 なんらかの事情において移行したいケースや、別のCI環境も利用してみたいと感じるようであれば、気軽に試してみても良いのではないでしょうか。

よりよいCI環境について知見のあるエンジニアの方がいましたら、ぜひこちらからご連絡いただければと思います。 一緒に最高のDXを作り上げましょう!

弊社オフィスで「Flutter Meetup Tokyo #9」を開催しました

こんにちは、スタディプラスの須藤(id:kurotyann)です。

今回のブログでは、5月22日(水)に弊社オフィスで開催した「Flutter Meetup Tokyo #9」についてまとめます。

f:id:kurotyann:20190522194615j:plain

Flutter Meetup Tokyo とは?

Flutter Meetup TokyoはFJUGが主催している、東京で開催するFlutter勉強会です。

今回で9回目を迎え、会場だけでなく、YouTubeでのライブ配信も行っています。

当日の様子は、ライブ配信のアーカイブまたは、Twitterのハッシュタグから確認できます。

www.youtube.com

twitter.com

運営スタッフから見た Flutter Meetup Tokyo

弊社ブログからは運営に関わった社員より、当日の様子や感想をお伝えします。

なお、当日の各発表については参加者の参加ブログを見てください。

Flutter Meetup Tokyo #9に行ってきました|あぼぼ|note

須藤(id:kurotyann)

弊社では、今年の2月から新規事業でFlutterを使ってアプリを開発しています。現在のところ、Flutterでの開発は順調に進んでおり、Flutterの対応力と進化のスピードに日々、驚いています。

いつか弊社オフィスで「Flutter Meetup Tokyo」を開催し、さらに登壇もすることで、このFlutterの魅力を伝えられればと思っていました。それが今回の第9回目で実現することができ、嬉しいと同時に少しホッとしています。

というのも、私は運営と5分LTの準備に追われ、前日と当日はとてもバタバタしておりました。定員70名という規模のイベント運営は初めてだったこともあり、軽食や飲み物の準備、会場設営など想像以上にやることが多かったです。

しかし、社員のみんなやFJUGの管理者の方々のサポートで、大きなトラブルもなく開催することができました。もしまた機会があれば、弊社で開催できればと思っています。そのときには、新規事業のアプリも完成してリリースされているはずです!

参加していただいた皆様、本当にありがとうございました!

若宮(id:D_R_1009)

Flutter Meetup Tokyoをついに弊社で開催することができました! 個人的には今回のMeetupが5回目の参加となります。 当日はほとんどバーカウンターの向こう側で過ごしていたのですが、これもまたいい思い出になりそうです。

今月はGoogle I/O 19でFlutter 1.5が発表されたこともあり、真新しいLTが多く非常に興味深い会になったのではないでしょうか。 合わせて、懇親会の雰囲気なども楽しんでいただけたようであれば嬉しいなと、運営スタッフとして感じています。

業務ではKotlinによるAndroidアプリ開発が主となっていますが、社内でもFlutterが盛り上がってきているので、引き続き個人開発を進めていきたいなと思っています。 もくもく勉強会など色々な形でFlutterコミュニティをサポートできればと思っているので、引き続きよろしくお願いいたします。

Flutter Japan User Groupの皆様、そして参加してくださった皆様、本当にありがとうございました!

大石(id:k_oishi)

Flutter Meetup Tokyoへの参加は2回目ですが、今回は入社間もない自社での開催となり当日の運営スタッフとして参加しました。

初めて勉強会の運営スタッフを経験しまして、大変ではありましたがとても良い経験をすることができたと同時に、普段参加している他社さんでの勉強会の運営の方々への感謝も忘れてはいけないと感じました。

初めてということもあり、今回は様々な面で改善の余地があったと感じましたが、今後に生かしていければと思っています。

登壇および参加していただいた皆様、ありがとうございました。

おわりに

次回のFlutter Meetup Tokyo #10の詳細は未定のようです。

しかし、6月には弊社オフィスにて「Flutterもくもく自習室 #3」を開催予定です。

第一回と第二回のFlutterもくもく自習室の模様は弊社ブログに投稿しております。

tech.studyplus.co.jp

もし、興味のある方がいたら、どうぞ第三回に参加して一緒にもくもく開発しましょう!

Studyplusのとある画面でYouTubeの動画再生に対応し、やらかしたお話

こんにちは、Studyplus iOSチームの明渡(ID: m_yamada1992)です。

今回は、今年3月にリリースしたStudyplusアプリにて大学の情報を表示する画面でYouTubeの再生に対応したお話、およびiOSアプリ側の実装にて盛大にやらかした話をつづっていきます。

YouTubeをStudyplusのアプリで表示する

動画を表示することになった背景

Studyplusは現状受験生のユーザーが非常に多く、「同じ志望校を目指す仲間と励まし合いたい!」というニーズに応えるため大学にまつわる情報を取り扱う機能が手厚く揃っております。志望大学を探す機能、同じ大学を志望しているユーザーの勉強時間ランキングなど。

そうすると、「Studyplusで志望大学を探すユーザーに自校をもっとアピールしたい!」という大学さんも少なからず存在するわけです。アピールする新しい手段の1つとして、動画を掲載もできるように対応することとなりました。

実現方法

YouTubeの動画サムネイルを表示して、サムネイルがタップされたらウェブビューのみの画面を表示して動画を表示するようにしました。実装イメージが以下のとおりです。

f:id:m_yamada1992:20190513212011p:plain:w300f:id:m_yamada1992:20190513212017p:plain:w300

左がサムネイル表示画面、右が遷移先の動画再生のみを目的とするWKWebView画面

なお、公式ライブラリの利用は真っ先に検討したのですが、iOSに関してはとうの昔に非推奨のUIWebViewを挟んで動作させることが前提だったので断念しました。 AndroidはきちんとメンテされているYouTube Android Player APIを採用してます。

YouTubeの動画サムネイルを取得する

YouTubeにて動画の情報を取り扱う手段として正式に提供されているYouTube Data API (v3)のうち、Videosリソースにて動画のサムネイル画像URLが取得できるので、そちらを使用しました。

ひとくちに動画情報といっても動画の投稿者やタイトル、説明文など今回不要な情報もたくさんあるので、クエリパラメータのpartfieldsで必要なレスポンスの値を絞り込みました。

let queryItems = [
    URLQueryItem(name: "id", value: id),
    URLQueryItem(name: "part", value: "snippet"),
    URLQueryItem(
        name: "fields",
        value: "items(snippet(thumbnails(standard(url),medium(url))))"
    )
]

過去に外部のAPIを使用する場面はそこそこあったのですが、「返ってくるレスポンスのうち○○と△△しか使わない」といったコメントを残さないと分かりにくいことがありました。 このように不要な値はレスポンスに含まないよう実装で制御し、使っている値が一目瞭然にできるのはすごく画期的に思えます。

YouTube独自のオーバーレイ表示へ対応

サムネイル画像をタップしたあと表示するWKWebViewの画面なのですが、Studyplusのアプリで素直な実装にて表示してみるとちょっとした問題が発生しました。

Studyplusアプリで実際にYouTubeの動画を再生した際、以下のキャプチャのような表示が可能です。

f:id:m_yamada1992:20190513162321p:plain

問題点

  • 端末の縦方向表示のみをサポートしているのに、YouTubeの動画プレイヤーを横方向で表示できてしまう
    • 端末を横にした状態で動画プレイヤーを閉じると、もとの画面でステータスバーが消えてしまう

なお、消えた状態のまま前の画面に戻ってもステータスバーは復帰せず、通常のナビゲーションバー下辺までの高さがステータスバー分縮んだままとなります。

キャプチャからも見て取れる通り、オーバーレイ表示上でステータスバーのみ標準UIを流用していることが影響している挙動のようです。

今回選んだ解決方法

記事冒頭のキャプチャでお察しかもしれませんが、YouTubeを表示するためのWKWebView画面でのみステータスバーの表示を諦めました。画面を表示時にステータスバーは非表示へ変更、画面を閉じるときに元通り表示すると他の画面は影響しないことが確認できたためです。

YouTube的にはプラットフォームの標準仕様より、ユーザーがいかに快適に動画を視聴できるかを追求した結果の挙動なのかしらとは思います。が、なぜステータスバーだけ標準OSで表示しているものを引っこ抜いていくのだろうか・・・?

こちらの手段に落ち着くまでにそこそこの試行錯誤や調査を要して戸惑ったので、そこまで独自の挙動をするのだったら全部独自のビューを表示すればいいのにと思ってしまいます。

やらかしたお話

結論から申し上げますと、YouTubeの動画サムネイル取得失敗のパターンが無限ループする実装を施してしまいました。

起こったこと

GCPのAPIなので、クォータを食い切ります。以下は弊社開発環境でのYouTube Data APIクォータ利用状況のGoogle Cloud Consoleの様子です。

GoogleCloudコンソール上で確認した、開発環境でクォータ上限突破した際のキャプチャ

キャプチャのうち、赤い点線が1日のクォータ上限です。現実的に上限を使い切ることはまずないだろうという値を設定しております。

1回目の時点で-8時間表記であることを見落として、夜中の3時に起こったアプリの操作では起こりえないという判断をしてアプリ担当としてはスルー。2回目に再発してこれはおかしいぞと調べたら無限ループを仕込んでいたことが発覚。

リリースを予定していたバージョンを数日前に審査提出・通過完了しており、翌日のリリース準備万端! というタイミングで2回目の再発でした。 同じ環境に接続されているうち1個の端末でエラーが発生するとクォータを一気に1日の上限まで使い切っちゃいます。

同じ事象を本番環境で発生させてしまうとサムネイルの取得が全ユーザーで最大24時間できなくなります。存在する動画はサムネイルが取得できるものとして実装しているため、YouTubeの動画をアプリで表示できなくなります。致命的です。

原因

対象の画面で既存バージョンから存在する、ほとんど同じ形で単純に画像の表示を行うセルにて取得した画像サイズに応じて高さを変更していました。そしてYouTubeのサムネイルを表示するセルを新しく作成するときに流用したのですが、その際に本来不要な画像サイズに応じた高さ変更処理諸共流用して放置して開発を進めたのが直接の原因です。

ディレクターチームにリリースを予定している機能を一通り動作確認してもらったあと、iOSチーム内でのソースコードレビューにて指摘をもらって上記の処理をリファクタリングしたのが事の発端でした。

実装の詳細

まず、動画を表示するセルにて表示する内容を設定するメソッドは以下のようなイメージです。

func setup(text: String, completion: @escaping (_ loadImageRatio: CGFloat?) -> Void) {
    // ...省略...
    if let thumbnailUrlString = videoThumbnailUrl {
        // YouTubeのサムネイルのURLをすでに取得済みの場合、画像を表示
        loadImage(urlString: thumbnailUrlString)
    } else {
        // 動画サムネイルURLの取得に1回失敗していた場合、このタイミングでリトライ
        guard let videoId = youtubeId else { return }
        VideoRepository.getVideoData(id: videoId, success: { thumbnailUrl in
            /* このセル用のコンテキストインスタンスへサムネイルのURLを保持 */
            // 取得したYouTubeのサムネイルのURLから画像を取得して表示
            self.loadImage(urlString: thumbnailUrl)
        }, failure: { _ in
            /* 動画を表示できない旨を表記 */
        }, finally: {
            // 成功したときは取得した画像の縦横比率を、失敗時は失敗したとき用の画像の縦横比率を渡す
            completion(imageRatio)
        })
    }
    // ...省略...
}

YouTubeから動画のサムネイルURLをすでに取得して保持済みの場合はその画像を表示、何らかの影響でサムネイルURLを取得できずに動画が表示できていなかった場合はリトライしてみるという実装です。

なお、この画面で表示する動画IDは弊社側で大学から頂戴したデータを管理するので、まず存在しないものはないはずという前提です。一瞬ネットワークの調子が悪くて取得失敗したなら、スクロールして戻ってきた際にリトライしたほうが親切だろうと。

問題が発生した動画を表示するセル生成処理

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // ...省略...
    let cell = tableView.dequeueReusableCell(withIdentifier: "VideoCell", for: indexPath)
    cell.setup(text: text, completion: { imageRatio in
        /* このタイミングで取得完了した画像の縦横比率をこのセル用のコンテキストインスタンスへ保持し、以降画面へセルを再表示しても同じ高さで表示 */
        tableView.reloadRows(at: [indexPath], with: .none)
    })
    // ...省略...
}

上記のうち reloadRows(at:with:) メソッドを呼んでいる箇所で、リファクタリング前にperformBatchUpdates(_:completion:)メソッドを引数nilで呼ぶという一見なぜこの処理をこのタイミングで呼ぶか分かりにくい状態でした。ですので、そもそも reloadRows(at:with:) メソッドでも同じ挙動が期待できる()から、こっちのほうがシンプルでいいよね! という結論にいたり修正してこの実装に落ち着きました。

※厳密にはreloadRows(at:with:) メソッドを実行時にはtableView(_:cellForRowAt:)メソッドが呼ばれ、 引数nilのperformBatchUpdates(_:completion:)メソッド実行時は呼ばれないという違いがあります

起こったこと

  1. セルを生成する
  2. 指定した動画IDがなんらかの原因で存在しない、またはアクセス不可なので動画のサムネイルURL取得を再度試みる
  3. 共通の完了処理を呼ぶ
  4. 完了処理でreloadRows(at:)を呼ぶ
  5. 動画を表示するセルを再生成する(1に戻る)

という流れでの無限ループが完成しました。 動画を表示するためのセルが見える範囲内に存在する限り、無限に処理が走り続けます。

対応

さらにそもそもの話で、YouTubeの動画サムネイルはサイズ指定して取得できるため、画像が空の状態で一旦表示したあとに高さが変わることへの考慮は一切必要ございませんでした。 なので、サムネイル取得の試行完了後に行なっていた再読み込み処理をばっさり削除するかたちで事象は解消しました。

対応後の実装

セルのsetupメソッドからcompletion引数を削除しました。

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // ...省略...
    let cell = tableView.dequeueReusableCell(withIdentifier: "VideoCell", for: indexPath)
    cell.setup(text: text)
    return cell
    // ...省略...
}

なんて無駄で弊害のある処理をしていたのでしょう! 驚きのシンプルさ。😭

修正版のリリース

リリース予定前日であり、その日修正版で審査を再提出してリジェクトされずに翌日リリースできるという保証はありません。ですので、社内のディレクターチームへ事象の説明や修正をいつリリースするかの対応を相談しました。

結果的な対応としては、もともとのバージョンを一旦予定通りリリースした直後に修正版を審査に提出、審査が通った次の日にリリースしました。

事象が発生したとしてもユーザー影響が限定的であることを鑑みての判断です。今回の事象が発生するのはケース入稿元のデータがおかしくないと滅多なことで発生し得ないこと、リリース日当日から動画の表示を始める大学がまだそれほど多くなかったこともあり。

さいごに

至極当たり前でたいへん初歩的ですが、以下を肝に銘じました・・・

  • こまかな仕様検討が開発と並行で走らざるおえない場合、未決定の状態で極力実装を進めない
    • 動画の取得が失敗した際、失敗したとき用画像を代わりに表示するかサムネイルの表示領域自体を非表示にするか開発フェーズ終盤まで確定させていなかった
      • 決まったあと不要と確定した処理を削れば良いかと思っていた節もある
    • 後回しにしがちだったエラーケースも早めに決めてもらえるよう、ディレクターへ積極的に声がけする
  • 既存の類似UIの処理を流用する際は、その時点で絶対に使用するとわかっている処理のみ絞り込んで利用する
    • "大は小を兼ねる"という概念はソースコードに当てはまらない、当てはめちゃいけない
  • リファクタリング先の影響範囲動作確認を徹底する
    • 全く同じように動くだろうと確認漏らすのはダメ絶対

現在担当しているプロジェクトの実装の品質は、前に担当したものよりかは安定しつつあるかなと自負してます。いや、安定していかないと不味いんですが。

ただし、新規事業開発でお忙しいところiOSチームリーダーの須藤さんにかなりの助力を頂いております。 今回の記事に記載したYouTubeのオーバーレイ表示に対する対応やサムネイルURL取得処理無限ループへの対応をはじめ、現在進行中のプロジェクトも。

まだまだ知識経験共に足りないひよっ子ですが、少しでも早くより力になれるよう、ひよっ子なりに学び取ったことは血肉として今後に活かしてゆく所存です。

そして、やらかしが発覚したあとの対応を決めるまでのスピード感や、「ブログ当番時に今回やらかした内容したためます〜」という具合にアウトプットすることへの寛容さにも驚いたり。

実は事象が発覚したリリース前日に自宅からのリモート勤務をしていて、これは1回会社に出勤して各方面へ直接相談しないと不味いかなと肝を冷やしました。 が、ディレクターチームへの対応の相談はMTGすら発生せず全部Slackのチャットで完結しました。ちょっと感動しました・・・良い環境に身を置けているなぁとしみじみ思います。

弊社のSSO事情について

こんにちは、サーバーサイドエンジニアの金澤です。

みなさんSSOしてますか?

今日は弊社の管理ツールなどで導入しているSSOについてお話しします。

現状

大きく分けて二通りの方法で実現しています。

  • awsのalbでopenid connect(以下oidc)
  • google G suiteのsaml

よくあることですがなぜ二つあるのかについては歴史的経緯以外の理由はありません。

利用する側から見た使い勝手もほぼ変わらないので、実装面などでの差をお話ししたいと思います。

導入方法

それぞれの導入方法について軽くご紹介します。

両者に共通することですが、ヘッダに認証情報が入っているのでhttpsのみの運用が強く推奨されています。

samlの設定例

googleのドキュメントはこちらにあります。

railsアプリケーションなので、OmniAuthとそのsaml strategyを使いました。

admin consoleからappを追加する

google G suiteのadmin権限が必要です。 admin画面から登録しましょう。 このアイコンが目印です。 Screen Shot 2019-05-08 at 18.35.04.png (22.0 kB)

  • SSO URLEntity IdというのがOmniAuthの設定に出てくるidp_sso_target_urlissuerなのでメモしておきましょう
  • 証明書もダウンロードしましょう
  • ユーザーの情報が欲しいときは、attribute mappingに追加
    • 弊社はメアドと名前が欲しかったので以下のようにしました。

Screen Shot 2019-05-08 at 18.33.55.png (84.4 kB)

また、googleからcallbackを呼ぶ必要があるのでそのurlを登録する必要があります。 デプロイする環境が決まったら設定しましょう。

Screen Shot 2019-05-08 at 18.39.08.png (60.8 kB)

entity idはユニークであればなんでもいいのですがurlを入れておくのが分かりやすいし必ずユニークになるので便利かと思います。

有効化する

作った時点ではAppの設定がOff for everyoneなのでonに設定する必要があります。 特定の人にだけ公開したい場合は、下記画面から手で設定することができます。

Screen Shot 2019-05-08 at 20.11.55.png (220.9 kB)

rails newする

いつものやつです。

GemfileにOmniAuthを足す

    gem 'omniauth'
    gem 'omniauth-saml'

initializerを足す

こちらの設定をします。 middlewareでもinitializerでもいいですが、弊社はinitializerにしました。 大体以下のような内容になります。

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :developer unless Rails.env.production?
  provider :saml,
    :issuer                             => "",
    :idp_sso_target_url                 => "",
    :idp_cert                           => ""
end

それぞれに入れるべきものはG suiteのadmin consoleへ登録するときにわかります。

callbackメソッド作る

先ほど出てきたcallbackメソッドを実装する必要があります。 以下サンプルコードです。

post "/auth/:provider/callback" => "welcome#callback"

def callback
    if request.env['omniauth.auth'].extra.response_object.present?
      attributes = request.env['omniauth.auth'].extra.response_object.attributes
      email = attributes[:email] || ''
      first_name = attributes[:first_name] || ''
      last_name = attributes[:last_name] || ''
      name = last_name + first_name
    else
      # OmniAuthが提供する開発用の認証方法
      # http://localhost:3000/auth/developer
      auth = request.env['omniauth.auth']
      name = auth[:info][:name]
      email = auth[:info][:email]
    end

    session[:email] = email
    redirect_to '/'
  end

大体このような感じでヘッダからユーザー情報を取り出せます。 あとはそれをお好みでセッションなどお好きなものに放り込んでそれの有無でログイン状態を判断できます。

albの設定例

awsのalbとoidcを使った設定例です。

oidcクレデンシャルの追加

  • こちらCreate credentials => OAuth client ID からクレデンシャルを追加する。

    • こちらはこちらで上記とは別の権限が必要になります。
  • Authorized JavaScript origins は適宜設定しましょう。

  • redirect urihttps://{{ ホスト名 }}/oauth2/idpresponse を登録しましょう。
    • /oauth2/idpresponse のハンドリングはalbが勝手にやってくれるのでここの実装は必要ありません。
  • client idclient secret をメモする。

アプリケーションの公開範囲制限

社内ツールなどは外部に公開する必要がないので、「OAuth同意画面」から「アプリケーションの種類」を「内部」に変更しましょう。

albに設定追加

  • awsコンソールから認証を追加したいalbを選び、 Listeners タブを選ぶ。
  • Rules のところに View/edit rules とあるので選ぶ。

全てのパスに認証をかけたい場合はこのような感じで設定できます。

Screen Shot 2019-05-08 at 20.00.46.png (218.5 kB)

  • Issuerと3種のendpointはgoogleという事業者で一つなので固定です。
    • 厳密には時期によって変わる可能性はあるはず
  • これらの情報はgoogleのOpenID Connect Discoveryから取得できます。仕様についてはこちら

ユーザー情報

上記設定のすぐ下にAdvanced settingsというものがあります(Timeoutはテストのため適当に変えたのでデフォルトでいいと思います)。

Screen Shot 2018-11-08 at 16.13.03.png (88.1 kB)

Scopeというのはsamlでいうattribute mappingsのようなもので、上記のOpenID Connect Discoveryにも scopes_supported という項目で何が指定できるか書いてあります。

最後に、認証が通ったらどこにforwardするか設定します。

実装

amazonによる実装例がこちらにあります。 Rubyで同じ内容に書き起こしましょう。 認証をかけたい機能ではrequestのヘッダを確認して、ユーザー情報が取れない場合は401を返すなり別のところにredirectするなり実装すれば終わりです。

それぞれのまとめ

saml

  • albよりは書くことが多い
    • とはいえOmniAuthが相当な部分吸収してくれているので大きな手間ではない
  • 証明書の期限があるので数年で使えなくなる
    • スパンが数年になるとどうしても優先度下がるし忘れそう
  • samlはRFCに仕様があるのでamazonにロックインされていないという自己満足

alb

  • 設定はsamlより楽
  • 証明書をアプリで意識しなくてもいい
  • ローカルで開発しながら動かすためにはログイン状態にするためちょっとアドホックなコードが必要
    • samlの方には開発用のログイン方法がOmniAuthにあるので楽
    • もしかしたらいい方法があるのかもしれない
  • amazonにロックインされているという被害妄想
    • oidc自体はOAuth 2.0をベースにした規格なのでopen
    • とはいえ現実的にはamazonから動かすことってあまりなさそう

終わりに

どちらが明確に優れているということは無いと思いますが、仮に自分がまたSSOのアプリを社内向けに用意するとしたらalbを使う気はします。

とはいえalbは当然amazonでしか使えないので、読んでくださっている方の中には使えない場合もあるかもしれません。

どちらの方法でもemailは取れるので(設定は必要)、「認証の結果セッションの中などどこかにemailが入っている」というぐらいまで責務を薄くして作っています。 そうすればあとで切り替えたくなったときも変更は最小限で済むかと思います。

権限についてですが、弊社は今の所誰が何をしたかは記録しているけれど権限制御をガチガチにしているわけではありません。 もしやることになったとしても「Gmailなどの組織図」と「管理ツールの権限の分布」が一致するということは希だと思うので、どのみちそこは手で頑張るしかないと感じています。となると外部に依存するよりは弊社のデータベースなどでemailと紐づけて管理、という形になるのかなとぼんやり思っています。

とはいえ凝りすぎるとSSOの楽さという長所が失われてしまうのでよっぽどの重大な機能(個人情報に関わるなど)でなければ権限を複雑にするつもりは今の所ありません。

何はともあれ、ssoを入れておくと本当に楽です。弊社は少し前に営業の方の入社ラッシュがあったのでそのときに導入できていなかったら多分すごく面倒だったろうなと思っています。

この記事が皆さんのSSO導入の第一歩になればこれに勝る喜びはありません。

ありがとうございました。

RubyKaigi 2019にPlatinumスポンサーとして参加しました!

こんちにちは、スタディプラスCTOの島田です。

スタディプラスはRubyKaigi 2019Platinum Sponsorsとして参加させて頂きました。

こちらで投稿したとおり4/18~19の3日間スポンサーブースを出展しました。
ブースでは学習記録サービス「Studyplus」にちなんで「ORB(推し Ruby Book)選抜総選挙」と銘打って、好きなRuby本の投票を実施いたしました。
RubyWorld Conference 2018での投票結果と、その時に投票頂いた方の意見を元に5つの書籍を選定しました。

選定書籍一覧 - パーフェクトRuby - プロを目指す人のためのRuby入門 - たのしいRuby - ゼロからわかる Ruby 超入門 - Ruby逆引きハンドブック

スポンサーブースにて

数量限定で準備したサコッシュでしたが多くの方に投票を頂き、あっと言う間に配布が終わってしまいました。

技術書典6にて頒布された「マンガでわかるRuby」「RubyとRailsの学習ガイド」を献本頂きました。

著者の方にサインも頂きました。

matzさんもいらっしゃいました。

3日間の投票結果はこちらになります。

感想

参加メンバーの印象に残った発表です。

島田

Pattern matching - New feature in Ruby 2.7

https://speakerdeck.com/k_tsj/pattern-matching-new-feature-in-ruby-2-dot-7 @k_tsjさんによるRuby2.7のパターンマッチングの発表。

Pattern matchingとは

  • Ruby 2.7で実験的に導入された機能
  • パターンによるマッチ処理をして、マッチした値を複数の変数に代入できる
  • case/in を利用する

構文

  • パターンが上から順番に、最初に一致するまで走査される
  • マッチしない場合はelseが実行される
  • パターンにマッチせずelseがない場合は、NoMatchingPatternErrorがraiseされる
case expr
in pattern [if|unless condition]
 ...
in pattern [if|unless condition]
 ...
else
 ...
end

設計で意識した事

  • 下位互換
  • Ruby的な文法

実装のパターンを抜粋

リテラル値を利用したパターン

case 0
  in 0
  in -1..1
  in Integer
end

_を利用したパターン

case [0,1]
in [_,_]
  :reachable
end

Pin Operator。変数の値に対してパターンマッチングを行いたい場合は^を使用

a = 0
case 1
in ^a
  :reachable
end

ASパターン

case 0
in Integer => a
  a #=> 0
end

Arrayパターン

class Array
  def deconstruct
    self
  end
end

case [0, 1, 2]
in Array(0, *a, 2)
in Object[0, *a, 2]
in 0, *a, 2
end

p a #=> [1]

Hashパターン

case {a: 0}
in a: 0
  true
end

# => true

まだ実験段階のパターンマッチですが、その幅広さにRubyの柔軟性をより広げていくものだと感じました。

花井

All bugfixes are incompatibilities

ruby trunk changesでおなじみの、@nagachikaさんによる、安定版Rubyのメンテナンスについてのお話です。 1年に1回歳をとる人間の脆弱性を念頭に、これからメンテナになる人への申し送り事項の伝達というスタイルの発表でした。 trunkからバックポートしたさいに起きた実際のバグを例に、メンテナンスのキモになる「何を取り込むか判断すること」について

  • パフォーマンス改善は不具合修正と言えばそうだが、バックポート対象にはすべきでなかった。trunkがよくなればいいだけで、安定版は安定性を損なわないようにすべきだった。
  • メソッド/定数探索周りはエッジケースも多く、文法周りは組み合わせが膨大で全てテストで網羅して事前検知するのは難しい。
  • 長い間放置されていた(気づかれていない)SyntaxErrorや、リリースした当時の状況的に使われてる量が少ないものは慌てて直さなくてもよかった。

「あるアプリケーションで困っている人がいても、もっと大きなアプリケーションを壊してしまう場合はよりユーザーが多い方を優先しなければいけないという判断をしないといけないのは心苦しい。しかし、それが安定版メンテナのお仕事なのです」 という結びの言葉が印象にのこりました。

Crystalball: predicting test failures

@p0dejeさんによるcrystalballを利用した回帰テストの選択実行について内部実装のお話とデモです。 小さな変更に対して全てのテストを実行するとどうしても時間がかかってしまうので、変更したコードに関係するテストをカバレッジベースで解析し、マッピングしたうえで変更箇所に関係しているテストだけが走るようにできるというものです。 コードの変更はもちろん、schemaファイルやFactoryBotの変更にも対応していて感動しました。セッション後にridgepoleのSchemafileにも対応してるのか聞いてみましたが、ridgepoleをご存じなくデフォルトでは対応していないとのことでした。 @p0dejeさんもGraphQL周りでやっている解決策として、自前でPredictorを書くという手があるそうなので、実際に試してみようと思いました。

Best practices in web API client development

キュアエンジニア@sue445さんによる、APIクライアント開発のベストプラクティスの紹介です。

APIクライアントの責務と、作成者(自分/他人)、公開範囲(public/private)の組み合わせによるケースごとにベストプラクティスが紹介されました。

まずAPIクライアントに作るべき機能として以下をあげています。 - パラメータの変換(型変換、スネークケース・キャメルケースの変換) - エラーコード1のラップ(サンドイッチメソッドの抽出2) - OAuth2アクセストークンの自動更新

逆に作るべきでない機能としては、 - アプリの責務である、APIレスポンスのキャッシュ - 必須や空白チェック以外のリクエストパラメータの詳細なバリデーション

をあげていました。そのほかGood Patternとして以下の7つの紹介がありました。

  • runtime dependencyをできるだけ使わない(できるだけRuby標準機能でつくる)
  • Getだけならopen-uriで十分
  • Faradayは銀の弾丸
  • CRUD全部必要なケースで有用
  • 主題を目立たせる
  • 共通で使うならインスタンス変数にしておく
  • Hash引数よりkeyword引数
  • メソッドの定義が必要なものを物語るので実装しやすい・テストしやすい
  • 新しいサービスなどで頻繁に更新されるならHashでもよい
  • パラメータオブジェクト
  • たくさんの引数を受ける場合に有効
  • パラメータオブジェクト自体のテストもかける
  • 導入基準はYARDでコメントを書きやすくなるか
  • メソッドアクセス可能なレスポンス
  • mashifyを入れえると簡単に実現可能
  • curlは万国共通
  • バグレポートしたいとき、API提供者がRubyを読めるとは限らないのでcurlでレポートを書く
  • faraday-curlを使うとラク

どの内容もすぐに使える実践的な内容であるばかりか、普段の開発でも心がけた方がいい内容も含まれており、 APIクライアント作成の知見が詰まった素晴らしい発表でした。

冨山

印象に残った講演

How to use OpenAPI3 for API developer

twitter.com committeeのOpenAPI3.0対応をしたota42yさんのお話でした.
弊社でもAPI開発の際にcommitteeを使用しており内部実装の話は大変為になりました.
committeeは抽象化されたvalidatorを用意して,OpenAPI2.0, OpenAPI3.0, JSON Schemaなど複数のスキーマに対応していて, OpenAPI3.0のスキーマを理解する部分はopenapi_parserで行っているようです.またバリデーションを行う際にはパスに対応するAPIの定義が含まれるオブジェクトを参照する必要がありここの実装にも工夫がありました.
パスの階層をノードとしたパトリシア木というデータ構造をRubyオブジェクトとして持っているという設計は階層ごとにネストしたHashで定義したり,パスを全件探索するような実装より優れていて印象に強く残りました.

Pragmatic Monadic Programing in Ruby

twitter.com こちらはjoker1007さんによるRubyでmonadをする話でした. Procは手続きを持った関数オブジェクトであるとすれば関数型言語的にかけるようにできるはずだというもので, RubyVM::ASTやTracePointなどを使って悪事を働くコードを披露しつつもスタックトレースは追えるよう実装されていたりと終始会場が賑わう楽しい発表でした.

またDay1のアフターパーティは商店街を貸し切って行われるという今までに無い形の懇親会で,商店街のあちこちで日本酒,ビール,食事などなどが提供されていて,多くの人が歩きながらお酒を飲んで談笑していたため多くの方と交流ができて楽しめました. 弊社メンバーと歩いていたらメタプログラミングRuby3の著者Paolo Perrottaがいたので一緒に写真を取っていただきました! f:id:atomiyama:20190509102457j:plain

最後に

昨年度に続きRubyKaigiの協賛をさせていただき、今回は初めてスポンサーブースの出展をさせていただきました。 ブースへは本当に多くの方に足を運んで頂き、投票への協力をしていただいた事に感謝です。

スタディプラスでは、今後もRubyコミュニティの貢献を考えております。 RubyKaigi 2020は長野県松本市という事なので、何かの形で協力を出来ればと思います。


  1. APIの通信にFaradayを使うとAPIクライアントのユーザーがFaradayを意識しないといけなくなるので、Rubyのエラーに変換するという例をあげていました

  2. 『リファクタリング:Rubyエディション』

  3. メタプログラミングRuby