クラシル開発ブログ

クラシル開発ブログ

クラシルサーバーサイドエンジニアのとある1日

こんにちは!dely 開発部でクラシルのサーバーサイドエンジニアをやっています @_kobuuukataです!👩🏻‍💻

コロナの緊急事態宣言の影響で、 dely もリモートワークを採用しています🏡
そこで今回は、クラシルのサーバーサイドエンジニアがリモートワーク時、どんな1日を過ごしているのか紹介したいと思います!

リモート時の1日のスケジュールはこんな感じです↓

f:id:aym413:20210531151111p:plain
リモート時のスケジュール

9:00 出社

dely では「フレックス制」を採用しています。
フレックス制と聞くと、コアタイム以外は自由に出勤していいというイメージがあるかもしれませんが、dely では、何時から何時の間で働きます!というのを事前に申告する形をとっています。(なお、前日までなら変更OK!)
ちなみに私はリモートの時は9時から、出社の時は10時からにしてます。
出社後は、Slack チャンネルに今日のやることを書いていきます(以下イメージ)

2021/05/31
- 現在取り組んでいるメインissue:〇〇機能改修(〜5/31)
- TODO
    - [ ] 〇〇機能改修の実装
    - [ ] 問い合わせの不具合調査
    - [ ] PRレビュー
- MTG
  - [ ] 朝会
  - [ ] サーバーサイド採用定例
  - [ ] サーバーサイドMTG
  - [ ] 1on1

9:00 PRレビュー/レビューコメントの対応

始業開始はまず、溜まっているPRレビューを見たり、自分が出したPRレビューのコメントが来ているものを対応してます。
朝は比較的 Slack 通知が飛んでくることが少ないので、この時間で集中して確認することが多いです。
最近はサーバーサイドのメンバーが増えてきたこともあり、1日で 3~5 件くらいの新規 PR が飛んできます。

10:00 Squad 朝会

各 Squad の朝会は 10:00 開始が基本ルール。メンバーが Squad を兼務しないという原則に基づくようにしています。
リモートの日は meet を使って朝会を実施しています。
私の所属する Squad では、PdM/デザイナー/Android/iOS/サーバーサイド/フードメンバーが参加し、朝会を行っています。朝会では、細かいタスクの進捗状況を確認するというよりは、リリースに向けた仕様の共有や分析結果の共有など Squad 全体に関わることを共有します。

Squad 体制とは?dely 開発部がなぜ Squad 体制を採用したのか?についてはこちらの記事をご覧ください↓

blog.tsubotax.com

10:15 Squad サーバーサイド内での職種朝会

私の所属する Squad は少し他 Squad よりもメンバーが多く、現在インターン生も含め5名のサーバーサイドエンジニアが1つの Squad に所属していることもあり、Squad 朝会が終わったあとに Squad 内のサーバーサイドメンバーでタスクの進捗状況を共有します。設計や実装で困ったことがあれば、ここで相談します。

10:30 サーバーサイド仕様書の作成

新規機能開発を行うときは、まずサーバーサイド仕様書を作成していきます。
本番環境で発生したバグ改修以外はこの仕様書を作成するルールとなっています。
サーバーサイド仕様書はサーバーサイド(Rails)以外にも、AWSの環境設定などが必要になる場合もあるので、設計漏れがないかSREメンバーにもレビューしてもらいます。
API エンドポイントのレスポンスは Android/iOS のメンバーと相談し、合意をとっておきます。
単にどう実装するのかを記載するだけではなく、なぜその機能を実装する必要があるのか?といった観点も含まれており、なるべく後戻りなく負債として残らないようにしています。

f:id:aym413:20210529220314p:plain
サーバーサイド仕様書テンプレート

13:00 お昼

リモートの時は自炊することが多いです。
クラシルでは、リモートの合間にササッとできるレシピも紹介されているので、ぜひみなさん作ってみてください👩🏻‍🍳

f:id:aym413:20210529221026p:plain:w300

お昼を食べたあと、天気がいい日はベランダで日向ぼっこするのにハマっています😎🌴

14:00 コードの実装

サーバーサイド仕様書のレビューが通ったら、いよいよ実装していきます。
API を実装する際は、API 作成ルールがドキュメントにまとめられているので、入社したてでもあまり迷うことなく実装できると思います!
また、リモートの際は、discord というツールを使っていて、何か困ったことや相談ごとあれば気軽に相談できるようにしています。

16:30 Rails エンジニア採用定例

dely では、採用も各職種ごとに KPI を持ち、メンバー全員が採用に携わっています💪
サーバーサイドでは Rails エンジニアの新卒・中途採用の進捗状況を確認し、KPI 達成に向け、改善を繰り返しています。

17:00 サーバーサイドMTG

Squad 体制になり、サーバーサイドメンバーで集まって話す機会が減ってしまったため、サーバーサイドメンバーで課題や共有事項などを話す会です。
なぜこれをやることになったのか?具体的にどんなことをやっているか?については、こちらの記事に書かれているので、是非ご覧ください!

tech.dely.jp

17:30 1on1

チームの上長によって頻度は異なりますが、私のチームでは2週間に1度のペースで 1on1 をやっています。
事前アンケートで、どんなテーマを話したいか?体調が悪いところはないか?目標の進捗状況はどうか?などの項目を記入し、その内容に沿ってざっくばらんに話してます。

18:00 退社

お疲れさまでした〜!帰宅前に Slack に今日やったことを書きます📝

2021/05/31
- 現在取り組んでいるメインissue:〇〇機能改修(〜5/31)
- TODO
    - [x] 〇〇機能改修の実装
    - [ ] 問い合わせの不具合調査
    - [x] PRレビュー
- MTG
  - [x] 朝会
  - [x] サーバーサイド採用定例
  - [x] サーバーサイドMTG
  - [x] 1on1

残業は1日1時間程度で、基本的に定時で帰れることが多いです。
私も入社前は本当に?と思っていましたが笑、本当に 20 時にはほとんど開発メンバーはいません!
みんなメリハリをつけて業務を行っているのがクラシル開発部の特徴かもしれません。

おわりに

いかがでしたか?
少しでも dely で働くイメージの参考になれば嬉しいです!


delyではエンジニアを募集しています

dely.jp

22・23卒の方はこちらから dely.jp

今日からエンジニアとして働く皆さんへ

f:id:mochizuki_pg:20210327140923p:plain


こんにちは
delyサーバーサイドエンジニアの望月 (@0000_pg)です

4月になり、春の季節がやってきました

新入生・新社会人の皆さん、おめでとうございます🌸🌸

今回は技術的な内容ではなく、せっかく春なので
新社会人となり、エンジニアとして働く皆さんや

これからエンジニアとして働いてみようかなと思っている皆さんに向けて
ポエム的な内容でお送りします😉🌸

はじめに


2020年の7月頃から
サーバーサイドエンジニアにおける中途採用のカジュアル面談や
1次面接を担当してきました

また、今年からは
ー部、新卒採用でも話しをさせてもらったりしています

多くのエンジニアの皆さんとお話しさせてもらい
対話のなかで、自分自身の考えもアップデートさせてもらっている感覚があり
とても有意義な時間だなと感じています

delyにおける新卒採用


delyにおいても20・21卒 (そして22卒) の方が入社を決めてくれて
パワーや可能性や希望のようなものに日々、圧倒されています✨✨✨

自分もdelyに中途入社した時は彼ら、彼女らと
そこまで変わりない年齢だったのですが
当時の自分と比べて、本当にしっかりしているな・・と感じます💦

エンジニアという仕事


エンジニアという職業は本当に面白いなと思います
自分がエンジニアになろうと思ったのも、モノづくりを通して
社会に対して、直接影響を与えられるということに面白味を感じたからです

どんなプロダクトをつくっていたとしても
それを利用する"者"がいて、
エンジニアリングであらゆる問題を解決、効率化していくのが
仕事というのは面白いなと思います

クラシルで言うと、SNSなどを通してユーザーさんの声が届きます
そういった自分 (たち) がつくったモノへの嬉しい言葉が励みになったりします

delyにおいては、エンジニアは "実装者" で終わることはなく
自らが主体性を持って意見や要望を述べ
プロダクトを開発していく必要があり、それも面白い所かなと思います

"技術力" がなくてもできることはある


エンジニアになりたての頃は、

「なんで自分はこんなこともできないのだろう」
こんなことも知らない自分は・・」

などと自分に失望したり、落胆する瞬間が必ずあります

でも、それが当たり前というか
それでいいんじゃないかと思います

なので開き直っていくのが大切だと思います


自分は藤倉成太さんのこの記事がとても好きで
いまでもふと思い出したときに読んでいます

type.jp

一部を引用します

「世界を変えたいとかいうのに、なぜか『修行宣言』をする人が多いんですよ。いつか技術が身に付いたらやります、とか、20代のうちは修行します、なんて。少しでもコードが書けるなら、10年後じゃなくて今始めればいいじゃないですか。確かに技術力は必要だけど、それがなくてもできることはいくらでもある。例えばチームで開発しているなら、こぼれ落ちた簡単なタスクをどんどん拾って、チームにどう貢献するかを考えることの方が大事です。

技術力だけが世の中を変える武器だというのは大間違いだし、いつまでもアマチュア気分でのんきなことを言っている場合じゃない。先輩のコードを読み書きしたり、コードレビューのやり取りを復習したり、現場で飛び交う言葉を調べることからだって始められるわけです。『エンジニアは技術力がなければ、何者にもなれない』なんて、まずスタート地点が違うんじゃないかと思います」


"技術力" というものは "経験" によって培われるものなので、
はじめから、技術力がある人はいないし
そこまで気にせずやればいいと思います

そして

自分のなりたい理想像と、現在の自分にギャップがあるなら
徐々にそのギャップを埋めていけばいいと思います

"窓口" になる


上の話につながるのですが、
自分はdelyに入社して、とにかく "窓口" になることにしました


日々の業務のなかでくる依頼をすべて自分に集約するということです
(もちろん属人化という意味ではないです)


自分が窓口になることで、
他部署の人とも強制的にコミュニケーションが必要になり
接点ができて自分にもメリットがありました

また、困ったときに頼ってもらえる存在になりたいな
という気持ちも同時に生まれました

リポジトリのドメイン知識をつける


ドメイン知識を付けることはやったほうがいいなと思います

前述の自分が窓口になることで
更にドメイン知識を付ける機会がありました


現状クラシルのサーバサイドにおけるコードがどうなっているか
全体の70%ぐらいは把握しています

ドメイン知識を付けることで困りごとが生じた際に
なにか手助けができる存在にもなれるし、実装の拡張を行う際にも役立ちます

とにかく模倣する


これはもう散々聞いた話ではあると思うのですが
周りに自分のレベルを遥かに超える人がいたりします

そういう人の技術をとにかく模倣したり、盗むことが大切だと思います
コードレビューや実装をみてどんどん自分のものにすればいいかなと思います


自分もクラシルは経験上、最も大きなプロダクトだったので
delyに入社して、学ぶことがたくさんありました

コードを読む、動かす、つくる


上の話の続きなのですが、やっぱりコードを読むことが
一番いいかなと思います

GitHubのOSSを色々検索してコードリーディングしたり
自分はAPIを使って色々つくるのがすきなので
OSSのAPI Client のコードを読むことが多いです

ライブラリの中身をみるのもいいです
(例えばRubyであれば、gemの中身など)

gem特有のメソッドが、中身をみたら
とても簡単な処理であることもあります

また、OSSコントリビュートするのもいいと思います

コントリビュート手順を読まないといけなかったり
CLAに署名したり、適切に追従したり

英文でPRを出したり
派生元ブランチを適切に指定して切ったり
PRを出すだけでも学ぶことがたくさんあると思います

他のメンバーを頼る


自分が失敗したなぁと思うのが、周りに頼ることができなかったという点です
入社したての頃は
何もできないのに、周りに頼ることもできませんでした


ただ、当時は他のメンバーはもっと難易度の高い課題に取り組んでいたり
チームとしても小規模でした

(完全な自走が求められるなかで、自分自身の技術力や経験が不足していた😥)


いまはサーバーサイドチームも2倍の人数になったので

tech.dely.jp

サーバーサイドMTGをおこなって、
色々なコミュニケーションが取れる機会をつくっています


結局のところ、自分ができることは限られているし
色んな人の助けを借りながらやったほうが
最終的に早く、うまくいったりします

色んなことに首をつっこみ、チャンスは掴み取る


なるべくチャレンジする機会があれば、
手を挙げるように心がけています

多少失敗しても、得られることのほうが圧倒的に多いです

自分も入社して初めて挑戦したことが多くあります

色んなことに、首をつっこむと多くの人と関わることになって
自分の存在を知ってもらえるし、 "信頼を得る" ことができます


この、"信頼を得る" ということが大事かなと思っていて

「この人ならこの業務を任せてもいいだろう」 とか
「この人に聞けばわかる」 という存在になることです


自分も "信頼" を稼いでる途中です😉💰

やってみてげんなりすることを繰り返す



落合陽一さんのnoteのタイトルからとりました

この言葉が好きで、本当にその通りだなと思います

自分も、いま思い出すとあまりに無様すぎて
笑うしかないような瞬間、瞬間が多くありました

ですが

やらないで斜に構えて自分の陣地を守っていると老化するし、
圧倒的成長! とか言っていれば
きっと若いまま何もしないで終わってしまうのだと思います

やってみて、色んな失敗をして、
自分自身の実力の無さに、げんなりしたり
絶望的な気持ちになることを
繰り返していくことで、"成長" できるのだろうと思います

おわりに


delyではエンジニアを募集しています

dely.jp

22・23卒の方はこちらから

dely.jp

クラシル開発体制の変化と「クラシルサーバーサイドMTG」という取り組み

こんにちは、開発部の高橋です。

2020年10月頃から「クラシルサーバーサイドMTG」と呼ばれる、クラシルのサーバーサイド内で定期的に集まって話しあう取り組みを行っています。

今回はこの取り組みの経緯や取り組み方などについてご紹介します。

経緯

クラシルサーバーサイドMTGを始めた経緯に話すには、まずクラシルの開発体制の変化について説明する必要があります。

開発体制変化の内容や意図などに関して知りたい方は、以下の記事を読んでもらえると分かりやすいかと思います。

blog.tsubotax.com

f:id:jity:20210309140731j:plain

クラシルの開発体制は左の集中型組織での開発から、2020年4月頃に右のSquad体制と呼ばれる職能横断的な開発体制に変化しました。

この体制変化により、元々は職能別に決定されていた座席や目標設定などがSquad別に決定され、業務時間のコミュニケーションの大半はSquad内で行われる形になりました。

これにはプロダクト開発という面で見ると、解決したい課題に対する把握や深い議論でき、チームワークが発揮しやすいというメリットがあると思います。


しかしながら、この開発体制の変化によって発生した課題もあります。

Squad体制化されたことにより座席も離れ、以前より職能別チームとしての感覚が薄れたことで、サーバーサイド同士のコミュニケーションが以前にも増して減ったと自分は感じていました。

これにより、サーバーサイド同士でお互いが何をやっていかを把握しづらくなったり、

各々が抱えている課題を相談しづらくなったりしているのではないかという危機感が芽生えました。

そこで他のサーバーサイドにも聞いてみたところ共感を得られたのでやってみようということで始めた、というのが大まかな経緯です。

開催方法

現状では、サーバーサイドメンバーがそれぞれトピックを持ち寄って、そのトピックについて30分という時間の中で話し合う形で行っています。

持ち寄り方としては、週に1回トピックを投稿するためのSlackスレッドを用意し、そこに書き込んでもらう方式にしてます。

(ただ、この方式だと新しく参画したメンバーが過去のログを追いにくいなど課題が出始めているため、別の方法を検討中です。)

f:id:jity:20210309153535p:plain
スレッドの様子(モザイクばかりになってしまった...)

投稿フォーマットは自由で、情報共有ツールで書いてそのURLを共有するパターンもあれば、スレッドに内容を直接書くようなパターンもあります。

話す順番はスレッドをトピックに書き込んだ順番にしており、

もし全員話しきれなかった場合は、次回は話せなかった人から優先的に話すというルールにしています。

話すトピックに関して

  • 設計・実装など技術的な話題
  • プロダクトの方針や施策的な話題
  • プロジェクトの進め方などの話題
  • その他困っていること・話しておきたいこと

など、トピックに関しては開発やチームに関することであれば何でもOKとしてます。

実際に話されたトピックの例を外に出せる範囲で紹介すると、例えば以下のようなものがありました。

  • 各Squadの今後のロードマップの話
  • 新機能のDB設計で困っている話
  • 参加した技術イベントで得た知見共有
  • 古の実装について有識者に聞く
  • Pull Requestレビュールールに関する相談
  • プライベートでグラフデータベース触ってみた話
  • 発生した障害に対する振り返り
  • リファクタリングの方針相談
  • Railsに追加された新機能について話す
  • など

やってみた所感

始めてから今4〜5ヶ月ほど経ちましたが、新たな課題は発生しつつも今の所うまくいっているように見えます。

以前よりも各々の状況を把握しやすくなりましたし、サーバーサイド内で相談しやすい雰囲気づくりに貢献できてるかなと思ってます。

とはいえ自分の感想だけだと単なる自己満足になってしまうので、他のサーバーサイドメンバーにも感想を聞いてみました。

  • 各自のやっていることが可視化される
  • 各自の課題に感じていることが可視化される
  • 週一で相談できる場があるという安心感
  • サーバーサイドエンジニア同士でコミュニケーションを取る機会になり、それ以外の場面でも相談しやすくなる
  • Squad体制になってから参画した身としてはこのMTGがあったおかげで馴染めた
  • 全員の意見を聞きたい軽い提案や相談がしやすい(全員のスケジュールを別途抑えるのはハードル高いので)
  • ここで各Squadの共有がされる場合が多いので、PRレビュー時のコンテキストの理解がしやすい

新たに生まれた課題

現在クラシルは各種エンジニアを大募集中で、それに伴ってサーバーサイドエンジニアの人数も急拡大しています。

これによって30分という時間だと相談しきれなかったり、MTG話せないメンバーなどもでてきてしまっているなどの問題も発生しています。

かといって単にMTGの時間を延ばせばよいかというと、必然的に30分のときよりも消費コストに対するパフォーマンスが落ちやすくなってしまうため悩ましいところです。

ここに関しては正直まだ模索中で、メンバーと相談しつつ改善できたらよいと思っています。

終わりに

先にも書きましたが、クラシルではサーバーサイドを始めエンジニア・デザイナーを絶賛募集中です!

興味があれば是非以下を覗いてみてもらえると嬉しいです!

join-us.dely.jp

手軽になめらかUI/UXを実現したい〜Material ComponentsのProgressIndicatorを使ってみた〜

ogp

こんにちは。dely開発部にてクラシルのAndroidエンジニアを担当しているnozaです。 月日の流れは早いもので、前回の記事から間があいてしまいましたね。

先月、Material Components for Androidのバージョン1.3.0*1が公開されましたね。 主な内容として下記のComponentが追加されてます。

  • MaterialTimePicker
  • ProgressIndicator

個人的にはProgressIndicatorを待ち望んでいました。 というのも、Android SDKで提供されているProgressBarだと、思った通りに手軽にプログレス表示するのが難しかったからです。 今回はProgressBarのモヤっとポイントと、ProgressIndicatorだとどんなプログレス表示が実現できるのかを紹介していきます。

ProgressBar*2

Android SDKで提供されるもので、標準の設定で円形にしたり水平バーにしたりできます。 画面内のコンテンツを取得するための通信中などに用いることが多いと思います。

Android版クラシルでは、画面内のコンテンツ表示の邪魔をしないようにToolbarの下にバーの形状で設置しようとしました。

f:id:nozakichi:20210303130527g:plain
こんな感じにしたいの図

しかしながら、お手軽に実現できないモヤっとポイントがいくつかあったのです。

ProgressBarのモヤっとポイント

バーの上下の余白

indeterminate=trueの場合、標準の設定で下記のようなアニメーションが実現できます。

f:id:nozakichi:20210302185111g:plain

しかし、このアニメーションで設定されるVectorDrawable(API Level21未満だとpng画像)自体に上下の余白が入っています。 Viewの範囲をわかりやすくするために背景色#ddddddを入れてみると・・・

f:id:nozakichi:20210302223533p:plain

このようにViewの描画領域とバーの間に余白があることがわかります。

意図した場所に配置するためにはこの余白を考慮して工夫する必要がありました。 工夫の例をいくつか挙げてみます。

例1:自前でDrawableを用意して、余白をコントロールする。

標準で用意されているDrawableに余白が含まれているなら、自前で作成してしまえば良いです。 が、VectorDrawableやアニメーションなど用意するものが多く、ある程度知識も必要になるため面倒くさいです。

progressDrawableindeterminateDrawableというattributeで設定可能

例2:上下のマージンに程よいマイナス値を設定してごまかす

<ProgressBar
    style="@style/Widget.AppCompat.ProgressBar.Horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="-6dp"
    android:layout_marginBottom="-6dp"
    android:indeterminate="true" />

marginにマイナス値を設定することで余白分を無くしています。 が、小手先で対応している感じがあってモヤっとします。

例3:layout_heightとscaleYでごまかす

<ProgressBar
    style="@style/Widget.AppCompat.ProgressBar.Horizontal"
    android:layout_width="match_parent"
    android:layout_height="4dp"
    android:indeterminate="true"
    android:scaleY="4" />

高さ4dpにすることで、バーの表示が縮んでしまうのでscaleY="4"で引き伸ばしています。

f:id:nozakichi:20210302224253p:plain

上下に余計な余白は入っていませんが、縮めて引き伸ばしているためボケてしまっています。 かっこ悪いですね。

といった感じで、お手軽でしっくりくる解決ができずモヤっとしていました。

表示/非表示時の切り替え

例えば通信中にはProgressBarを表示し、通信完了したら非表示にしたいと思います。 よくやる方法としてはvisibilityを操作する方法ですが、それだとパッと切り替わってしまうためチカチカした印象になります。

f:id:nozakichi:20210303132739g:plain
通信状態に応じてProgressBarが突然出たり消える例

突然何かが表示されたり消えたりすると潜在的な違和感を与えてしまうので、もっと滑らかに出たり消えたりさせたいものです。 表示状態を切り替える時にアニメーションをうまいこと実行させるのも手ですが、お手軽に実現したいというモヤっと感がありました。

そこで、Material ComponentのProgressIndicatorの出番です。


Material ComponentのProgressIndicator

前述したモヤっと感を、Material ComponentのProgressIndicatorを利用して解決してみましょう。 水平バーで表現できるLinearProgressIndicatorを使います。

 <com.google.android.material.progressindicator.LinearProgressIndicator
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        app:hideAnimationBehavior="outward"
        app:showAnimationBehavior="inward"
        app:indicatorColor="#ffaa4e"
        app:trackColor="#eeebe7" />

上記のxmlで定義したLinearProgressIndicatorをlinearProgressIndicatorという変数名で取得しておきつつ・・・

// 表示したい時に呼び出す
linearProgressIndicator.show()

// 非表示にしたい時に呼び出す
linearProgressIndicator.hide()

※Material Componentの導入*3は省略しています。

f:id:nozakichi:20210303130527g:plain
LinearProgressIndicatorで実装した例

はい、これだけでできました! 滑らかで美しい!

ProgressIndicatorで設定可能なattributes

Material DesignのProgress indicators*4にどんな風に扱えるのか記載がありますが、どんな見た目になるのかやってみましょう。

進捗を表す部分はindicatorColorで、その下地はtrackColorで色を設定できます。

f:id:nozakichi:20210302230200p:plain

また、indicatorColorには色の配列を設定可能です。

<!-- colors.xml に下記を定義 -->
<array name="progress_colors">
    <item>#f00</item>
    <item>#ff0</item>
    <item>#0f0</item>
    <item>#00f</item>
</array>


<com.google.android.material.progressindicator.LinearProgressIndicator
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:indeterminate="true"
    app:indicatorColor="@array/progress_colors" />


indeterminateな水平バーを使用する場合、indeterminateAnimationTypeというattributeで、色配列がどのように適応されるのかを指定できます。

indeterminate
AnimationType
見た目
disjoint f:id:nozakichi:20210303133005g:plain
contiguous f:id:nozakichi:20210303133104g:plain

ただし、"contiguous"という設定を使用するには条件がいくつかあるので注意です。

  • trackCornerRadius(後に説明)は設定不可
  • indicatorColorで設定した色が3色以上
    • 配列が3つ以上でも、色が3色以上である必要がある

※設定を無視してくれればいいのですが、レイアウトxmlの読み込みでエラーになりクラッシュしました。



trackThicknessで高さ(太さ?)を、 trackCornerRadiusで、indicator部分に丸みを設定できます。

"丸み"とはなんなのかは、実際の表示をみてもらうとわかりやすいです。 (よりわかりやすくするためにtrackThicknessを1増してます)

見た目
trackCornerRadius設定なし f:id:nozakichi:20210302231927g:plain
trackCornerRadius設定あり f:id:nozakichi:20210302232021g:plain



表示アニメーション

表示時はapp:hideAnimationBehavior、非表示時はapp:showAnimationBehaviorのattributeで設定できます、 ProgressIndicatorは標準ではnoneが設定されていて、アニメーションがありません。

設定値 非表示時(hide) 表示時(show)
none 標準で設定されているもの。アニメーションしない。 標準で設定されているもの。アニメーションしない。
outward 下端から上端に向かって折りたたまれる 下端から上端まで拡大
inward 上端から下端に向かって折りたたまれる 上端から下端まで拡大

それぞれを設定してみた時の見た目はこんな感じです。

hide show 見た目
outward outward f:id:nozakichi:20210303133150g:plain
outward inward f:id:nozakichi:20210303133303g:plain
inward outward f:id:nozakichi:20210303133327g:plain
inward inward f:id:nozakichi:20210303133215g:plain

ちなみに表示アニメーションが実行された後はvisibilityが変更されます。 show()の後にはView.VISIBLEが、hide()の後にはView.INVISIBLEView.GONEとなりますが、これはsetVisibilityAfterHide(int visibility)というメソッドで設定できます。



アニメーション方向

標準では左から右へ満たされたり流れたりするアニメーションですがapp:indicatorDirectionLinearというattributeで変更可能です。

determinateな場合

indicatorDirectionLinear 見た目
leftToRight f:id:nozakichi:20210303133422g:plain
rightToLeft f:id:nozakichi:20210303133453g:plain

indeterminateな場合

indicatorDirectionLinear 見た目
leftToRight f:id:nozakichi:20210303133523g:plain
rightToLeft f:id:nozakichi:20210303133543g:plain

startToEndendToStartも設定可能だが省略 ※LinearProgressIndicatorだけに用意されたattribute

他にもCircularProgressIndicator用に用意されたattributeもあるのですが、もりもりになってきたのでひとまずここまで。



ProgressBar、ProgressIndicatorの使い所

ProgressIndicatorの紹介をしてきましたが、どんなプログレス表示をしたいかによってProgressBarを使うのか、ProgressIndicatorを使うのかは変わってくると思います。

ProgressIndicatorはMaterialDesignな見た目の表示はお手軽に実装できますが、独自のアニメーション(例えばロゴがグルグル回ったり跳ねたり・・・)を設定するattributeは用意されていません。 凝ったプログレス表示をしたい場合は、Drawableを自前で作成してProgressBarに適応した方が良さそうです。 筆者は今回ProgressBarの中身も覗いてみたのですが、アニメーションの設定とか楽しそうだなと思いました。



おわりに

MaterialComponentのProgressIndicatorについて紹介してきましたが、Android版クラシルでも実際に導入しています。 Android版クラシルでは、読み込み中の表示から通信中、コンテンツ取得後の表示までがなめらかでユーザーの目に優しいものになっていると思います。 それはサービスに大きく関わるような効果ではありませんが、アプリ全体を通じて「安心感」や「温かみ」のようなものをユーザへ与えられているのではないかなと思います。

極端な例を用意してみましたが、どう感じますか?

f:id:nozakichi:20210303133607g:plain f:id:nozakichi:20210303133645g:plain

新しい機能を通じてユーザーに最適で最大の価値を提供する事はもちろん大切なことですが、このような細かなところへの配慮を重ねることでも快適さを実現していきたいですね。

なぜ MVVM + FRP は Elm Architecture に勝てないのか

こんにちは、delyでクラシルiOSアプリ開発を担当している稲見 (@inamiy)です。 この記事は「dely #2 Advent Calendar 2020」の25日目の記事です。

昨日は、delyのSREチームのjoooee0000(高山)さんによる delyのSREチームがオンコールトレーニングを導入する3つの理由 の記事でした。 オンコール対応できるエンジニア、強くてカッコいい・・・

私の方からは、メリークリスマス🎄🎅🔔 にふさわしい Elm Architecture による unidirectional なプレゼントをお届けします🎁

(2020/12/26 EDIT: タイトルを「なぜ MVVM は Elm Architecture に勝てないのか」から「なぜ MVVM + FRP は Elm Architecture に勝てないのか」に変更しました)

iOS開発における MVVM + FRP

2020年現在、多くのiOSアプリ開発の現場では、RxSwift等を用いた関数型リアクティブプログラミング (Functional Reactive Programming, FRP) によるMVVM (Model-View-ViewModel) 設計が主流だと思います。

MVVMは、テストしにくい UIViewController からアプリの表示ロジックを別クラス (ViewModel) に切り出し、複雑なビュー構造(=可変参照と副作用の悪夢)から解放されて、コードの可読性の向上と、テストをよりシンプルに書くための基本的な設計パターンとなっています。

従来のiOS開発では、ViewController〜ViewModel 間のデータのやり取りについて、

  • データの保持に「可変状態 (ViewModel内のvar変数など)」
  • データの送信に「委譲(デリゲート)」や「コールバック」

を使った方法が考えられてきました。

// Before: 従来のViewModel
public class ViewModel {
    // 可変状態
    public var state: String
    
    // コールバック付き関数
    public func doSomething(callback: (String) -> Void) {
        let result = state + " world" // 何か計算する
        callback(result) // コールバックで返す
    }
}

// Before: 従来のViewController
class ViewController: UIViewController {
    let viewModel: ViewModel
    ...
    
    func viewDidLoad() {
        ...
        viewModel.state = "hello" // 1. 状態をセットする
        
        // 2. ビュー側から手動でメソッドを呼び出して、計算結果をコールバックで受け取る
        viewModel.doSomething(callback: { text in
            print(text) // 3. hello world が出力される
        })
    }
}

しかし、FRPの登場によって、これまでの pull方式(状態の更新とコールバックの呼び出しタイミングが異なる2ステップ)から push方式(状態の更新と同時にコールバックが自動的に呼び出される1ステップ)に一変しました。 可変状態として BehaviorRelay (BehaviorSubject)、コールバックとしてそのストリーム機能部分である Observable が用いられるようになりました。

// After: FRPを使ったViewModel
public class ViewModel {
    // 可変状態
    public let state: BehaviorRelay<String>
    
    // ストリーム出力
    public func doSomething() -> Observable<String> {
        return state.asObservable().map { $0 + " world" }
    }
}

// After: FRPを使ったViewController
class ViewController: UIViewController {
    let viewModel: ViewModel
    ...
    
    func viewDidLoad() {
        ...
        
        // 1'. 先に購読(監視)して準備する
        viewModel.doSomething().subscribe(onNext: { text in
            print(text) // 3'. hello world が出力される
        })
        
        // 2'. 状態をセットすると、3が自動的に呼ばれる(同じコールスタック内)
        viewModel.state.accept("hello")
    }
}

一見すると、1と2の順序が逆転しただけに見えますが、After の2' を毎回呼び出す度に、3' 内の購読のクロージャが自動的に呼び出される のに対して、Before 1 の場合は何度呼び出しても、追加で 2 を呼ばないと最新の状態を用いた計算を実行することができません。 つまり、 FRPの購読機能(オブザーバーパターン)によって、メソッドを毎回手動で呼び出す手間が省ける ことがFRPの利点の一つです。

iOSのUI開発においては、ビュー側でViewModel内の表示用データの Observable を購読することによって、データバインディングという形で「1回の購読だけでUIの連続更新が可能」になります。

以降の話では、「iOS開発における MVVM + FRP パターン」をまとめて「MVVM」と呼ぶことにします。

一般的なViewModel

上述の2つのコードは、ViewModel内部の状態がどちらも public なので、外部から直接アクセスできてしまう=将来の状態の予測可能性が簡単に壊される懸念があります。 通常の開発では、内部状態を private に隠蔽して、代わりに PublishRelay 等を用いた入力用のデータフローを追加することが一般的です。

// After 2: FRPを使ったViewModel (状態隠蔽)
public class ViewModel {
    // private な可変状態
    private let state: BehaviorRelay<String> = .init(value: "!!!")
    
    // public な入力
    public let input: PublishRelay<String>
    
    // ストリーム出力
    public func doSomething() -> Observable<String> {
        return input.asObservable()
            // 入力をトリガーに、現在の状態を取得
            .withLatestFrom(state) { input, state in
                return input + "world" + state
            }
    }
}

class ViewController: UIViewController {
    let viewModel: ViewModel
    ...
    
    func viewDidLoad() {
        ...
        
        viewModel.doSomething().subscribe(onNext: { text in
            print(text) // hello world!!! が出力される
        })
        
        viewModel.input.accept("hello")
    }
}

この頻出パターンでは、状態が withLatestFrom 経由で取得された簡易的なデータフローとなっていますが、ここに(UI)アーキテクチャーを考える上での本質情報が隠されています。

それは、ViewModelの基本的な役割が 「入力ストリームと内部状態をもとに、新しいストリームを外部に出力する」 ということです。 「入力」と「内部状態」、そして「出力」という3つの要素は、まさに計算機理論の基礎的モデルであるステートマシン(状態機械)そのもの といえます。

複雑化しすぎたViewModel

しかし残念ながら、多くのiOS開発現場におけるMVVM設計は、このような単純な作りにはなっていません。 もちろん、業務が発展していくに従って、ビジネスロジックが複雑にならざるを得ない事情がありますが、私たちiOS開発者が FRPを過信しすぎて複雑なデータフローを構築してしまう ことにも大きな問題があります。

具体的な例を挙げると、ViewModelはしばしばこのように肥大化しがちです:

// After 2: FRPを使いすぎたViewModel
public class ViewModel {
    // private な可変状態の集まり
    private let state1: BehaviorRelay<String>
    private let state2: BehaviorRelay<String>
    private let state3: BehaviorRelay<String>
    private let state4: BehaviorRelay<String>
    private let state5: BehaviorRelay<String>
    ...
    
    // public な入力ストリームの集まり
    public let input1: PublishRelay<String>
    public let input2: PublishRelay<String>
    public let input3: PublishRelay<String>
    public let input4: PublishRelay<String>
    public let input5: PublishRelay<String>
    ...
    
    // public な出力ストリームの集まり
    public let output1: Observable<String>
    public let output2: Observable<String>
    public let output3: Observable<String>
    public let output4: Observable<String>
    public let output5: Observable<String>
    ...
    
    // 初期化と同時にデータフローのグラフを構築
    public init() {
        // output1 は input1 と state1 に依存
        output1 = input1.asObservable()
            .withLatestFrom(state1) { input, state in
                return input + "world" + state
            }

        // output2 は、input2 と state1, state2, state3 に依存
        output2 = input2.asObservable()
            .withLatestFrom(state1) { ($0, $1) }
            .withLatestFrom(state2) { ($0, $1, $2) }
            .withLatestFrom(state3) { ($0, $1, $2, $3) }
            .flatMap { ... }

        // output3 は、input3、input4 と output2 も使いつつ、state4 に依存
        // 何なら追加で別の副作用も同時に行う
        output3 = Observable.combineLatest(input3, input4, output2)
            .map { ... } 
            .withLatestFrom(state4) { ... }
            .flatMap { ... }
            .do(onNext: { ... })
        
        // output4 は、以下略
        ...
        
        // 結果的に、withLatestFrom, combineLatest等を多用した、
        // 入力と状態が複雑に絡み合うカオスなデータフローのグラフが出来上がる
    }
}

これはあたかも、多層なニューラルネットワークを頑張って一から手書きしているようなものです。 init() 内部のコードがFRPのパイプラインで埋め尽くされ、数百行のコードに膨れ上がることも少なくありません。

このようなFRPの過剰使用とコードの複雑化のことを、個人的に リアクティブ・スパゲティ と呼んでいます。 (もちろん、FRPが存在しなかった頃に比べれば、パイプライン化によって可読性は随分と高まった方なのですが)

なぜリアクティブ・スパゲティは起きるのか

リアクティブ・スパゲティが発生する原因は明確です。 「(いつの間にか)state2input2output2 が生え始めた」 からです。

個々の Observable が存在することは、それぞれがデータフローを増やしてしまう要因になります。 そして、チーム全体を通してFRPをよく理解していないと、簡単にストリームの分岐や合流、余分に追加された状態とその手動ハンドリング(例:disposeBag 以外の Disposable をViewModelが所有している)が大量に発生してしまい、循環的複雑度が爆発的に増加します。

よりコードの可読性を高く保ち、簡潔に書くためには、入力・内部状態・出力それぞれが一本化されてシンプルさを維持しなければなりません。

Elm Architecture

ここで、Elm というプログラミング言語に目を向けてみましょう。

細かい言語仕様についてはここでは触れませんが、フロントエンドエンジニアの方であれば、The Elm Architecture を知っている方も多いと思います。 プログラミング言語全般のUI設計思想に大きな影響を与え、JavaScript Redux、Swift Composable Architecture、Rust Yew、PureScript Halogen、Haskell Miso など、事例を挙げると枚挙に暇がありません。

ざっくり言うと、Elm Architecture は「入力 Msg と現在状態 Model から次の状態 Model を出力する」という基本に忠実な Unidirectional UI設計のことです。 純粋関数である update(または Redux でいう reducer)関数を定義し、プログラム実行の際に使用します。

// Swiftで書いた例
enum Msg { case increment, decrement }
typealias Model = Int

// update : msg -> model -> model
func update(msg: Msg, model: Model) -> Model {
    switch msg {
    case .increment: return model + 1
    case .decrement: return model - 1
    }
}

プログラムの実行については、次のような形の関数を呼び出します:

/*
sandbox :
    { init : model
    , view : model -> Html msg
    , update : msg -> model -> model
    }
    -> Program () model msg
 */
func makeProgram(
    init: Model,                  // 初期状態
    view: Model -> Html<Msg>,     // ビューのレンダリング
    update: (Msg, Model) -> Model // reducer
) -> Program<Model, Msg>

FRP 時代の Elm Architecture (〜v0.16)

ところで、Elm Architecture は v0.17 以前にはFRPを使っていた ことをご存知でしょうか?

Signal と呼ばれる、おおよそ RxSwift.BehaviorRelay (BehaviorSubject) と同じデータ構造を使って、副作用を含むイベントのストリームをパイプライン処理していきます。

そのElm + FRP時代のオペレータの中でも特に有名なのが foldp (fold from the past) と呼ばれる、過去を畳み込む関数です。

// foldp : (a -> state -> state) -> state -> Signal a -> Signal state
func foldp<Input, State>(
    update: (Input, State) -> State, 
    initial: State
) -> (Signal<Input>) -> Signal<State>

「過去を畳み込む」というと、なんだか中二心がくすぐられる思いがしますが、なんてことはない、RxSwift.scan と同じ意味です。

実はこの foldp は、前節の MVVM と同じく、「入力ストリームと内部状態をもとに、外部に新しいストリームを出力する」 という計算のエッセンスが随所に散りばめられています。

  • update: (Input, State) -> State:畳み込み計算 (= reducer)
  • initial: State:初期状態
  • Signal<Input>:単一の入力ストリーム
  • Signal<State>:単一の出力ストリーム

(NOTE: この出力ストリームはその後、 makeProgram 内で view を使って Signal<Html> に変換されて画面に出力されます) (NOTE: (Signal<Input>) -> Signal<State> の部分を Signal Function と呼び、 Arrow と呼ばれる構造を持ちます= Arrowized FRP。これがいわゆる Mealy Machine の話へとつながります)

従来のFRPでは、「入力・内部状態・出力」のエッセンスを実現するために、FRPパイプラインを真面目に実装する必要がありました。 一方で、foldp が教えてくれるのは、 複雑奇怪なパイプラインを作る代わりに update 関数一つを用意 すればそれで済むということです。 これが、Elm v0.17 で A Farewell to FRP になったきっかけとも言えます。

MVVM vs Elm Architecture

それでは早速、MVVM と Elm Architecture を比較していきましょう。

今回は話を簡単にするため、MVVMの場合の入力と出力のストリームがそれぞれ2つずつあると仮定します。 また、各 Observable ストリームは無限時間存在するものとし、 onError / onCompleted を行わないものとします。

すると、Elm Architecture (foldp) によるストリームの一本化の場合、 MVVMのような Observable を複数構成する代わりに、一本化された Observable の型パラメータに入る「状態」と「アクション」の型を複数に細分化する構造を取る ようになります。

// アプリ全体の2アクション
// Action ≅ Action1 + Action2 。足し算はEitherで書くことができる。
enum Action { 
    case action1(Action1) // 子アクション1
    case action2(Action2) // 子アクション2
}
// アプリ全体の2状態
// State ≅ State1 × State2 。掛け算はタプルで書くことができる。
struct State {
    var state1: State1 // 子状態1
    var state2: State2 // 子状態2
}

通常、アクションは enum (直和型)、状態は struct (直積型)を使う場合が多いので、一旦その形にならうものとします。

直和型と直積型については、簡単に言うと、 「代数的データ型 = 型で足し算と掛け算ができる」 というものです。 足し算は Either 型、掛け算はタプル型 だと考えることができます。

代数的データ型 (Algebraic Data Type = ADT) の詳細はこちらをご参考下さい。

ここまでの話を一旦整理すると、

  • MVVM では複数のObservableを入力・出力に持つ
    • 複数を表現するこの場合、掛け算(タプル)で考える
  • Elm (foldp) では、入力・出力に1つずつの Observable のみを使い、その値を代数的データ型で細分化する
MVVM Elm
入力 Obs<Action1> × Obs<Action2> Obs<Action>
≅ Obs<Aciton1 + Action2>
出力 Obs<State1> × Obs<State2> Obs<State>
≅ Obs<State1 × State2>

(Obs は Observableの略)

Observable の代数的性質

ここで天下り式になりますが、 Observable の重要な性質として、以下のことが成り立ちます。 (ここでは、データフロー=川と呼ぶことにします)

Obs<A> = Aが流れる川
Obs<B> = Bが流れる川

Obs<A> × Obs<B> = Aが流れる川と、Bが流れる川

は、一つにまとめて、

Obs<A + B> = AまたはBが流れる川

に置き換えることができる(その逆も成り立つ)

この仮説は直感的にも正しそうに見えますね。 証明は、片方からもう一方に変換するコードを実装できるかどうかで決まります。

// Obs<A> × Obs<B> → Obs<A + B>
func fromMvvmToElm<A, B>(a: Observable<A>, b: Observable<B>) -> Observable<Either<A, B>> {
    return Observable.merge(a.map(Either.left), b.map(Either.right))
}

// Obs<A + B> → Obs<A> × Obs<B>
func fromElmToMvvm<A, B>(aOrB: Observable<Either<A, B>>) -> (Observable<A>, Observable<B>) {
    return (
        aOrB.compactMap { 
            if case let .left(a) = $0 { return a } else { return nil }
        },
        aOrB.compactMap { 
            if case let .right(b) = $0 { return b } else { return nil }
        },
    )
}

この相互の関係性から分かることとして、2つの関数を交互に呼ぶと

  • fromElmToMvvm(fromMvvmToElm(a, b)) = (a, b)
  • fromMvvmToElm(fromElmToMvvm(aOrB)) = aOrB

と、どんな入力値を代入しても元の入力値に戻ります。 この「相互変換して元に戻せる」性質のことを 同型 (isomorphic) といい、

Obs<A> × Obs<B> ≅ Obs<A + B>

と書くことができます(「≅」はイコールではなく、同型を意味します)

結局、何が言いたいかというと、

MVVM Elm
入力 Obs<Action1> × Obs<Action2> Obs<Action>
≅ Obs<Action1 + Action2>

MVVM と Elm の「入力」の構造については、どちらも同じことを言っているに等しい と結論付けることができます。

余談: Observable は単なる「足し算の型の圏」から「掛け算の型の圏」への強モノイダル関手だよ

なお、余談ですが、Observable<Never> についても思いを馳せると、面白いことに気づきます。 Observable<Never> は(今回の前提においては)「無限に続く絶対に値を流さないストリーム」を意味しますが、実際に実装してみると:

let never: Observable<Never> = Observable.create { observer in
    // 何もしない、というかできない
    return Disposables.create()
}

の書き方一通りしかありません。(Observable.never と同じ)

一方で、Swiftの Void (Unit型) もまた () ただ一つのみを値として持ちます。 つまり、 Observable<Never> ≅ Void が成り立ちます。

ここで、おもむろに圏論(数学)という飛び道具を持ち出すと、1 = Void, 0 = Never とおいて、

  • μ: Obs<A> × Obs<B> → Obs<A + B>
  • ε: 1 -> Obs<0>

が同型射(逆方向の関数もある)であることから、Observable が モノイダル圏 (型, +, 0) から (型, ×, 1) への強モノイダル関手 (strong monoidal functor, nLab) であることが分かります。

何を言っているのか良く分からないかもしれませんが、要するに Observable は強かったのです。

出力ストリームの合成の限界

さて、入力に関して MVVM と Elm Architecture の構造は同じであることが分かりました。 それでは、出力についてはどうでしょうか?

MVVM Elm
出力 Obs<State1> × Obs<State2> Obs<State>
≅ Obs<State1 × State2>

結論を先に言ってしまうと、出力は 相互に変換して元に戻すことができません。

試しに、変換関数として次の実装を考えてみましょう。

// 1. Obs<A> × Obs<B> → Obs<A × B>
func fromMvvmToElm<A, B>(a: Observable<A>, b: Observable<B>) -> Observable<(A, B)> {
    return Observable.combineLatest(a, b) { ($0, $1) }
}

// 2. Obs<A × B> → Obs<A> × Obs<B>
func fromElmToMvvm<A, B>(ab: Observable<(A, B)>) -> (Observable<A>, Observable<B>) {
    return (ab.map { $0.0 }, ab.map { $0.1 })
}

一見すると、この対応関係は成り立ちそうに見えますが、残念ながら上手くいきません。 例えば、Rx marble diagram で適当なデータフローを考えてみると:

a  : a0--a1----------a2-->
b  : b0------b1--b2------>

// fromMvvmToElm(a, b)
ab : a0--a1--a1--a1--a2-->
     b0  b0  b1  b2  b2

// fromElmToMvvm(fromMvvmToElm(a, b))
a' : a0--a1--a1--a1--a2-->
b' : b0--b0--b1--b2--b2-->

a != a', b != b' なので元に戻らないことが分かります。

他にも combineLatestzipwithLatestFrom などの他の合成オペレータに置き換えたり、 distinctUntilChanged 等を用いてフィルタリングしてもロジックが複雑化するのみで、同型であることを導くことは困難です。

2.の実装がとても自然なストリーム分解の導出に見える一方、1.の実装で 2つのObservableの(掛け算を使った)合成による不可逆性が発生している と言えます。

この原因の根本について、筆者は次のように考えます:

  • combineLatestzip, withLatestFrom 等を使った Observable の掛け算の合成は、(時間的同期を取るために)内部で発行した値を一部メモリキャッシュするという「副作用」が発生し、これがFRPのストリームの計算結果に対しても不可逆性を生じさせている

もし、この仮説が正しいとするなら、次の点についても言えそうです:

  • fromElmToMvvm は自然な導出(純粋な map のみを使っているので)
    • Elm Architecture から MVVM への出力の変換は容易
    • MVVM への出力変換で、各々のストリームが前回の値を重複して発行してしまう問題があるが、distinctUntilChanged を使えば、MVVMのように差分更新のみを抽出することも可能
  • fromMvvmToElm は、どのような掛け算的合成を行っても、不可逆な結果に終わる
    • MVVMからElmを構成することはできない

Q. combineLatest を使えば、全ての出力をかき集められるのでは?

ところで、上述の 「MVVMからElmを構成することはできない」は、やや飛躍した結論に思われるかもしれません。 実際、数学がどうこうという謎めいた話を無視すれば、combineLatest を使って散らばった各出力の Observable を一点に集めることが可能だからです。

しかし、この単純な方法は「限られたケース」においてのみ可能であるだけで、一般的には成立しませんし、また非効率的です。 主に、次のような課題があります。

  • combineLatest 引数の各 Observable は subscribe 時点で 初期値を持っていなければならない
    • 初期値がないと、combineLatestonNext がなかなか始まらず、Elmにおける状態更新のタイミングを再現しない
  • combineLatest は、掛け算的合流計算のために メモリキャッシュを消費 し、Elm に比べて非効率になりやすい
  • Reactive glitch(同じ上流元の同期的合流問題)
    • 2つの異なる出力が、同じ1つの入力をトリガーとして派生した場合、タイミング問題が生じて、中間の変更状態が反映される

ちなみに Reactive glitch の根本問題は、 2つの出力が「同時に更新」される場合に、個々の Observable に分解できない ことが原因です:

// aとbが両方同時に更新
ab : a0------a1-->
     b0      b1

// fromElmToMVVM(ab)
a  : a0------a1-->
b  : b0------b1-->

// fromMvvmToElm(fromElmToMvvm(ab))
ab': a0-----a1-a1-->
     b0     b0 b1

一瞬だけ (a1, b0) (場合によっては (a0, b1))という余計な中間状態が発生している ことが分かります。 この問題点として、もし ab' をVirtual DOMに渡してUI差分レンダリングした場合、不要な計算が走ることにつながります。

Reactive glitch に対する解決策としては、FRPの中で トポロジカルソート を用いたQueueによる管理の方法が挙げられます。 詳しくは、こちらのURLをご参考下さい。

なお、RxSwift や ReactiveSwift を始めとする、ほとんどのFRPライブラリは、Reactive glitch 問題に対応していません。 もし対応しているフレームワークがあれば、ぜひ教えて下さい。

まとめ

いかがでしたか?

この記事では、MVVMに対するElm Architectureの優位性について、FRP (Observable) の持つ数学的構造 に着目して仮説を立ててみました。 Virtual DOM (差分レンダリング) フレームワークの有無や良し悪しに関係なく、結論を導ける という点が、この話の一番の面白い点だと思います。

さらに学びたい方のために、Swiftで解説した Elm Architecture について、こちらのスライドをご参考いただければ幸いです。

今回のブログを書くにあたり、参考にした文献・Webサイトはこちらになります。

おわりに

delyでは現在、 クラシル」「TRILL」を一緒に開発しながら共に成長していけるメンバーを絶賛大募集 しています。

join-us.dely.jp

もし、この記事を読んで「私も strong (monoidal) になりたい」と思いましたら、ぜひ私が先日書いた入社ブログも合わせて読んでみて下さい。 会社のカルチャーや中の人の雰囲気、事業内容について紹介しています。

note.com

また、2021/01/21 19:00〜にクラシルiOSチームのオンライン雑談会を開催します。 こちらもぜひ奮ってご参加ください!

bethesun.connpass.com

最後までお読みいただきありがとうございました!

delyのSREチームがオンコールトレーニングを導入する3つの理由

f:id:joe0000:20201223092159p:plain

こんにちは!

AWSのカオスエンジニアリングの新サービスもリリースされ、オンコールトレーニングへの関心が高まっているのを感じています。delyのSREチームのjoooee0000(高山)と申します。

この記事は「dely #2 Advent Calendar 2020 - Adventar」の24日目の記事です。

昨日は新規事業開発をしている おっくー (@okutaku0507) さんによる 「KPI自動通知Botで始める 数字に執着するプロダクトマネジメント|奥原拓也 / クラシルPdM|note」でした。 KPIの必要性から具体的なBot化の知識まで具体的に解説されているのでぜひ参考にしてみてください!

note.com

adventar.org

adventar.org

はじめに

今回は、delyのSREチームがオンコールトレーニングを導入する3つの理由を紹介したいと思います。

delyのSREチームはこれまで、元々在籍していたメンバーと、2019年の10月に入社したメンバー2人体制で運用してきました。そして今年(2020年)の10月から、新しくSREとして入社したメンバーと育休から帰ってきたメンバー(育休前はサーバーサイド担当)の2人を足して4人体制になりました。現在、4人になった新生SREチームをチームとして再スタートさせるべく、チームミッションの再定義や役割スコープの決定、チーム体制の整備を行なっています。

その中で、delyのSREチームはオンコールトレーニングの導入を決定しました。なぜ導入することになったかを3つの項目に分けて紹介していきたいと思います。

(なお、この記事のオンコールトレーニングがさす具体的なトレーニングの内容はSRE本の28章 「SREの成長を加速する方法: 新人からオンコール担当、そしてその先へ」で紹介されているトレーニング内容をベースとしたものです)

オンコールトレーニングを導入する3つの理由

1. オンコール体制強化が急務

まずはシンプルに現状の体制でのオンコール対応に限界があるためです。

現在は元々在籍していたシニアエンジニアがメインでオンコール対応をしており、他のメンバーは補助という形でオンコール対応にあたっています。

そのため、メイン担当者がオンコール対応において単一障害点となってしまっています。さらに、delyのSREチームが運用を担当しているサービスはレシピ動画サービスの「クラシル」に加え、最近GCPからAWSへのリプレースを行なった「TRILL」という女性向けメディアも追加され2つになりました。なので、特に障害発生タイミングが被った場合に地獄がおこります。同じAWSのサービスを使っているため、AWS障害時などには容易におこりうることが想像できます。

f:id:joe0000:20201223224036p:plain

「はい!その通りなのですが....」

delyのSREチームのメンバーは比較的新しいメンバーで構成されていることに加えて、サーバーサイドチームから異動したメンバーや、前職でがっつりオンコール対応をしていたわけではないメンバーも在籍しています。

そのような状況で、他のメンバーがオンコールのメイン対応を担えるようになるにあたり以下のようなハードルがありました。

オンコールのメイン担当者になるための3つのハードル

⑴ 新しいメンバーがオンコール対応をぶっつけ本番でやるのは危険

delyのSREチームが運用を担当している2つのサービスは、それぞれ月間利用者数千万人を超える巨大サービスです。 障害によりサービスが停止した際には大きな損害が出てしまうため、新しいメンバーがぶっつけ本番で対応するのは危険です。

⑵ シニアSREが優秀すぎて他のメンバーが実績を積む機会がない

この現象はオンコール対応にはよく発生する問題のようです。システム障害対応についての本にはこのようなオンコール対応の教育に関する問題について言及されています。

システム障害は、多くのシステムにおいて最優先の対応事項です。そのため、障害対応メンバーについては、社内で最もできる人間がアサインされます。あなたよりも経験が長いメンバーがいるのであれば、おそらく、あなたが障害対応を指揮する「インシデントコマンダー」になることはないでしょう。このような現場の場合、あなたはいつ経験を積むのでしょうか?それは、他にできる人間がいないときです。

(木村 誠明. システム障害対応の教科書.より)

これでは、メイン担当者の負荷が一向に減らず、永遠に単一障害点になり続けます。

⑶ 通常のプロジェクト業務だけではシステム全体像把握の効率が悪い/把握度が属人的になる

オンコール対応にはサービスが稼働しているシステムについての知識が必要になります。業務をこなしていく中でだんだんとシステムに関する深い知識が身についていくものだと思います。

しかし、アサインされたプロジェクトによってどこに知識がついていくか偏りが出てしまいます。 例えば、現在検証環境周りの再構築をやっているのですが、そこにがっつり関わっているメンバーは本番環境システム側に触れる機会が相対的に少なくなってしまいます。

また、自主的なリバースエンジニアリングや自主学習を通してシステムを理解することもできますが、自学のためどれほどの習熟度なのか判断する材料がないこと(どの程度の習熟度でオンコール対応できるという判断基準をもうけられない)と、効率が悪いことが問題としてあります。


この3つのハードルを越えるために、GoogleのSRE本を元にオンコールトレーニング導入を検討しました。

そこで疑問なのは、「そもそもオンコールトレーニングってどれくらいの効果があるの?どれくらいのチーム規模で導入するべきなの?」という話です。NetflixがオンコールトレーニングをやってAWSの障害に強かった話やGoogleのSREチームがオンコールトレーニングを導入している話などは有名ですが、あの規模だからできたことなのではと考えてしまいます。

しかし、Googleが出しているサイトリライアビリティーワークブックには、delyのチーム構成に似たオンコールトレーニングにおけるGoogleのモデルケースが存在していました。これは直接的な導入の理由になるわけではありませんが、似たチーム構成で成功例が存在していることは導入を後押しする理由になりました。

こちらはサイトリライアビリティワークブックのオンコール対応の章で紹介されていたモデルケースです。

マウンテンビューの Google SREチームの事例

<チーム構成>

● SREテックリードのSara

● 他のSREチームから来た経験豊富なSREのMike

● SREになるのは初めてのプロダクト開発チームからの異動者

● 4人の新規採用者(「Nooglers」)


<ストーリー>

数年前、マウンテンビューのGoogle SREのSara は、3 ヶ月以内にオンコールにならなければならないSRE チームを立ち上げました。

----中略----

チームが成熟していても、新しいサービスのオンコールになるのは挑戦であり、新しいマウンテンビューのSREチームは相対的に若いチームです。にもかかわらず、この新しいチームはサービスの品質やプロジェクトの速度を犠牲にすることなくサービスをオンボードできました。彼らは即座にサービスに改善を加えましたが、その中にはマシンのコストを40%下げたり、リリースのロールアウトをカナリアやその他の安全性チェックと合わせて完全に自動化したりといったことが含まれていました。新チームは 99.98%の可用性、言い換えれば四半期ごとにおよそ 26 分のダウンタイムをターゲットに、信頼性あるサービスを提供し続けてもいました。 新SREチームは、これほどのことをどのように自分たちで達成したのでしょうか?それは、スタータープロジェクト、メンタリング、トレーニングを通じてでした。

(サイトリライアビリティワークブック P149)

このチームは、オンコールトレーニングを実施することでたくさんのシステム変更を加えているにも関わらずSLOを達成し、サービスの信頼性を担保しています。そしてそれは、オンコールトレーニングによって成し遂げられたことということです。

また、この事例において具体的にどのようなトレーニングをおこなったかについても本に記載されています。

このような背景と最後の後押しが、オンコールトレーニングをdelyのSREチームに導入する意思決定につながりました。

2. SREの運用改善業務のアウトプット最大化

オンコールトレーニングは、シンプルにオンコール対応ができるようになることに加えて別の役割も果たすと考えています。それは、SREの通常業務をする上でも非常に重要な知識が習得できるという点です。

delyのSREチームの本来注力するべき業務はシステムの設計と運用の改善にあります。

改善には、下記のような工程が存在します。

①課題を発見する

②課題に適切な優先順位をつける

③適切な解決策を考えて実行する

ここで重要なことは、①と②の工程ができていなければ、どんなに③が優れていても影響は小さいということです。

よって、改善業務において①と②の工程がとても大事になってきます。つまり、チームのアウトプットを最大化するには、まず課題発見と優先順位づけの精度を高めていく必要があります。

そこで課題となってくるのが、delyのSREチームは比較的新しいメンバーが多いためまだシステムに対する知識にムラがあることです。特にシステムの全体を俯瞰して課題の優先順位づけを行うことにはまだハードルがあります。また、システムに内在する既知の課題を網羅的に把握する機会がありません。全員の課題発見のスピードを上げ、優先順位づけを行えるようになることで①と②の質を高めていく必要があります。

オンコールトレーニングを行うことにより課題発見と優先順位づけをする上で重要な知識が身につくと考えています。

オンコールトレーニングにはいくつかの種類ややり方が存在しています。それを大別すると、システムについての網羅的な学習障害をベースにした学習 に分類できます。

網羅的な学習

課題を発見するには、そもそもシステムについて知る必要があります。システムについて学習をすることにより、課題の発見/理解がしやすくなります。また、網羅的な学習により全体像が見えることで課題の優先順位づけの精度を上げることができます。

障害ベースの学習

障害ベースの学習を行うことにより、システムの現状の弱点を把握することができます。「システムの現状の弱点 = 課題」です。障害ベースの学習を行うことで、現状潜んでいる課題を知ることができます。

このように、オンコールトレーニングをすることが課題の発見と優先順位づけをスムーズにできる手助けをしてくれます。結果的に、オンコールトレーニングをすることでSREチームが出すアウトプットの価値を上げることにつながると考えています。

3. 新規社員がスムーズに戦力になれる仕組み作り

これまで、オンコールトレーニングを整備するとオンコール対応強化やSREの運用改善業務をする上でメリットがあるという話をしてきました。それにより、今後入社するメンバーにとってもスムーズに戦力になれる環境が整います。

さらに、新規社員を迎えるにあたりもう一つオンコールトレーニングを行うメリットがあります。それは、システムに関するドキュメントが豊富になるという点です。

delyにはもともと、在籍日数や立場に関係なく誰でも 活躍することができるうように情報の透明性を高くし、情報の非対称性をなくすという文化があります。

オンコールトレーニングで習得した知識をメンバーが ドキュメンテーションすることで現状のシステムの構成やモニタリング、ロギング、ロールバックの手順など豊富な知識が可視化されることになります。現状もドキュメントはかなり豊富なほうですが、オンコールトレーニングの導入によりさらに豊富になっていく予定です。これは、SREチームだけではなくサーバーサイドチームなど他のチームにとってもいい影響を及ぼすことが考えられます。

オンコールトレーニングを導入することで、入社後の戦力になるスピードを上げることができ、さらにドキュメントがそろうことでストレスなく業務にあたる環境作りが望めます。

まとめ

これらより、delyのSREチームではオンコールトレーニングを導入することになりました。オンコールトレーニングをすることはオンコール対応ができるようになるだけではなく様々なメリットがあります。

また、大規模なチームではなくても導入をすることができると考えています。 導入の仕方や実際になにをやっているかなどは今後のブログでまた共有していこと思います!

delyではSREメンバーを募集しています。@gomesuit @bababachi @akngo22 など愉快なメンバーが揃っていますのでご応募お待ちしております!

f:id:joe0000:20201223224305p:plain

join-us.dely.jp

また、「クラシル Tech Talk」などのイベントも多数行っています。 エントリー前に開発部の様子を知りたいという方はぜひ覗いてみてください。ご応募をお待ちしております!

bethesun.connpass.com

クラシルの新規事業を支える検索機能!Elasticsearchの導入と運用のポイント

こんにちは、delyコマース事業部サーバーサイドエンジニアの小川です。

最近クラシルにて、ネットスーパー機能のリリースができました! (以下 クラシルネットスーパー)

入社して1年くらいたちますが、とってもエキサイティングな毎日を過ごしています。

この記事は「dely #1 Advent Calendar 2020 - Adventar」の24日目の記事です。

前日は仁多見さんの記事でした!↓

思った以上に大変だったクラシルでの Scoped Storage 対応 - クラシル開発ブログ

はじめに

みなさんはElasticsearchを利用して、開発中のサービスに検索機能を導入したことはありますでしょうか。 今や様々なサービスで利用されているかと思います。

クラシルネットスーパーでは、キーワード検索以外の部分でもElasticsearchを活用しています。

レシピに紐づく食材を取得したり

f:id:te_o:20201222145500p:plain

商品を分類・識別したり

f:id:te_o:20201222145753p:plain

普通だったら、玉ねぎカテゴリに玉ねぎの商品を紐付ける運用をすると思いますが、 ネットスーパーの商品は1店舗当たり何万件も商品があり、それが日々入れ替わるので上記の運用が現実的ではありません。

商品の分類・識別の自動化が必須だったので、それを今回は自然言語ベースで行いました。 分類・識別や一般的なキーワード検索のどちらも行える、Elasticsearchがとても相性よかったです。

記事メディアとはまた違った検索を提供する特徴的な事例だと思いますが、 検索エンジンの最適化はコツコツとやっていくことがマストです。

最適化についてどんな機能があるか、また運用のポイントなどをまとめていこうと思います。

  • Elasticsearchについて基本だけは知っているけど使ったことない方々
  • 検索最適化したいけどやり方がわからない方々

の参考になれば幸いです!

最適化の際に必要なElasticsearchの知識

スコアリングの式を知る

Elasticsearchは特定のクエリに対して、類似度を測りスコアリングしています。 スコアリングは主に、tf-idf を利用して算出されています。

f:id:te_o:20201223111011p:plain

参考:https://lucene.apache.org/core/8_6_0/core/org/apache/lucene/search/similarities/TFIDFSimilarity.html

q がクエリ、d がドキュメント、 t がターム(ドキュメントの内容を形態素解析した得られた単語)として見てみましょう。

根拠を持って検索最適化を行うためには、スコアリングの式についての理解が必須です。

同義語について知る

Elasticsearchでは「同義語」をanalyzerに設定することができます。

analyzerの概念図 f:id:te_o:20201221183334p:plain

例えば、表記揺れやタイポなどは同義語として設定し、同じものとみなす独自のanalyzerを作成することができます。 例 (トマト,とまと,tomato)など

同義語はanalyzerの概念図のToken Filtersのレイヤーに属します。

以下の図は、同義語の概念について表しています。

内部のユースケースは、外部のユースケースより単純です。 外部に行くにつれ複雑になりますが、検索エンジンとしての洗練度・パワーが増します。

様々な「同一性」に対して同義語を使うことは不正解ではありませんが、 これらを理解せずに無作為に同義語の設定を行うと、予期せぬ挙動をする検索エンジンが爆誕します。

同義語はとても強力です。 うまく使いこなせば、様々なユースケースに対応できる検索エンジンができるでしょう。 しかしうまく使いこなせないと、気付いた時には運用の難易度がとても高い検索エンジンが誕生してしまうかもしれません。

参考:https://opensourceconnections.com/blog/2018/12/07/synonyms-by-any-other-name-part-1/

さらに精度を高めたい

他にもfunctionクエリやword2vecなどを組み合わせて最適化を行うことができます。 実は画像検索でさえできてしまいます。

詳しい説明は省きますが、そう言った手段もあることを把握しておくだけでとても有益です。 個人的にはword2vecについて興味を持っていて、より有益に使えないかなとソワソワしています。手段の目的化はヨクナイ

最適化の運用・設計のポイント

検索最適化の正解を明確にすること

これは意外と大切です。最初にざっくりでいいので、「想定されるクエリ」と「クエリに対する理想の検索結果」を整理しておきましょう。

例えばネットスーパーで言うと、

ユーザーは「トマト 2個入り」や「トマト 〇〇県産」とは検索しないと想定します。 おそらく「トマト」と検索して出た結果の中から、目当ての「トマト」を選択してカートに入れるでしょう。

となったとき、トマトに付随する「2個入り」や「〇〇県産」と言う単語は、無視できる事になります。 余計な単語は無視することが検索精度向上には大切です。

また、「トマト」と検索した時は、「トマトジュース」や「トマトケチャップ」が結果に出ることは望ましくありません。 「トマトジュース」が欲しいユーザーは「トマトジュース」と検索するからです。 何も最適化せずにElasticsearchを使うと、「トマト」で「トマトジュース」なども出てきてしまいそうですね。

以下のように検索最適化の正解を明確にすることが大切です。

  • 想定されるクエリ:トマト
  • クエリに対する理想の検索結果:原体のトマト(トマトジュースや、ケチャップはでない)

それにより、

  • 「2個入り」や「〇〇県産」と言う単語は、無視できる。
  • トマトジュースなどが出てこないようにするにはどういったらいいか

と言う事項を初めから考慮に入れられるようになります。

最適化の作業の難易度・影響範囲・作業量のバランス

検索最適化の正解が明確になって初めて、最適化の作業が始まります。

  • 想定されるクエリ:トマト
  • クエリに対する理想の検索結果:原体のトマト(トマトジュースや、ケチャップはでない)

を達成するためにたくさんのアプローチがあると思います。最適化の手法に正解はありません。

どのアプローチを選択するかの判断基準として、「難易度・影響範囲・作業量のバランス」を見ると良いです。

アプローチ①

f:id:te_o:20201222152346p:plain

プロダクトの初期においては必要なアプローチかと思います。

例えば、

  • 検索クエリが複数渡された場合、or検索だったのをand検索にする
  • タイトルとディスクリプションでスコア計算の重みが同じだったのをタイトル重めに調整する
  • ストップワードを追加する

などです。

初期はリソースが少ないかつ、影響範囲についてあまり考えなくても良い場面(リリース前など)があると思います。

100点の検索精度ではなく、70~80点くらいの検索精度を目指せれば良いかなと個人的に思います。

アプローチ②

f:id:te_o:20201222162211p:plain

アプローチ①の手法でだいたいの精度が出てきたら、移行して良いと思います。

1つ例をあげると、前述した同義語の概念のうち、Alt Labelsレベルの同義語の登録であれば、誰でもできるかと思います。 運用や設計次第ではもっと高レベルの最適化を専門知識なしに行えるかもしれません。

ここで大切なのは「いかに影響範囲を狭くできるか」になります。

チューニングするたびに現状の検索結果に影響してしまうと、 一方の精度は伸びるけど一方は落ちてしまうなんていうことになりかねません。

着実に70~80点から100点に近づけていける運用が組めると良いと思います。

結局は

プロダクトのフェーズ・特性・リソースなど様々な面から選択していくと良いと思います。

個人的には、「特定の分野における自然言語は限られている」かつ「長期的運用が必要なものが属人化するとヤバイ」と考えているので、 よほどの事がない限り、早い段階で作業内容がストックする前提で、アプローチ②が行えるような設計・運用を試みます。

最後に

以上Elasticsearchのポイントについてざっとまとめてみました。

クラシルネットスーパーでは、割と綺麗な運用で色々な要件を達成できました。 ネットースーパーの商品に手作業でラベルをつけなくても、分類・識別できるようになるのは大きいです。

技術に使われるのはなく、技術を使いこなすことが大切ですね。

レシピ連携機能を作る時、検索について何も知らない自分が弊社CTOに、 「レシピの食材とネットスーパーの商品を手作業で紐付けてくしかない」と言った時に、「ぼくたちがこれをトマトだと判別するのもアルゴリズムだから」と言われて一蹴されたのはとても昔に感じます。

明日はそのCTOである、たけさんの記事です。 お楽しみに!

delyではエンジニアの採用をひたすらやっていますので、興味ある方はぜひのぞいてみてください!

join-us.dely.jp

bethesun.connpass.com