Quantcast
Channel: クックパッド開発者ブログ
Viewing all 802 articles
Browse latest View live

クックパッド基幹システムのmicroservices化戦略 〜お台場プロジェクト1年半の軌跡〜

$
0
0

インフラストラクチャー部の青木峰郎です。 最近はDWH運用の傍ら、所属とまったく関係のないサービス開発のためのデザインスプリントをしつつ、 Java 10でgRPCサーバーを書きつつ、 リアクティブプログラミングを使った非同期オーケストレーション層を勢いだけで導入したりしています。

ですが今日はそれとはあまり関係なく、クックパッドの中核サービスであるレシピサービスの アーキテクチャ改善プロジェクト、「お台場プロジェクト」の戦略について話します。

これまで、お台場プロジェクトで行った施策について対外的に発表したことはあっても、 全体戦略について話したことはありませんでした。 その一番の理由は、正直に言って、プロジェクトオーナーであるわたしにもプロジェクト全体の姿が見えていなかったからです。 しかし現在プロジェクト開始から1年半が経過してようやく全貌が見えてきたので、すべてをお話ししようと思います。

クックパッドの「本体」システム

クックパッドは現在では大小様々なサービスをリリースしています。 しかしその中でも最初期から存在し、 現在でもあらゆる意味で中核にあるサービスがいわゆる「クックパッド」、 社内では「本体」や「cookpad_all」さらに略して「all」などと呼ばれているレシピサービスです。

このレシピサービスは世界最大のモノリシックなRuby on Railsサービスであり、 いま手元で適当に数えただけでRubyのコードが27万行(テストを除く)、 テストが51万行、HTMLテンプレートが14万行あります。 このコード量でウェブサービスのcookpad、APIサーバーのpantry、 バッチのkuroko、管理画面のpapaの4アプリケーションを主に実装しています。

お台場プロジェクトとは

そして、この巨大な「本体」システムのアーキテクチャを根本から刷新し、 改善するプロジェクトが「お台場プロジェクト」です。 わたしがこのお台場プロジェクトを開始したのは去年(2017年)のバレンタインデー、2月14日のことでした。 そのときやったことは「とりあえず改善したいことをリストアップする場所」としてGitHubにレポジトリを作っただけでしたが、 その後にお台場プロジェクトは技術部の注力課題に昇格してメンバーも大幅に増員され、1年半が経過しました。

約30万行というコードサイズは世間一般で言えば超巨大とは言えないでしょうが、 少なくとも容易なプロジェクトではないことは確かです。 2017年2月の時点では、このプロジェクトを達成できると言う人も、俺がやると言う人も社内にはいませんでした。 むしろ、本体には関わりたくない、できるだけ触りたくない、 最低限の機能追加以外の余計なことはしたくないという雰囲気が充満していたように思います。

お台場プロジェクトが目指すもの

お台場プロジェクトの目的は、「本体」にも大規模な機能追加・変更ができるようにすること、 さらにその大規模な変更をできるだけ少ない時間でできるようにすることです。 逆に言うと、プロジェクト開始時点では、思い切った機能追加はできなかったということになります。

大規模な変更を行えない技術的な理由としては以下のような点が挙げられます。

  • コードを変更すると意図しないところが壊れる。例えばウェブサービスをいじるとガラケーの認証が壊れる。
  • ライブラリが古かったとしても依存が多すぎて気軽に更新できない。
  • 実行環境が非常に複雑かつ特殊で、迂闊にデータベースを追加したりできない。
  • 普通のツールが動かない。例えばコードカバレージが取れない、並列テストが動かない。
  • ObjectクラスやStringクラスのような非常に基本的なクラスが改変されており、普通の動きをしない。

また、組織的・プロセス的な理由もあります。

  • あるコードのオーナーが誰かわからない。例えばuserリソースのAPIを変更したくても誰にも相談できない。
  • GitHubのissue・pull requestが多すぎてとても全部は見ていられない。通知も何も機能しない。
  • 「本体」をいじる開発者が多すぎて、改善系のpull requestを作ると頻繁にコンフリクトする。

「本体」で大きな変更を行おうと思うと、これらの問題がすべて同時に襲いかかってきます。 例えばI/Oの激しいシステムを追加するためにDynamoDBを使いたいと思ったとしても、 AWS-SDKのバージョンが古いのでまずはSDKのバージョンを更新するのに1ヶ月かかる、 テストが遅いので検証にも時間がかかり、そのあいだに別のpullreqがマージされてコンフリクト、 実装を進めていくと既存のクラスに変更が必要そうなことがわかってきたがオーナーが誰かはわからない、 がんばって実装してみたが触ってもいないバッチのCIが通らない、 ようやく理由がわかって直してデプロイしたらなぜかガラケーサイトが落ちた……という具合です。

これはさすがに誇張だろうと思われるかもしれませんが、残念ながらすべて事実を元にした話です。 こんな開発を続けていれば、大きな機能を追加しようという気がなくなって当然でしょう。

タスクの優先順位の決定

こんな状態ですから、なんらかの改善をしなければいけないことはわかります。 しかし、はたして何から手を付けたらよいものでしょうか。 まずは問題をリストアップしてはみたのですが、あまりにも問題が多すぎ粒度もバラバラ、しかもどれも大変そうで腰が引けます。 タスクを絞るところが肝であることはどう考えても明らかでした。 そこで、ビジネス上の価値と政治的判断を加味しつつ、「やること」「やらないこと」を次のようにエイヤと決めました。

やること:

  • APIサーバー(pantry)のアーキテクチャ改善
  • 不要なサービスの廃止
  • デッドコードの自動検出と削除
  • ストレージ数の削減
  • 特殊な実行環境・開発環境の廃止

やらないこと:

  • ウェブサービス(cookpad)のビュー改善
  • Railsのメジャーバージョンアップ
  • 細かい実装レベルの改善

最初に決めなかったこと:

  • 全面的にmicroservice化するかどうか

まず、ユーザー数と有料会員数の分布、将来向かう方向を考えると、 ウェブやガラケーよりもスマホアプリのAPIサーバー(pantry)が重要であることは明らかなので、 そのアーキテクチャ改善につながらないタスクは原則捨てると決めました。 そうすると例えばウェブのビューの実装改善などは自動的に「やらないこと」になります。

microservices化はギリギリまで先送り

一方で、先に述べたように、本体のコードはオーナーがよくわかりませんし、 最初からmicroservices化するとは決断できなかったので、 いきなりそこに大々的に踏み込んでいくことはしませんでした。 例外は検索システム1つだけです。

最終的にコードをなにかしら分割することは避けられないとは思っていましたが、 サービスとして分割する以外にも、たとえばRails engineとして分割するなど代替案はいくつかあります。 とにかく戦略的に、大々的に分割するぞーと宣言して分割はしたけど失敗でしたという事態だけは絶対に避けたかったので、 どうしても分割を避けられない時が来るまで決断を遅らせることにしたわけです。

これほど慎重になった背景にはもちろん理由があります。 実は、クックパッドでは2015年から2016年くらいにすでに一度microservices化を試して失敗した経験があるのです。

当時はmicroservices化の機運が社内で盛り上がっており、 今後「本体」に機能を追加するときは必ずmicroservicesに分けようということになりました。 しかしこのときの経過が非常にまずくて、業務プロセスを複数のアプリに分断してしまって作業が増えたり、 管理アプリだけ別サービスに分割したことによってひたすら新規APIを作るはめになったりと、 microservices化全般に悪い印象だけが残ってしまったのです。 後者の管理アプリに至っては最終的に「本体」の管理アプリに統合され、microservicesではなくなってしまいました。

この時の経験があったため、最初からシステム分割を行うのはできるだけ避けることにしました。 最初に社内のエンジニアにお台場プロジェクトの話をしたときも、 microservices化に反対する意見が出た場合に備えて想定問答集を作ったほどです。 もっとも実際に話してみると、当時とは社内外の状況が変わったこともあって、反発はほとんどありませんでした。

f:id:mineroaoki:20181228000116j:plain
お台場プロジェクト発表時の社内の様子

ちなみにそのときの写真がこれで、わたしが用意した大変わかりやすいスライドで成田CTOが喋っているの図です。 お台場プロジェクトが終わってからプレスリリースを打つときに使おうと思っていた秘蔵写真ですが、 いい機会なので公開します。

最初は確実に成果を出せるコード削除を実施

いきなりサービス分割をしない代わりにまずやったタスクが、古いサービスの廃止と、古いAPIサーバーの廃止です。 ほぼ誰も使っていない機能の廃止はトレードオフがほぼないので、どう考えてもやったほうが得であり、 しかも誰も反対しないからです。まず最初にわかりやすい成果を作るために最適のタスクでした。 社内に効果を示すためにも、チームが自信を得るためにも、 比較的簡単にできて結果のわかりやすいタスクから始めるのは妥当でしょう。 時期的にもちょうど大きな機能が分社されて消せるコードがたくさんありました。

また、今後コードを消していくうえで、 本番で使われていないコード(デッドコード)がツールで自動判定できたら非常に楽ができるので、 その方法を少し調べて実施することにしました。 その結果できあがったのがRubyのLazy Loadingを使って実行されないコードを探す手法です。 現在ではこのシステムによって自動的に不要コードを検知できるようになっています。 さらに、このデッドコード検出機能はブラッシュアップされてRuby 2.6にも取り込まれました

「Railsのメジャーバージョンアップはしない」

Railsのメジャーバージョンアップもお台場プロジェクトではやらないと最初に決めました。 これは単純に「Railsをバージョンアップしたところでアーキテクチャの根本改善にはつながらない」 という理由もありますが、その他に一種のシンボル的な意味もあります。

この点を説明するには、まず少しだけクックパッドの組織構造の話をする必要があります。 クックパッドでは永らく、 「本体」のソフトウェアアーキテクチャ(主にRails)については技術部の開発基盤というグループが責任を持ち、 それより上の機能については各事業部が分割して持つという責任分担が行われてきました。 結果として2016年までの数年は、開発基盤グループに新しい人が入るととりあえず 「本体」のRailsバージョンアップをするというのが洗礼の儀式のように行われていたのです。 しかしこのタスクが技術的にも政治的にも非常につらく、 結果として若者が「本体」に対するヘイトをためていく構造になっていました。

そういった歴史の結果として、クックパッドにおいては「Railsのバージョンアップ」というタスクが 「これまでの開発基盤の役割」とほぼ同じ意味を持っています。 そんな状況で、開発基盤で新しいプロジェクトを始めます、 それじゃあまずRailsバージョンアップをやりますと言ったら、いままでと何も変わりません。 中身は同じで名前だけ変えたんですねということになりかねないからです。

お台場プロジェクトはシステムアーキテクチャの改善プロジェクトであると同時に、 組織アーキテクチャの改善プロジェクトでもあります。 巨大な1つのシステムをメンテするのはもはや手に余るので分割統治しよう、 というのがお台場プロジェクトの目的ですから、組織もいまのままであるわけがありません。 具体的には、開発基盤グループが不要にならなければいけません。

つまりある意味で開発基盤を解散させるプロジェクトでもあるお台場プロジェクトで、 これまでの開発基盤と同じことをやるというのはどう考えても筋が通らないわけです。 ですから、お台場プロジェクトでは絶対にRailsのバージョンアップはしないと決めました。

アプリケーション構造の整理

コード削除とほぼ同時にやったのが、アプリケーション構造の整理でした。

これについてはそもそも問題自体の説明が必要でしょう。 「本体」システムはウェブサービスやAPIサーバー、非同期ジョブ、 バッチなど複数のRailsアプリケーションからなるのですが、 そのうちAPIサーバーと非同期ジョブはウェブサービスの「モード」として実装されていました。 ウェブサービスを起動したとき、特定の名前のファイルが存在したら APIサーバーのエントリポイントが生えてAPIサーバーとして動くという、凄まじい実装がされていたのです。

プロジェクト開始から2017年いっぱいくらいにわたって、このものすごい実装を排除しました。 APIサーバー(pantry)は独立したアプリケーションとし、非同期ジョブ(background-worker)はバッチ(kuroko)に置き換えるなどして廃止。 同時に古いAPIサーバー(api, api2)を消したこともあり、アプリケーション構造はだいぶシンプルにすることができました。

f:id:mineroaoki:20181228000249p:plain
アプリケーション構造の整理

全面的なmicroservices化を決断

当初はmicroservices化を前面に出すかどうかはまだ決めかねていたのですが、 現在はすでに全面的にmicroservices化することを決めています。 その決め手となったのは、最初に小さな機能を分割してみて、その効果が明白に感じられたことでした。

具体的には、スマホアプリのA/Bテストなどに使っているuser_featuresという機能を分割した時点です。 この機能はもともと技術部がオーナーでしたし、専用Redis 1つだけにアクセスする構造になっていたため、 政治的にも技術的にも都合がよかったのです。 そこでこの機能を分割してみたところ、分割したあとのほうが明らかにつくりがわかりやすく、 改善しやすくなりましたし、実際に改善が進みました。 誰が実装すべきかも明確で、それでいて他の部署の人間も逆に手を出しやすくなったと感じています。 やはりコード共有というのは「誰も持っていない」のではだめで、オーナーありきのほうがうまくいくなと感じます。

わたし個人としても最近、本体のAPIサーバー(pantry)にとある機能を追加するためにmicroserviceを1つ実装したのですが、 DynamoDBを中心としたアーキテクチャ設計からstaging環境・production環境の構築に最小の実装までを、わたし1人で1週間弱で終えることができました。 これはお台場プロジェクトをやっていなければとてもできなかったことです。 もし2016年時点のpantryでこれをやれと言われたら何ヶ月必要になっていたか予想できません。

すべてがHakoになる

もともとクックパッドではmicroservicesのためのインフラは整備されつつありました。 例えば次のようなアプリケーションやミドルウェアが稼働しています。

さらに直近ではRubyのgRPCライブラリの置き換えなども行われています。 すでに新規のサービスはすべてこれらのインフラに乗っていますが、「本体」をどうするかだけはずっと宙に浮いた状態だったわけです。

2018年になって、「本体」もこの共通インフラに乗せると決めた時点で、話は非常にシンプルになりました。 現在では徐々にではありますが「本体」がHako化(コンテナ化)されつつあり、来年内の完了を見込んでいます。 社内のすべてのシステムがmicroservices構成になり、コンテナで動く状態が視界に入ったと言えるでしょう。

microservicesへの分割戦略

さて、microservices化を決断した場合、次に問題になってくるのが、「どこでサービスを切るか」です。 正直、これはシステム設計の話なので、パッケージをどう分けるか、クラスをどう分けるかと同じようなものであり、 決定的な基準がありません。

しかし、特に「本体」システムに限って言えば分割する場合の成功パターンがわかってきました。

そのパターンとは、データベースがすでに分かれている機能については、データベースを中心としてそれに紐付く部分を分割することです。 「本体」はRailsアプリケーションにしては珍しいことに、非常に大量のデータベースにアクセスしています。 database.ymlを見る限りだと、実に20以上のデータベースが接続されているようです。 これらのデータベースを、データベースとそれに紐付くコードをまとめて分割すると、 意味的にもデータフロー的にも無理がなく分割できることがわかりました。 これは冷静に考えてみれば当然と言えば当然なのですが、 このことに気付いてからは、「データベースが切れているならシステムも切れる」というわかりやすい基準ができました。

具体的には、Solrを核として分離したレシピ検索のシステム(voyager)、 専用Redisを中心として分離したA/Bテスト機能(user_features)、 専用Auroraをベースとして分離した「料理きろく」機能などがこのパターンでうまく分割できた例です。 今後もこのパターンに沿って、専用MySQLを持つブックマーク機能(MYフォルダ)や、 投稿者向けの統計機能(キッチンレポート)を分割していく予定です。 逆に、メインの一番巨大なMySQLに紐付いた機能群は最後に分割することになるでしょう。

microservicesに分割するという話になった当初は分割の基準がよくわかっていなかったので、 例えば「レシピのようによく使うリソースを最初に切り出すのがよいのではないか」 「事業部3つに合わせていきなり3つに分割しよう」などなど、様々な考えが錯綜していました。 その根底には、ひとまず切り出せそうな部分はいくらか見えているのだが、 それを順番に地道に分割していくくらいではいつまでたっても本丸のコア部分の分割まで至らないのではないか……という焦りがあったと思います。

しかし実際にやってみて最もうまくいった分割方法はやはり「データフローが明確に切れるところで切る」ことです。 慌てず騒がずデータフローを分析して、端から削り切るのが結局は最短の道だと思います。

大きな静的データの共有問題

microservicesへ分割していくうえで他に困ることの1つが「大きな静的データの共有」の問題です。 例えばクックパッドだとテキストの分析に使われている専用辞書がこれにあたります。 この辞書は検索サービスからも検索バッチからもレシピサービスからも、 その他ありとあらゆるところから頻繁にアクセスされており、 これを果たして単純に単体サービスとして分割してしまっていいものか難しいところでした。

『マイクロサービスアーキテクチャ』などによると、 このようなタイプのデータは原則としてはサービスにするよりデータとして配布してしまったほうがよいようです。 クックパッドでは偶然にも、この辞書をGDBM化してメモリに乗せる仕組みが少し前に入っていました。 そこでこの仕組みを利用して、GDBMファイルを各アプリケーションに配布することでひとまず解決をみました。

その後、GDBMファイルのバージョン問題にぶちあたって少し方式を変更したりもしましたが、 いまのところうまく動いています。

APIオーケストレーション層の導入: Orcha

microservices化に関する直近の試みはAPIオーケストレーション層「Orcha(オルカ)」の導入です。 オチャではありません。 これはわたしが勢いだけで入れてみたものなのですが、思ったより便利で驚いています。

OrchaはJavaで実装されており、 Spring ReactorとSpring Fluxをベースとしたリアクティブプログラミングを活用しています。 下図のようにリバースプロクシ(rproxy)とAPIサーバー(pantry)の間に入り、 pantryを含めたmicroservices群のAPIを統合して、スマホアプリ用のAPIを提供します。

f:id:mineroaoki:20181228000351p:plain
APIオーケストレーション層Orcha

オーケストレーション層を入れようと思った最初の動機は、 スマホアプリから複数のAPIを呼ぶレイテンシーを削減することでした。 しかし実際に入れてみていま感じている最大の利点は、 「本体」にさわらずに既存のAPIを拡張できるという点です。 アーキテクチャを改善していくうえで非常に便利な道具が一つ加わったと感じています。

例えばレシピを取得するAPIに新しい情報を差し込みたい場合であれば、 本体のAPIサーバーが返したJSONを加工して情報を追加することで達成できます。 ようするに、「高機能なJSON用sed」のような動きをしているわけですね。

今後の展開

お台場プロジェクトは来年2019年から、最終の第4期に突入します。 これから1年強は、本体を片っ端から分離しHako化するという正面対決になるでしょう。 最初はデータベースが分かれている機能から分離を始め、徐々にメインDBを切り崩します。

また、せっかくOrchaという新しい自由な遊び場ができたので、 それを活用してGraphQLの導入を試してみようと思っています。 ちょうどスマホアプリの側でもiOS, Androidともにアーキテクチャが刷新されつつあるので、 新しい仕組みを導入するにはいいタイミングです。

まとめ

本稿では、クックパッドの中核たるレシピサービスのアーキテクチャを改善する 「お台場プロジェクト」について、その戦略のすべてをお話ししました。 特に意識して行ってきたことは次のような点です。

  • 意図的にこれまでとの違いを出すタスク選択
  • 最初は成果の出しやすいコード削除から
  • microservices化は小さく試して全面展開
  • オーケストレーション層で展開の自由度を高める

現在のところプロジェクトの最大の問題は、とにかく人が足りないということです。 エンジニアは全分野で足りていないのですが、サーバー側は特に足りません。 Railsならまかせろ!な方にも、Railsブッ殺す!な方にも、 やりごたえのある楽しいタスクがありますので、 ぜひ以下のフォームから応募をお願いいたします。

https://info.cookpad.com/careers

Special Thanks 〜またの名を戦績リスト〜

わたしはお台場プロジェクトについてはあくまで戦略レベルしか関与しておらず、 実装レベルの判断は特に聞かれない限り担当者にすべて任せています。 その点で、お台場プロジェクトは個々のエンジニアの力量によるところが大きいプロジェクトであり、 ここまで来られたのはすべてメンバーのおかげと言ってよいでしょう。 この記事の最後に、各自が撃破したタスクを記して、終わりにしたいと思います。 なお、プロジェクト開始から2018年内までに完了したものだけを、だいたい時間順に列挙しています。

※書き忘れてるやつあったらすまん……


Dynamic Type

$
0
0

モバイル基盤部のヴァンサン(@vincentisambart)です。

使っているアプリのフォントサイズを変えたいと思ったことありますか?目があまり良くないから文字を大きくしたい。逆にもっと多くの情報を一目で見られるために文字を少し小さくしたい。

フォントサイズをアプリ内で調整できるアプリもありますが、iOSではシステム全体のフォントサイズを調整できる「Dynamic Type」と呼ばれる機能があります。iOS全体の設定に一般→アクセシビリティ→さらに大きな文字(英語だとSettingsにGeneral→Accessibility→Larger Text)で変えられます。対応しているアプリではフォントサイズがその設定に合わせられます。

標準より少し小さくできるとはいえ、「アクセシビリティ」設定に入っているのもあって文字を大きくする方がメインのユースケースの気がします。

Dynamic TypeはiOS 7から使えるようになりましたが、関連している様々な便利機能がその後のiOSバージョンに追加されました。

一応Dynamic Typeを使うにはAuto Layoutは必須ではないのですが、手動レイアウトと一緒に使うのはおすすめできません。

システム設定変更

設定を変える前に、簡単に戻せるようにまずは標準設定を覚えておきましょう。標準設定は以下のスクリーンショットのように「さらに大きな文字」が無効で、下部のスライダーがど真ん中です。

Dynamic Type標準設定

スライダーを動かすとフォントサイズがどれくらい変わるのかすぐ見られます。一番小さい設定(extra smallまたはXS)にすると、以下のようになります。

f:id:vincentisambart:20190104132028p:plain:w320

「さらに大きな文字」無効のまま一番大きい設定(extra extra extra largeまたはXXXL)にすると以下のようになります。

f:id:vincentisambart:20190104132100p:plain:w320

アクセシビリティサイズはXXXLより大きく、「さらに大きな文字」を有効にすると選択できるようになる5つの設定です。その一番大きい設定(accessibility extra extra extra large、またはAX5)にすると以下のようになります。

f:id:vincentisambart:20190104131951p:plain:w320

因みにDynamic Type対応しているアプリでも、アクセシビリティサイズをXXXLと同じ扱いにしているアプリもあります。文字がとても大きいのでレイアウトが崩れやすいからでしょう。

設定の値

ユーザーに選択されたDynamic Type設定はUIContentSizeCategoryという型で、UIApplication.shared.preferredContentSizeCategoryまたは(iOS 8以上では)traitCollection.preferredContentSizeCategoryで取得できます。現時点(iOS 12)で利用できるすべての値は以下のとおりです。

  • unspecified (iOS 10以上)
  • extraSmall (XS, xSmall)
  • small (S)
  • medium (M)
  • large (L) – 標準設定
  • extraLarge (XL, xLarge)
  • extraExtraLarge (XXL, xxLarge)
  • extraExtraExtraLarge (XXXL, xxxLarge)
  • accessibilityMedium (AccessibilityM, AX1)
  • accessibilityLarge (AccessibilityL, AX2)
  • accessibilityExtraLarge (AccessibilityXL, AX3)
  • accessibilityExtraExtraLarge (AccessibilityXXL, AX4)
  • accessibilityExtraExtraExtraLarge (AccessibilityXXXL, AX5)

設定画面で「さらに大きな文字」が有効になっているときだけに選択できるアクセシビリティサイズは名前がaccessibilityから始まります。iOS 11以上では、アクセシビリティサイズかどうか確認するためにUIContentSizeCategoryisAccessibilityCategoryというメソッドがあります。また、同じくiOS 11以上でUIContentSizeCategory<, <=, >=, >で比較できるようになるのでtraitCollection.preferredContentSizeCategory > .extraExtraExtraLargeも使えます。

content size categoryの値によって、レイアウトを変えたりできます。例えばDynamic Typeの設定画面で大きいアクセシビリティサイズを選ぶ時、「さらに大きな文字」スイッチがラベルの右からその下に移ります。Dynamic Type設定によってUIStackViewの設定が変わるのでしょう。

f:id:vincentisambart:20190104131951p:plain:w320

自分のアプリ内、Dynamic Typeの設定に合わせてするレイアウト変更は作成時以外、どういうタイミングでやれば良いのでしょうか。

変更に反応

アプリがバックグラウンドにある間に、ユーザーがDynamic Typeの設定を変えるとアプリが終了されるわけではありません。ユーザーがアプリに戻ったら、アプリが変更を知らされる仕組みが2つあります:

  • UIContentSizeCategory.didChangeNotificationというnotificationがアプリに送られます。
  • iOS 8以上ではtraitCollectionDidChangeメソッドが呼ばれます。UITraitEnvironmentプロトコルのメソッドですが、UIViewUIViewControllerが実装しているので、自分のビューやビューコントローラに以下のコードでDynamic Typeの設定変更に反応できます。
overridefunctraitCollectionDidChange(_ previousTraitCollection:UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory {
        // 設定が変わったときにやりたいこと
    }
}

Text Style (書式)

設定によってレイアウトを変えても、Dynamic Typeの「Type」は文字、フォントのことなので、肝心のフォントサイズはどうすれば良いのでしょうか。アプリに使われているすべてのスタイルをcontent size categoryごとに用意するのは手間が掛かりすぎます。

そのため、iOS 7からシステムにいくつかのText Styleが用意されています。ワードプロセッサーでいうと「書式」と同じ概念です(ただし自分の好みに合わせて変えることができません)。Human Interface Guidelines(HIG)のTypographyページにスタイルのリストと各content size categoryでのフォントサイズが見られます。アクセシビリティサイズも同じページの少し下にAX1, AX2, AX3, AX4, AX5として紹介されてあります。因みにフォント自体はシステムフォント(San Francisco)です。

そのスタイルはコード上ではUIFont.TextStyleです。iOS 7ではスタイルが少なかった(body, caption1, caption2, footnote, headline, subheadline)のですが、iOS 9(callout, title1, title2, title3)やiOS 11(largeTitle)で少し増えました。

スタイルによって変わるのはフォントサイズだけではなく、headlineスタイルの場合、フォントがボルドになります。

large設定では、iPadで各スタイルを表示すると以下のようになります。

f:id:vincentisambart:20190104132124p:plain:w500

extraExtraExtraLarge設定では、以下のようになります。

f:id:vincentisambart:20190104132150p:plain:w500

accessibilityExtraExtraExtraLarge設定では、以下のようになります。この最大サイズでは各スタイルの差があまりないですね。

f:id:vincentisambart:20190104132207p:plain:w500

コードでは、UIFont.preferredFont(forTextStyle:)がメインのAPIです。UIFont.preferredFont(forTextStyle: .body)が今のDynamic Type設定に合っているbodyスタイルのフォントを返してくれます。

Interface Builderでは、スタイルをlabelやtext viewのFont設定にText Stylesの中で選べます。

f:id:vincentisambart:20190104131624p:plain:w390

自動フォント調整

上記のセクションの使い方だけでは、アプリがバックグラウンドにある間にユーザーがDynamic Typeの設定を変えてアプリに戻ったら、新しい設定が自動的に反映されるわけではありません。自分でcontent size categoryの変更に反応して、labelやtext viewのフォントを指定し直す必要があります。テキストスタイルをコードで指定していればまだ良いのですが、Interface Builderで指定した場合、コードで再指定することになってしまいます。

その問題を避けるため、iOS 10以上ではUIContentSizeCategoryAdjustingプロトコルに準拠しているクラス(UILabel, UITextField, UITextView)にadjustsFontForContentSizeCategoryというプロパティがあります。Interface Builder上の「Automatically Adjusts Font」と同じです。

f:id:vincentisambart:20190104131820p:plain:w325

以下その機能を「自動フォント調整」と呼ぶことにします。

自動フォント調整が上記のプロパティで有効になっているビューは、Dynamic Type設定変更後、バックグラウンドにあったアプリに戻るとフォントサイズが自動的に更新されます。ただし、フォントサイズが更新されるのは上記に紹介したtext style、またはあとで紹介するfont metrics、が使われている場合のみに有効です。

有効にされたら自動フォント調整が効く 有効にされても自動フォント調整が効かない
label.font = UIFont.preferredFont(forTextStyle: .body)label.font = UIFont.systemFont(ofSize: 12)
Interface BuilderでフォントにText Stylesのどれかを選んだ Interface Builderでカスタムやシステムフォントを選んだ

実際Interface Builderでテキストスタイルでないフォントを指定して、「Automatically Adjusts Font」にチェックを入れた場合、ビルド時に警告が出ます。コードでそのビューのフォントに自動フォント調整が効くフォントを指定したら、ちゃんと動きますがビルド時の警告が残ります。なので、コードでフォントを指定する場合、自動フォント調整を有効にするのもコードでやる(view.adjustsFontForContentSizeCategory = true)のが良いでしょう。

カスタム

テキストスタイルAPIはシステムフォントしか使えませんし、システムフォントを使うとしても、スタイルの種類が多くありません。

テキストスタイルAPIが用意されたスタイルを使う前提で作られたとしても、コードで自分の定義したサイズやフォントを使う方法がなかったわけではありません。少し考えるだけで以下の2つの方法がすぐ思いつくのでしょう。

  • switch preferredContentSizeCategory { ... }– すべてのcontent size categoryの分のフォントとサイズを用意します。かなり手間が掛かりますし、スタイルを変えたいときも大変です。
  • ((UIFont.preferredFont(forTextStyle: .body).pointSize / 17.0) * myFontSize).round() (17.0がbodyスタイルの標準のポイントサイズです)で自分のフォントのサイズをcontent size categoryに合わせて調整します。

実は、現時点でクックパッドiOSアプリがDynamic Typeに対応している唯一の画面、レシピ詳細では長い間後者を使っています。

もちろんこの2つの方法では、自動フォント調整が使えません。Dynamic Typeの設定に変更があったときにフォントサイズを調整したければ、「変更に反応」セクションで紹介した方法で変更に反応して各ビューのフォントを再指定します。

Deployment TargetがiOS 10以前のアプリはカスタムなスタイルを使いたかったら、上記の方法しかありませんが、iOS 11以上だと、もう少し楽なAPIが提供されています。

UIFontMetrics

iOS 11から誕生したUIFontMetricsクラスを使うと、好きなフォントやフォントサイズを選んで、そのカスタムなスタイルのサイズをDynamic Typeの設定に合わせられるだけではなく、自動フォント調整も使えます。

使い方は以下のようです。

letcustomFont= UIFont(name:"AmericanTypewriter", size:17)!// 好きなサイズのシステムフォント`UIFont.systemFont(ofSize: myCustomSize)`でも大丈夫ですletscaledFont= UIFontMetrics.default.scaledFont(for:customFont)
label.font = scaledFont
label.adjustsImageSizeForAccessibilityContentSizeCategory =true

もしフォントサイズが大きすぎるとレイアウトが崩れてしまう場合、最大フォントサイズを指定できます。

letscaledFont= UIFontMetrics.default.scaledFont(
    for:UIFont.systemFont(ofSize:customFont),
    maximumPointSize:50.0
)

UIFontMetricsにはフォントだけではなく、画像のサイズを文字の大きさに合わせるためのメソッドもあります。

letscaledSize= UIFontMetrics.default.scaledValue(for:sizeToScale)

シンプルですね。懸念点が1つあります:Interface Builderでその機能が使えません。ビューをInterface Builderで配置しても良いのですが、フォントはコードで指定する必要があります。

メトリックスはテキストスタイル次第

上記のコードでUIFontMetricsのインスタンスはUIFontMetrics.defaultを使っていましたが、UIFontMetricsのドキュメントを見ると、init(forTextStyle textStyle: UIFont.TextStyle)もあります。実はUIFontMetrics.defaultUIFontMetrics(forTextStyle: .body)に初期化されたものです。UIFontMetrics(forTextStyle: .title1)UIFontMetrics(forTextStyle: .caption2)なども使えます。

どうしてテキストスタイルを指定できるのかと言いますと、Dynamic Type設定によってフォントのサイズがどう変わるのかはテキストスタイルごとに少し違うためです。以下の図を見ればもう少し分かるかと思います。図に使われた数値をどう計算したのかあとで説明します。

f:id:vincentisambart:20190104131516p:plain

カスタムスタイルごとにサイズの推移をどのシステムスタイルに合わせたいのか決めるのが大変だと思うので、分からない時はUIFontMetrics.defaultを使って良い気がします。

UIFontMetricsの計算

上記は説明せずに図を出しましたが、実際サイズがどう計算されるのでしょうか?UIFontMetrics.scaledFont(for:)の返しているフォントの自動フォント調整対応は自分で実装できないかもしれませんが、計算くらいはできるのではないでしょうか。

実はUIFontMetrics.scaledFont(for:)に使われる計算が上記の「カスタム」セクションで紹介したすぐ思いつきそうな((UIFont.preferredFont(forTextStyle: .body).pointSize / 17.0) * myFontSize).round()に近いです。ただし、割合はpoint sizeからではなく、leadingから計算されています。

数字は既に紹介しましたHIGのTypographyページのDynamic Type Sizesにある「Leading」にあります。図に使った数字は対象のcontent size categoryのleadingを標準(large)のleadingで割っただけです。

因みにフォントに関して「Leading」は「リーディング」ではなく、「レディング」と読みます。

TypographyページにあるLeadingというのは日本語で「行送り」のことです。1行のベースラインから次の行のベースラインまでの距離です。

UIFontにあるleadingは違っていて、こっちは「行間」のことです。TypographyページにあるLeadingがUIFontでいうとleading + lineHeightです。

いろんな説明よりコードの方が分かりやすいかもしれません。下記のコードに使われているUIFont.preferredFont(forTextStyle:compatibleWith:)がiOS 10以上でしか使えませんが、もっと古いiOSバージョンではleadingの値のテーブルをコードに埋め込めば簡単に実装できるしょう。

因みに下記のコードに使われているUITraitCollectionが表示に影響ありそうな環境の状態(サイズクラス、画面スケール)や設定(Dynamic Type)をまとめるものです。UIFontMetricsの場合、サイズがどう変わるのか見たいとき、Dynamic Type設定をいちいち変えるのが大変なので、UITraitCollectionを渡すのが良いのですが、ビューのフォントを指定する場合は基本的にUITraitCollectionを渡さず、ユーザーの設定に合うものを求めます。

import UIKit

privateextensionUIFont {
    // https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/typography/#dynamic-type-sizes にある"leading"に相当します// 実はUIFontには`_bodyLeading`というメソッドがありますが、プライベートなので再実装が必要ですvarbodyLeading:CGFloat {
        return lineHeight + leading
    }
}

// `UIFontMetrics`の機能の一部を再実装するクラス// 注意:自動フォント調整対応のフォントを作るには本物の`UIFontMetrics`を使うしかありませんclassSimpleFontMetrics {
    privatevartextStyle:UIFont.TextStyleinit(forTextStyle textStyle:UIFont.TextStyle) {
        self.textStyle = textStyle
    }

    staticlet `default`:SimpleFontMetrics= .init(forTextStyle: .body)

    privatestaticletdefaultContentSizeCategoryTraitCollection= UITraitCollection(preferredContentSizeCategory: .large)

    privatefuncunroundedScaledValue(for value:CGFloat, compatibleWith traitCollection:UITraitCollection?) ->CGFloat {
        letdefaultFont= UIFont.preferredFont(forTextStyle:textStyle, compatibleWith:SimpleFontMetrics.defaultContentSizeCategoryTraitCollection)
        letcurrentFont= UIFont.preferredFont(forTextStyle:textStyle, compatibleWith:traitCollection)

        return (value * currentFont.bodyLeading) / defaultFont.bodyLeading
    }

    // `UIFontMetrics.scaledValue(for:compatibleWith:)`と同じですfuncscaledValue(for value:CGFloat, compatibleWith traitCollection:UITraitCollection? =nil) ->CGFloat {
        // 表示に使われている画面のスケール(ポイントごとのピクセル数)letdisplayScale:CGFloat// `traitCollection.displayScale`が0だったら未定だということなので`traitCollection`が指定されていないと同じ扱いですiflettraitCollection= traitCollection, traitCollection.displayScale !=0 {
            displayScale = traitCollection.displayScale
        } else {
            displayScale = UIScreen.main.scale
        }
        // ピクセル単位で四捨五入return (unroundedScaledValue(for:value, compatibleWith:traitCollection) * displayScale).rounded() / displayScale
    }

    // `UIFontMetrics.scaledFont(for:maximumPointSize:compatibleWith:).pointSize`に相当しますfuncscaledFontPointSize(for pointSize:CGFloat, maximumPointSize:CGFloat=0, compatibleWith traitCollection:UITraitCollection? =nil) ->CGFloat {
        assert(pointSize >=0, "You cannot create a font of negative size.")
        // フォントサイズの四捨五入はピクセル単位ではなくポイント単位ですletscaledPointSize= unroundedScaledValue(for:pointSize, compatibleWith:traitCollection).rounded()
        if maximumPointSize ==0 {
            return scaledPointSize
        } else {
            return min(scaledPointSize, maximumPointSize)
        }
    }
}

UIFontMetricsの少し不自然なところ

上記にUIFontMetricsがサイズ計算に使っている比率がフォントのサイズ(pointSize)のではなく、行送り(leading + lineHeight)のだと書きましたが、フォントサイズの計算にその比率が使われるから少し不自然な結果になることがあります。

letlargeTraitCollection= UITraitCollection(preferredContentSizeCategory: .large)
letaxxxlTraitCollection= UITraitCollection(preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge)
letbaseFont= UIFont.preferredFont(forTextStyle: .body, compatibleWith:largeTraitCollection)
UIFont.preferredFont(forTextStyle: .body, compatibleWith:axxxlTraitCollection).pointSize // => 53
UIFontMetrics.default.scaledFont(for:baseFont, compatibleWith:axxxlTraitCollection).pointSize // => 48

標準文字サイズ(large)のbodyスタイルのフォントをUIFontMetricsで一番大きいDynamic Type設定に拡大されたフォントと、一番大きいDynamic Type設定でのbodyのフォントはサイズが少し異なります。そのため、UIFont.preferredFontUIFontMetrics.scaledFontを同じ画面で混在させない方が良いかもしれません。

因みに、今回はその方が分かりやすかったのですが、普段はUIFont.preferredFontの戻り値をUIFontMetrics.scaledFontを渡さないでおきましょう。一応動くのですが、UIFontMetrics.scaledFontの戻り値をまたUIFontMetrics.scaledFontに渡すとObjective-Cの例外が発生します。

画像

テキストの横に画像があると、その画像をある程度テキストのサイズに合わせたいことがあるかもしれません。手動でUIFontMetrics.scaledValue(for:)を使ってもできますが、iOS 11からUIButtonUIImageViewが準拠しているUIAccessibilityContentSizeCategoryImageAdjustingプロトコルにadjustsImageSizeForAccessibilityContentSizeCategoryというプロパティが登場しました。Interface Builderにある「Adjust Image Size」と同じです。

f:id:vincentisambart:20190104132302p:plain:w325

そのプロパティがtrueの場合、Dynamic Type設定によってビューの画像が拡大されますが、アクセシビリティサイズが選ばれている場合のみです。他の設定ではサイズが変わりません。

サイズ調整で画像が大きくなると荒くなってしまう可能性があります。もとの画像がPDFでしたら、Asset Catalogの設定で「Preserve Vector Data」にチェックを入れたら綺麗に拡大されます。

f:id:vincentisambart:20190104132325p:plain:w325

その他

  • 広い画面でもコンテンツが広がりすぎないためにある読みやすい幅機能はDynamic Typeと一緒に使われるように設計されているので、ぜひ紹介記事をご覧ください。
  • 既存のアプリは全画面を一気に対応するのは難しいでしょう。1つの画面で対応しているビューとしていないビューが混在すると不自然な表示になりますので、画面単位で対応した方が良い気がします。
  • テーブルのセルの高さはできるだけシステムが自動計算するようにしましょう:estimatedRowHeightの指定を忘れず(指定されないと自動計算が動かないので)、高さ(rowHeight)をUITableView.automaticDimensionにします。rowHeightestimatedRowHeightUITableViewのプロパティで指定しても、UITableViewDelegateのメソッドで返しても、どっちでも大丈夫です。
  • Auto Layoutを使ってビューを配置するとき、ベースラインアンカー(firstBaselineAnchorまたはlastBaselineAnchor)に対する縦制約はconstraint(equalToSystemSpacingBelow:multiplier:)(またはlessThanバージョンgreaterThanバージョン)を使うと、制約の高さがフォントのサイズに合わせて変わります。Interface Builder上では「Constant」の値に「Use Standard Value」を選ぶのと同じです。

まとめ

Dynamic Type設定でユーザーが自分の視力や好みに合わせてフォントのサイズを選べますが、アプリ側での対応が必要です。

iOS 7でその機能が導入されてから、対応がやりやすくなる機能が少しずつ増えて、iOS 11以上ではだいぶ楽になったのではないでしょうか。

もっと多くのユーザーの使いやすさのために対応しているアプリを増やしておきましょう。

Cookpad TechConf 2019 を開催します!

$
0
0

こんにちは! 広報部のとくなり餃子大好き( id:tokunarigyozadaisuki)です。

2019年2月27日(水)、クックパッドの技術カンファレンス「Cookpad TechConf 2019」が開催されます。

公式サイト : https://techconf.cookpad.com/2019

f:id:tokunarigyozadaisuki:20190131135452p:plain

Cookpad TechConf 2019 について

【「Cookpad TechConf 2019」開催概要】
開催日:2月27日(水)12:30開場、17:30終了予定
場所:恵比寿ガーデンプレイス ザ・ガーデンホール(東京都目黒区三田1-13-2)
参加費:無料

「Cookpad TechConf 2019」は、クックパッドのサービスづくりのノウハウを発信する技術カンファレンスです。 私たちクックパッドは「毎日の料理を楽しみにする」というミッションを掲げ、世界中における食と料理の課題をテクノロジーで解決するために、様々な新規プロジェクトに挑戦しています。今年は、「クックパッドの新規事業を支える技術」をテーマとし、クックパッドのエンジニアやデザイナーがどのようにサービス開発に取り組んでいるのか、またその過程で得た技術的知見について公開します。

執行役 CTO 成田の基調講演のほか、前半は本カンファレンスのテーマである、クックパッドの新規事業を支える技術やデザイン開発の現場について、後半にはテクニカルセッションをご用意しております。詳しい内容については、公式サイトをご覧ください。

会場には、今回発表するセッションに関連するプロダクトを展示するブースもご用意いたしますので、ぜひお立ち寄りください。

自作キーボードキット Cookpad Pad をノベルティとして少量配布します

昨今、ブームになっている自作キーボードですが、この度「Cookpad Pad」という自作キーボードを設計し、作りました。ごくわずかではございますが、本カンファレンスにてノベルティとしてお渡ししたいと考えております! 6つのキーで、「C」「O」「K」「P」「A」「D」を打つことができます。オープンハードウェアとして公開もしています。

f:id:tokunarigyozadaisuki:20190131140559j:plain

みなさまのご参加お待ちしております! 

本カンファレンスへの応募締切は2/4(月)までとさせていただいております。まだ「カンファレンス+懇親会」のお申込みも受付中です! 当日は、登壇社員以外にも運営として関わる社員が多数おります。お見かけの際はぜひお声がけください。みなさまにお会いできますことを楽しみにしております。

cookpad.connpass.com

Hackarade #05: IoT

$
0
0

こんにちは!スマートキッチン事業部のシュガー(佐藤彩夏)です。趣味は電子部品アクセサリー作りです。

クックパッドでは、Hackaradeというエンジニアの技術力を底上げするための社内ハッカソンを定期的に開催しており、第5回目の今回は「IoT」のテーマで開催することになりました。

f:id:sugar_ayaka:20190201162614j:plain
開催風景

なお、過去には下記のようなテーマで実施しております。

まず簡単にIoTがテーマになった背景をお話します。

クックパッドには、今年の1月にスマートキッチン事業部という新しい事業部ができました(去年1年間は研究開発部の中の1グループでした)。
スマートキッチン事業部では、OiCyという、人と機器とレシピをつなぐことで毎日の料理を楽しみにするサービス&機器の開発を行っております。

つまり、ソフトウェアだけでなくハードウェアの開発もしており、ハードウェアを開発するメンバーもいます(私もその1人です)ので、
今回はハードウェアも絡むIoTをテーマで開催することになりました。

このブログでは開催概要と成果の一部をご紹介します。

開催概要

今回、参加者が約100人いたので、全員で回路を組んだり、ハンダ付けするのはあまり現実的ではないと思い、
M5Stack Grayという回路を組まなくても容易にセンサーをつなげる(Stack)ことができるモジュールを使用することにしました。

具体的には、ESP32(Wi-FiとBluetoothを内蔵する低電力なマイコン)、ディスプレイ、ボタン、バッテリー、SDカードスロット、スピーカー、9軸センサが内蔵していて、サイズは5cm四方くらいです(写真中央の四角いもの)。

f:id:sugar_ayaka:20190201182046j:plain
M5Stackと同梱物

全体としては下記のような流れで実施しました。

  • 事前準備:開発環境を整えてきてもらう&くじ引きタイム

  • 第一部:M5Stackの説明と演習(ハードウェア開発)

  • 第二部:AWS IoTの説明と演習(ソフトウェア開発)

  • 第三部:乾杯&発表会

事前準備:開発環境を整えてきてもらう&くじ引きタイム

まず事前準備として、下記をしてもらいました。

  • Arduino IDEのインストール

  • USBドライバーのインストール

  • Arduino Core for ESP32の導入

詳細はこのあたりの記事に詳しく掲載されてます。

そして当日の朝、運営側で用意したセンサーを1人1個くじ引きで引いてもらい、そのセンサーを使って好きなものを開発してもらうことにしました。

f:id:sugar_ayaka:20190201165036p:plain
ハズレのセンサーを引いてしまったCTO

センサーは、水分センサー、光センサー、アルコールセンサー、火炎センサー、ダストセンサー、温湿度気圧ガスセンサー、測距センサー、ジョイスティックを用意しました。

なお、弊社はビルの関係で火気厳禁なので、火炎センサーは喫煙所に行かないと試せないという罠。

加えて、会場には今回M5Stackとセンサーを購入したスイッチサイエンスさんにもお越しいただき、楽しそうなセンサーやコントローラーなどをお貸し出しいただきました。

f:id:sugar_ayaka:20190201174432p:plain
スイッチサイエンスさんからの差し入れ

第一部:M5Stackの説明と演習(ハードウェア開発)

第一部は、スマートキッチン事業部のハードウェアエンジニアの山本さんが、M5Stackの説明とセンサーの説明とハンズオン演習を行いました。

その資料が下記です。最後のページにサンプルプログラムへのリンクもありますので是非ご活用ください。

ここでの驚きは、だいたいハードウェアのハンズオンは半分くらいの人が引っかかるので、せいぜい10人くらいでやることが多いのですが、
100人規模でやったにも関わらず大きくつまずく人はおらず、見ていた限り全員が午前中に何かしらを動かせるようになっていたことです。
感動。

f:id:sugar_ayaka:20190201170229j:plainf:id:sugar_ayaka:20190201170312p:plain
開発の様子

第二部:AWS IoTの説明と演習(ソフトウェア開発)

続いて、AmazonのAWS担当の方から、AWS IoTを活用してM5Stackと連携するハンズオンを実施していただきました。

AWS IoTとは、インターネットに繋がるデバイス(今回でいうとM5Stack)とAWSクラウドを連携して双方向通信ができるサービスです。

とても丁寧でわかりやすい資料を使って下記の内容をご説明いただきました。

f:id:sugar_ayaka:20190201170733p:plain
Amazonさんのハンズオン資料(一部抜粋)

第三部:乾杯&発表会

ここまでで、全員がM5StackとAWS IoTを使って一通りサンプルを動かせるようになりましたので、
このあと、数時間の自由制作の時間を設け、夕方くらいに乾杯&発表したい人が発表するという形式を取りました。

f:id:sugar_ayaka:20190201171305p:plain
発表前にまずは乾杯&腹ごしらえ

成果物

面白い発表がたくさんあったのですが、全ては紹介しきれない(そして楽しすぎて多くの写真を撮りそこねた)ので一部だけご紹介します。

まずこちらは、測距センサーを用いた姿勢矯正デバイスです。

f:id:sugar_ayaka:20190201171734p:plain
姿勢矯正デバイス

PCの画面上にセンサーを取り付け、一定の距離近づくと姿勢が悪いことを検知してスクリーンセーバーでお知らせしてくれるというもの。

こういったものは世の中にいくつかありますが、ウェアラブルとかカメラを設置したりしなくても、シンプルに実装されているのがいいなと思いました。

続いて、スクワットを検出してレベルアップしていくデバイス。

f:id:sugar_ayaka:20190201174813g:plain:w250
スクワットデバイス

こちらは内蔵している9軸センサーを活用して、スクワットを検出し、回数に応じて音が変わるというもの。
例えば、まず5回スクワットすると音が鳴ってレベルが上がり、次は10回、20回・・・とハードルが上がっていく設計でした。

私も学生時代に運動支援研究をしていた頃、ひたすら縄跳びを飛んでいたことがあるので、
これはデバックが大変だっただろうなぁ、と努力賞を差し上げたい発表でした。

こんな感じで次々に面白い発表が続いて行ったのですが、ここでふと気づいてしまいました。

「あれ?これIoT(Internet of Things)じゃなくて、ただのT(Thing)じゃないか・・・?」

そうですね。これらはArduino(通信機能のついていないマイコン)とか使えばできちゃいますね。

ここで素晴らしい発表が。

システム障害が起きると、M5Stackの画面の色が緑から赤に変わって障害をお知らせしてくれるシステム。

f:id:sugar_ayaka:20190201172911p:plain
障害を通知してくれるシステム

これは、やっとIoT!しかも実務上も使えて素晴らしいシステムです。拍手。

他にも、色合わせゲームや、スマートロックシステムや、植物の水分が足りなくなると通知されるシステムや、Alexaと連動したキッチンタイマーや、素数が出るとゴリラのアスキーアートが画面に表示されるシステム(?)などなど、発想力豊かな発表がたくさんありました。
(そういえば料理に関係する発表はキッチンタイマーくらいしかなかったような)

f:id:sugar_ayaka:20190201180149p:plain:w300
ゴリラのアスキーアート

ちなみに私は、部屋の湿度や温度やガスの数値が一定以下になるとSlackで通知されるという、なんの工夫もない普通に便利なシステムを作りました。

f:id:sugar_ayaka:20190201163738p:plain:h300f:id:sugar_ayaka:20190201180607p:plain:h300
温湿度気圧ガスセンサーの値に応じてSlackに通知

これをきっかけに、社内のエンジニアがハードウェア開発にも目覚めてくれるといいなと願っています。

ということで、以上、第5回Hackaradeのレポートでした☆

DroidKaigi 2019 にクックパッド社員が1名登壇&ブースでお待ちしております!

$
0
0

こんにちは!広報部のとくなり餃子大好き( id:tokunarigyozadaisuki)です。

さて、エンジニアが主役のAndroidカンファレンス、DroidKaigi 2019の開催まであと二日となりました!

クックパッドは、本カンファレンスにゴールドスポンサーとして協賛します。そして、クックパッドに所属する @litmonが登壇し、@nshiba@shanonimが当日スタッフとして関わってくれております。 約20名のクックパッド社員が、DroidKaigi 2019 に参加致しますので、会場でお見かけの際にはお声がけいただけますと嬉しいです。

登壇の詳細

はじめに、登壇スケジュールと内容を紹介します。 @litmonが登壇するのは、2日目の最後のセッションです! 

2月8日(金)18:30〜 Room 4

門田福男 (@litmon) : Google Play Consoleのリリーストラックを有効活用してリリースフローの最適化を行った話

概要 : Google Play Consoleにはアプリのリリースを行う際にいくつかのトラック(alpha, beta, production)を選択することができる。また、2018年には新たにinternalトラックが開放された。 クックパッドアプリでは、これらのトラックを有効活用し、リリース自動化を行い、人間によるリリーススケジュールの管理をやめたときの話をする。 また、その際にぶつかった技術的制約などにどう対応したか、リリース自動化に向けて行った様々なTipsを紹介する。

コメント

クックパッドアプリは、複数の部署が協力して一つのアプリを開発しています。他部署とのコミュニケーションコストが肥大化していく中、打開策として機械による毎週自動でリリースを行う仕組みを実現しました。本セッションではどうしてクックパッドが自動リリースを行うようにしたのか、またAndroidアプリではそれをどのように実現したのか、その結果どうだったかなどを発表します。アプリのリリースフローについて悩んでいる方や、自動化に興味のある方、ぜひ遊びに来てください!

ブース

クックパッドは、DroidKaigi 2019 にてブースを出展いたします。Androidカンファレンスならではのクックパッドノベルティを数量限定でご用意いたしております! ぜひ、お立ち寄りくださいね。

Cookpad.apk #2 を開催します

本カンファレンス後、2/18(月)には昨夏#1を実施した「Cookpad.apk」の第2回を開催することにいたしました。DroidKaigi 2019 で惜しくも不採択となってしまったトークを中心に、現在社内で行っているAndroidアプリ開発に関する知見や学びについて共有いたします。懇親会の時間もございますのでお楽しみに! 

※本イベントはDroidKaigi 実行委員会が運営する公式イベントではありません。
※ご好評を頂き、全て満席となりましたのでご了承下さい。たくさんのお申し込みありがとうございました。

cookpad.connpass.com

おわりに

発表内容へのご質問やクックパッドにご興味をお持ちの方は、お気軽にブースまでお越しください! みなさまにお会いできることを楽しみにしております。

Prebid.js 導入による Header Bidding 改善の舞台裏

$
0
0

こんにちは。メディアプロダクト開発部の我妻謙樹(@itiskj)です。 サーバーサイドエンジニアとして、広告配信システムの開発・運用を担当しています。好きな言語は Go と TypeScript です。

以前、"Header Bidding 導入によるネットワーク広告改善の開発事情"というタイトルで、

  • Header Bidding の仕組み
  • 弊社の広告配信のクライアント側の設計
  • Transparent Ad Marketplace(以下、TAM)導入の過程

についてご紹介しました。今回は、TAM に次いで Prebid.jsをあわせて導入した際の知見についてご紹介します。

What is Prebid.js?

Prebid.jsとは、OSS で開発されている Web 向け Header Bidding ライブラリです。 http://prebid.org/において開発されているサービスの1つで、他にはアプリ向けの Prebid Mobile、サーバーサイド向けの Prebid Serverなどが開発されています。

https://github.com/prebid/Prebid.js/

Prebid のサービス群を用いると、以下二種類いずれかの Header Bidding に対応できます。

  • Client-to-Server Header Bidding(以下、C2S)
    • Prebid.js 及び Prebid Server を用いる
  • Server-to-Server Header Bidding(以下、S2S)
    • Prebid Server を自前でホスティングするか、すでに提供されている第三者サーバーを利用する

C2S 及び S2S の Pros/Cons は以下の通りです。

TypeProsCons
C2S対応事業者数が多い/トータルでの実装コストが低いクライアントのネットワーク帯域をより消費する
S2Sサーバーサイドで制御できる/クライアント側からは Single Request対応事業者が C2S と比べて限られている/Cookie Sync の技術的課題

今回は、「対応事業者数が多い」こと、及び「実装コストの改修」を考慮して、C2S 方式を導入しました。

Glossary

本文で言及している用語のうち、ドメイン知識のため説明が必要と思われる用語の一覧です。

word description
事業者 Header Bidding で入札リクエストを送っている先の事業者のこと。ここでは基本的に SSP(DSP を接続する場合もある)のことを指す。
スロット 広告が実際に表示される枠のこと。
DFP DoubleClick For Publisher の略。新名称は Google Ad Manager。
APS Amazon Publisher Services の略。TAM などの一連の広告サービスを提供する総称。
TAM Transparent Ad Marketplace の略。APS のサービスの一つで、Header Bidding を提供する。

Development with Prebid.js

Prebid.js を導入する際に、基本的に 公式ドキュメント | Getting Started及び Publisher API Referenceを参考することになります。公式ドキュメントがかなり充実しているので、基本的なユースケースであれば、ほぼ嵌らずに実装できるでしょう。

以下は、Getting Startedに紹介されている、最小限の実装例です。ここで使用されている API のうち、主要なものについて紹介します。

なお、Google Publisher Tag(以下、GPT)との併合を前提としています。

<html><head><linkrel="icon"type="image/png"href="/favicon.png"><script async src="//www.googletagservices.com/tag/js/gpt.js"></script><script async src="//acdn.adnxs.com/prebid/not-for-prod/1/prebid.js"></script><script>var sizes = [[300, 250]];var PREBID_TIMEOUT = 1000;var FAILSAFE_TIMEOUT = 3000;var adUnits = [{                code: '/19968336/header-bid-tag-1',                mediaTypes: {                    banner: {                        sizes: sizes}},                bids: [{                    bidder: 'appnexus',                    params: {                        placementId: 13144370
}}]}];// ======== DO NOT EDIT BELOW THIS LINE =========== //var googletag = googletag || {};            googletag.cmd = googletag.cmd || [];            googletag.cmd.push(function(){                googletag.pubads().disableInitialLoad();});var pbjs = pbjs || {};            pbjs.que = pbjs.que || [];            pbjs.que.push(function(){                pbjs.addAdUnits(adUnits);                pbjs.requestBids({                    bidsBackHandler: initAdserver,                    timeout: PREBID_TIMEOUT});});function initAdserver(){if(pbjs.initAdserverSet)return;                pbjs.initAdserverSet = true;                googletag.cmd.push(function(){                    pbjs.setTargetingForGPTAsync && pbjs.setTargetingForGPTAsync();                    googletag.pubads().refresh();});}// in case PBJS doesn't load            setTimeout(function(){                initAdserver();}, FAILSAFE_TIMEOUT);            googletag.cmd.push(function(){                googletag.defineSlot('/19968336/header-bid-tag-1', sizes, 'div-1')
                   .addService(googletag.pubads());                googletag.pubads().enableSingleRequest();                googletag.enableServices();});</script></head><body><h2>Basic Prebid.js Example</h2><h5>Div-1</h5><divid='div-1'><scripttype='text/javascript'>                googletag.cmd.push(function(){                    googletag.display('div-1');});</script></div></body></html>

pbjs.addAdUnits

事業者ごとの設定項目を、スロットごとに追加します。 実質的には、pbjs.adUnitsフィールドに渡された引数を追加しておくだけです。

Source Code: - pbjs.addAdUnits()

この際、事業者ごとの設定項目を設定する必要がありますが、全て以下のドキュメントに記述されています。

入札者ごとの設定項目ドキュメント: http://prebid.org/dev-docs/bidders.html

ただし、以下の注意点があります。

  • 事業者ごとに、パラメータの型が String / Number / Object で差異がある
    • ex. placementIdが、文字列のこともあれば数字のこともある
  • ドキュメント上は任意(optional)だが、事業者から「必ず付与してください」と言われる場合がある
  • 一部ドキュメントが古い可能性があり、そのタイミングで事業者に最新のパラメーターを聞く必要がある

pbjs.requestBids

実際に Header Bidding 入札リクエストを行っている、要のメソッドです。

  • 設定された全事業者に対してリクエストを行う準備をする
  • auctionManager クラスを通して、auction を生成する
  • auction.callBids()で実際にリクエストを行い、入札結果レスポンスが返ってきたら、callback を実行する

Source Code: - pbjs.requestBids() - auctionManager.createAuction() - auction.callBids()

pbjs.setTargetingForGPTAsync

Header Bidding 入札結果を、GPT の Key/Value に設定します。したがって、入札が完了した後に呼び出す必要があります。

Source Code: - pbjs.setTargetingForGPTAsync() - targeting.setTargetingForGPT()

Prebid.js のソースコードを追っていくとわかりますが、入札結果の存在したスロットに対して、gpt.PubAdsService.setTargeting()を呼び出しています。

/**   * Sets targeting for DFP   * @param {Object.<string,Object.<string,string>>} targetingConfig   */
  targeting.setTargetingForGPT = function(targetingConfig, customSlotMatching) {window.googletag.pubads().getSlots().forEach(slot => {// ...
      slot.setTargeting(key, value);
    })
  };

Debugging Prebid.js

Prebid.js には、公式で数々のデバッグ方法やベストプラクティスが紹介されています。主に以下のドキュメントに詳しいです。

開発者用デバッグモードやChrome Extension などのツールも揃っており、入札フローが複雑な割には比較的デバッグがしやすい印象です。

主要なものについて紹介します。

Debug Log

pbjs.setConfig API には、Debugging option が提供されています。以下のように Option を渡すと、必要十分なログを出力してくれます。

pbjs.setConfig({ debug: true});

しかし、ブラウザリロード(pbjs の再読込)の度に設定がリセットされてしまうので、ブラウザの Console から打ち込む用途として利用するのでは不便です。

一方、弊社の広告配信サーバーの JavaScript SDK のビルドプロセスでは webpack を利用しており、ビルド環境(production/staging/development)を define-pluginを用いてソースコードに埋め込んでいます。

この仕組を利用し、ステージング及び開発環境では、デフォルトでデバッグログを有効にします。

// index.jsthis.pbjs.setConfig({
  debug: process.env.NODE_ENV === "development",
})
// webpack.config.js
plugins: [/**   * DefinePlugin create global constants while compiling.   *   * @doc https://webpack.js.org/plugins/define-plugin/   */new webpack.DefinePlugin({"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
  }),
],

Snippets

Chrome で提供されている Snippetsという機能を利用し、本番環境でも手軽に入札リクエストや入札結果の中身をログに出力することができます。

これらのデータは Network タブから各事業者への入札リクエスト・レスポンスを覗くことでも確認できなくはないのですが、視認性が低いため Snippets を利用しています。

例えば、以下は Tips for Troubleshootingで紹介されている Snippets です。スロットごとに、全事業者に対する入札の結果、最終的に win した入札を表示してくれます。

var bids = pbjs.getHighestCpmBids();
var output = [];
for (var i = 0; i < bids.length; i++) {var b = bids[i];
    output.push({'adunit': b.adUnitCode, 'adId': b.adId, 'bidder': b.bidder,
        'time': b.timeToRespond, 'cpm': b.cpm
    });
}if (output.length) {if (console.table) {
        console.table(output);
    }else{for (var j = 0; j < output.length; j++) {
            console.log(output[j]);
        }}}else{
    console.warn('No prebid winners');
}

Chrome Extension

Prebid.js の公式ツールとして、Headerbid Expertという Chrome Extension が公開されています。

こちらのツールは、エンジニアだけでなくディレクターやプロジェクトマネージャーなども、気軽に自社の Header Bidding 入札結果を確認できるツールです。このツールを使うことで、事業者ごとの選別や、各社ごとのタイムアウト設定の見直し、インプレッション損失のリスクの洗い出しなどに利用できます。

f:id:itiskj:20190212120755j:plain
headerbid expert screenshhot

分析結果の見方については、Prebid.js Optimal Header Bidding Setupというドキュメントページに詳しく紹介されています。ある特定の事業者のタイムアウトに引っ張られて機会損失をしているパターン、何らかの設定ミスで DFP へのリクエストが遅れて機会損失をしているパターンなどが紹介されています。

Prebid.js Modules

Prebid.js は Module Architectureを導入しており、各事業者ごとのアダプターや通貨関連の共通処理をまとめたモジュールなどが提供されています。そして、ファイルサイズを可能な限り最小限に抑えるため、自社が必要なモジュールのみを Prebid.js Downloadページからダウンロードしたものを利用することが基本です。

Source Code: - src/modules/*.js

今回は、そのうちでも特に主要なモジュールについて紹介します。

Currency Module

Prebid.js を開発していると、入札結果の金額に関して、例えば以下のような要件が必ずと言っていいほど発生するはずです。

  • 事業者ごとに、net/gross が違うが、入札結果からオークションする前に net/gross の単位を統一したい
  • 入札金額の粒度をより細かくして、機会損失を最小限に抑えたい
  • JPY/USD などの通貨設定が事業者ごとに違うが、DFP にリクエストする前に通貨単位を統一する必要がある
  • 通貨単位を統一する場合、為替を考慮する必要がある

その場合は、公式で提供されている Currency Module を使うことになります。Currency Module を導入すると、pbjs.setConfig()に以下の設定項目を渡すことができるようになります。

Source Code: - currency.js

例えば、以下は設定例です。

this.pbjs.setConfig({/**     * set up custom CPM buckets to optimize the bidding requests.     */
    priceGranularity: "high",
    /**     * setting for the conversion of multiple bidder currencies into a single currency     * http://prebid.org/dev-docs/modules/currency.html#currency-config-options     */
    currency: {
        adServerCurrency: 'JPY',
        conversionRateFile: 'https://currency.prebid.org/latest.json',
        bidderCurrencyDefault: {
            bidderA: 'JPY',
            bidderB: 'USD',
        },
        defaultRates: {
            USD: {
                JPY: 110,
            }},
    },
});

conversionRateFileには、為替レートを変換する時に参考にする為替レートが格納されたファイルの URL を設定することができます。独自で更新したい場合は、こちらを自社の S3 Bucket などを見るようにしておいて、別途ファイルを更新するような仕組みを導入すればよいでしょう。デフォルトでは、jsDelivrと呼ばれる Open Source CDN に配置されてあるファイルを見に行くようになっています。

// https://github.com/prebid/Prebid.js/blob/master/modules/currency.js#L8const DEFAULT_CURRENCY_RATE_URL = 'https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json?date=$$TODAY$$';

もちろん、毎回 Network を通じてファイルを取得しているわけではなく、Currency Module 内部でオンメモリに為替レートをキャッシュしています。

// https://github.com/prebid/Prebid.js/blob/c2734a73fc907dc6c97d7694e3740e19b8749d3c/modules/currency.js#L236-L240function getCurrencyConversion(fromCurrency, toCurrency = adServerCurrency) {var conversionRate = null;
  var rates;
  let cacheKey = `${fromCurrency}->${toCurrency}`;
  if (cacheKey in conversionCache) {
    conversionRate = conversionCache[cacheKey];
    utils.logMessage('Using conversionCache value ' + conversionRate + ' for ' + cacheKey);
  }// ...

なお、https://currency.prebid.org/latest.jsonというファイルが、https://currency.prebid.orgにて提供されています。curl --verboseした結果が以下のとおりです。

curl --verbose http://currency.prebid.org/ | xmllint --format -
* TCP_NODELAY set
* Connected to currency.prebid.org (54.230.108.205) port 80 (#0)
> GET / HTTP/1.1
> Host: currency.prebid.org
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/xml
< Transfer-Encoding: chunked
< Connection: keep-alive
< Date: Fri, 08 Feb 2019 04:17:03 GMT
< x-amz-bucket-region: us-east-1
< Server: AmazonS3
< Age: 43
< X-Cache: Hit from cloudfront
< Via: 1.1 31de515e55a654c65e48898e37e29d09.cloudfront.net (CloudFront)
< X-Amz-Cf-Id: XEpWTG_WXRO4w44X9eIrOV2r_sR-i9EyoZpUwhIRkzXwzqr71w1GyQ==
<
{ [881 bytes data]
100   869    0   869    0     0  27899      0 --:--:-- --:--:-- --:--:-- 28032
* Connection #0 to host currency.prebid.org left intact
<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name>currency.prebid.org</Name>
  <Prefix/>
  <Marker/>
  <MaxKeys>1000</MaxKeys>
  <IsTruncated>false</IsTruncated>
  <Contents>
    <Key>latest-test.json</Key>
    <LastModified>2018-10-15T21:38:13.000Z</LastModified>
    <ETag>"513fe5d930ec3c6c6450ffacda79fb09"</ETag>
    <Size>1325</Size>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
  <Contents>
    <Key>latest.json</Key>
    <LastModified>2019-02-07T10:01:03.000Z</LastModified>
    <ETag>"6e751ac4e7ed227fa0eaf54bbd6c973d"</ETag>
    <Size>1331</Size>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
  <Contents>
    <Key>test.json</Key>
    <LastModified>2018-12-05T11:00:47.000Z</LastModified>
    <ETag>"c4a01460ebce1441625d87ff2ea0af64"</ETag>
    <Size>1341</Size>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
</ListBucketResult>

結果から、次のことがわかります。

  • Amazon S3 に格納されている
    • Server: AmazonS3
  • CloudFront で配信されている
    • X-Cache: Hit from cloudfront
  • latest.json / test.json / latest-test.jsonが提供されている

特に理由がないのであれば、こちらのファイルを使うことで概ね十分だと言えるでしょう。

また、net/gross の変換には、pbjs.bidderSettings | bidCpmAdjustmentを用います。

this.pbjs.bidderSettings = {
    bidderA: {
        bidCpmAdjustment : (bidCpm) => bidCpm * 0.85,
    },
    bidderB: {
        bidCpmAdjustment : (bidCpm) => bidCpm * 0.80,
    },
};

Integration with TAM

"Header Bidding 導入によるネットワーク広告改善の開発事情"でお伝えしたとおり、いくつかの事業者についてはすでに TAM 経由で Header Bidding 入札を行っていました。今回は、TAM と並行する形で Prebid.js の導入をする必要がありました。

以下に、全体のデータフローを示しました。parの部分で、TAM および Prebid.js 経由の Header Bidding 入札を並行して行い、両者から結果が返ってきたら DFP にリクエストします。

type description
ads 社内広告配信サーバ
display.js 広告表示用の JavaScript SDK
cookpad_ads-ruby display.js を埋め込むための Rails 用ヘルパーを定義した簡易な gem
apstag TAM の提供する Header Bidding 用ライブラリ
googletag DFP の提供するアドネットワーク用ライブラリ
pbjs Prebid.js 用ライブラリ
SSP SSP 事業者(実際は複数事業者が存在している)

f:id:itiskj:20190212120829j:plain
Sequence Diagram for Prebid.js and TAM migration

DFP にリクエストをする前に、TAM と Prebid.js 両者の入札を完了させておきたかったので、Promise.allでリクエストを行い、待ち合わせる形で実装しました。以下は、本番で利用しているコードの抜粋です(エラーやロギングなど、本質ではない行を削除したもの)。

  requestHeaderBidding(slots) {const prebidPromise = this.requestPrebid(slots);
    const apsPromise = this.requestAPS(slots);

    return Promise.all([prebidPromise, apsPromise])
      .then(() => this.headerBiddingFinishCallback())
      .catch(err => Logger.error(err));
  }

  requestPrebid(slots) {returnnew Promise((resolve) => {
      pbjs.que.push(() => {
        pbjs.addAdUnits(this.getPrebidAdUnits);

        pbjs.requestBids({
          bidsBackHandler: (result) => {
            resolve({
              type: "prebid",
              result: result || [],
            });
          },
          timeout: this.prebid_timeout,
        });
      });
    });
  }

  requestAPS(slots) {returnnew Promise((resolve) => {
      apstag.fetchBids(thihs.apstagBidOption, (bids) => {
        resolve({
          type: "aps",
          bids: bids || [],
        });
      });
    });
  }

  headerBiddingFinishCallback() {
    googletag.cmd.push(() => {
      pbjs.setTargetingForGPTAsync();
      apstag.setDisplayBids();

      googletag.pubads().refresh();
    });
  }

Conclusion

アドテク関連のエンジニア目線での事例紹介や技術詳解はあまり事例が少ないため、この場で紹介させていただきました。特に、Prebid.js は、開発自体はドキュメントが丁寧な分嵌りどころは少ないものの、実際の導入フローにおける知見は、日本においてほとんど共有されていません。そこに問題意識を感じたため、この機会に Prebid.js の導入フローを紹介させていただきました。

広告領域は、技術的にチャレンジングな課題も多く、かつ事業の売上貢献に直結することが多い、非常にエキサイティングな領域です。ぜひ、興味を持っていただけたら、Twitterからご連絡ください。

また、メディアプロダクト開発部では、一緒に働いてくれるメンバーを募集しています。少しでも興味を持っていただけたら、以下をご覧ください。

レシピのタイトルから材料を予測する🚀

$
0
0

研究開発部のサウラブです。

本稿ではユーザがレシピの作成にかける労力を減らすために取り入れた、機械学習を利用した機能の一つについて 解説します。この機能を利用すると、ユーザがレシピのタイトルを入力することで、利用されるであろう材料が予測できます。

要約

  • レシピのタイトルから材料を予測できるモデルを作りました。
  • 投稿開発部と協力してレシピエディタに材料提案機能を追加しました。

App Storeで入手可能な最新のCookpadアプリ(v19.6.0.0)でこの機能を使用できます。

モデルはどうなっているか

1. Embed

f:id:bira:20190220104854p:plain

  • 学習(Training): Word EmbeddingとSentence Embeddingを学習してS3にアップロードします。(次のセクションで説明
  • 前処理(Preprocessing): 特殊文字を削除します。 多くのCookpadユーザーはテキストに特殊文字を使用しています。 例:"✧おいしい♡タンドリーチキン♡^-^✧"に特殊文字が含まれています: , ,^-^。特殊文字には材料に関する情報が含まれていないので、それらを削除します。特殊文字を削除するには、次のpython Functionを作成しました:

コードを表示する

import re
  defremove_special_characters(text):
      non_CJK_patterns = re.compile("[^"u"\U00003040-\U0000309F"# Hiraganau"\U000030A0-\U000030FF"# Katakanau"\U0000FF65-\U0000FF9F"# Half width Katakanau"\U0000FF10-\U0000FF19"# Full width digitsu"\U0000FF21-\U0000FF3A"# Full width Upper case  English Alphabetsu"\U0000FF41-\U0000FF5A"# Full width Lower case English Alphabetsu"\U00000030-\U00000039"# Half width digitsu"\U00000041-\U0000005A"# Half width  Upper case English Alphabetsu"\U00000061-\U0000007A"# Half width Lower case English Alphabetsu"\U00003190-\U0000319F"# Kanbunu"\U00004E00-\U00009FFF"# CJK unified ideographs. kanjis"]+",  flags=re.UNICODE)
      return non_CJK_patterns.sub(r"", text)

  • トークン化する(Tokenize): MeCabを使ってテキストをトークン化します。
  • Embedding: Word EmbeddingとSentence Embedding モデルを使用して、Cookpadデータベース内の各レシピのタイトルをベクトルに変換します。
  • 索引付け(Indexing): Faissを使用してベクトルにインデックスを付け(method = IndexFlatIP=Exact Search for Inner Product)、インデックスをS3にアップロードします。Faiss(Facebook AI Similarity Search)は、ベクトルの効率的な類似検索のためにFacebook AIによって開発されたライブラリです。 Faissは10億スケールのベクトルセットで最近傍検索をサポートします。

    2. Search&Suggest (API Server)

    f:id:bira:20190220104850p:plain

  • S3からWord EmbeddingモデルとSentence EmbeddingモデルとFaiss Indexをダウンロードします。
  • Word EmbeddingモデルとSentence EmbeddingモデルとFaiss Indexをメモリにロードします。
  • Embeddingモデルを使用して、入力されたタイトルをベクトルに変換します。
  • Faissを使用してk個の類似するレシピを検索します。
  • 類似するレシピの中で最も一般的な材料を提案します。

Embeddingsを学習する:

レシピのタイトルデータでWord Embeddingモデル(Fasttext)を学習します。

gensimでFasttextを使っていました。gensimはとても使いやすいです。

コードを表示する

from gensim.models import FastText
# recipe_titles : [.....,牛乳で簡単!本格まろやか坦々麺,...]# tokenize recipe titles using MeCab and then train fasttext model# recipe_title_list(tokenized) : [...,['牛乳','で','簡単','!','','本格','まろやか','坦々','麺'],....]
ft_model = FastText(size=100,min_count=5,window=5,iter=100, sg=1)
ft_model.build_vocab(recipe_title_list)
ft_model.train(recipe_title_list, total_examples=ft_model.corpus_count, epochs=ft_model.iter)

なぜFasttextを選んだのですか?

Fasttext(これは本質的にword2vecモデルの拡張です)は、各単語を文字n-gramで構成されているものとして考えます。 そのため、単語ベクトルは、これらの文字数n-gramの合計で構成されます。例:”中華丼”の単語ベクトルはn-gram”<中”、”中”、”<中華”、”華”、”中華”、”中華丼>”、”華丼>”のベクトルの合計です。Fasttextはサブワード情報で単語ベクトルを充実させます。それゆえ: - 稀な単語に対してもより良いWord Embeddingsを生成します。たとえ言葉が稀であっても、それらの文字n-gramはまだ他の単語中に出現しています。そのため、その Embedding は使用可能です。例:”中華風”は”中華丼”や”中華サラダ”のような一般的な単語と文字n-gramを共有することは稀であるため、Fasttextを使用して適切な単語のEmbeddingを学習できます。 - 語彙外の単語 - 学習用コーパスに単語が出現していなくても、文字のn-gram数から単語ベクトルを作成できます。

Sentence Embeddingモデルを学習します。

二つの Sentence Embedding モデルを試してみました:

  • Average of Word Embeddings:文は本質的に単語で構成されているので、単に単語ベクトルの合計または平均を取れば文のベクトルになると言えるかもしれません。 このアプローチは、Bag-of-words表現に似ています。これは単語の順序と文の意味を完全に無視します(この問題で順序は重要でしょうか?🤔)。

コードを表示する

import MeCab
  VECTOR_DIMENSION=200
  mecab_tokenizer_pos = MeCab.Tagger("-Ochasen")
  defsentence_embedding_avg(title, model=ft_model):
      relavant_words = [ws.split('\t') for ws in mecab_tokenizer_pos.parse(title).split('\n')[:-2]]
      relavant_words = [w[0] for w in relavant_words if w[3].split('-')[0] in ['名詞', '動詞', '形容詞']]
      sentence_embedding = np.zeros(VECTOR_DIMENSION)
      cnt = 0for word in relavant_words:
          if word in model.wv
              word_embedding = model.wv[word]
              sentence_embedding += word_embedding
              cnt += 1if cnt > 0:
          sentence_embedding /= cnt
      return sentence_embedding

  • トークン化する(Tokenize): MeCabを使用して文を形態素解析します。
  • フィルタ(filter) :名詞、形容詞、動詞だけを残して、他の単語を除外します。
  • 平均(Average): フィルタ処理した単語のWord Embeddingを取得し、それらを平均してタイトルベクトルを取得します。

  • Bi-LSTM Sentence Embeddings: Cookpadのレシピデータを使って教師あり学習によってSentence Embeddingを学習します。ラベルは2つのレシピ間のJaccard Similarityから導き出します。レシピを材料のセットと見なすと、2つのレシピ間のJaccard Similarityは次のように計算されます。 f:id:bira:20190220115601p:plain

    アイデアは、それらの間の高いJaccard Similarityを持つレシピのレシピタイトルベクトルをSentence Embeddingスペース内で互いに近くに配置することです。

    • データセットを作成します: 2つのレシピのタイトルと、これら2つのレシピの類似度を表すJaccardインデックスを含む各サンプル行を持つデータセットを作成します。{title_1, title_2, Jaccard_index}
    • 下のネットワークを学習します: f:id:bira:20190220104951p:plain上記のネットワークは2つの設定で学習することができます:
      • Regression: g(-) : sigmoid と y = Jaccard Index
      • Classification: g(-): dense+dense(softmax) と y = Jaccardインデックスから派生したクラスラベル 5クラスの分類設定で上記のネットワークを学習することによって学習されたF( - )は、最もよく機能するようです。ネットワークにとって、回帰問題よりも分類問題の方が解きやすい場合があります。

      Kerasでネットワークを実装する:

コードを表示する

from keras import backend as K
    from keras import optimizers
    from keras.models import Model
    from keras.layers import Embedding, LSTM, Input, Reshape, Lambda, Dense
    from keras.layers import Bidirectional
    import numpy as np
    defcosine_distance(vects):
        x, y = vects
        x = K.l2_normalize(x, axis=-1)
        y = K.l2_normalize(y, axis=-1)
        return K.sum(x * y, axis=-1, keepdims=True)

    title_1 = Input(shape=(MAX_SEQUENCE_LENGTH,))
    title_2 = Input(shape=(MAX_SEQUENCE_LENGTH,))
    word_vec_sequence_1 = embedding_layer(title_1)  # Word embedding layer(fasttext)
    word_vec_sequence_2 = embedding_layer(title_2)  # Word embedding layer(fasttext)
    F = Bidirectional(LSTM(100))
    sentence_embedding_1 = F(word_vec_sequence_1)
    sentence_embedding_2 = F(word_vec_sequence_2)

    similarity = Lambda(cosine_distance)([sentence_embedding_1, sentence_embedding_2])
    similarity = Dense(5)(similarity)
    y_dash = Dense(5, activation='softmax')(similarity)
    model = Model(inputs=[title_1, title_2],  output=y_dash)

    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    model.fit([train_title_1, train_title_2], y)  # [train_title_1, train_title_2], y are respectively input titles and class label
    np.save('bilstm_weights.npy', F.get_weights())

  • 前のステップで学習したF(-)を文のEmbeddingとして使用します:

コードを表示する

from keras.models import Model
    from keras.layers import Embedding, LSTM, Input, Reshape, Lambda, Dense
    from keras.layers import Bidirectional
    import numpy as np

    title = Input(shape=(MAX_SEQUENCE_LENGTH,))
    word_embedding = embedding_layer(title)
    F = Bidirectional(LSTM(100))
    sentence_embeddding = F(word_embedding)
    sentence_embedding_model = Model(input=title, output=sentence_embedding)

    sentence_embedding_model.layers[2].trainable = False
    sentence_embedding_model.layers[2].set_weights(np.load('bilstm_weights.npy'))
    defsentence_embedding_bilstm_5c(text):
        txt_to_seq = keras_tokenizer.texts_to_sequences([mecab_tokenizer.parse(text)])
        padded_sequence =  sequence.pad_sequences(txt_to_seq,maxlen=MAX_SEQUENCE_LENGTH)
        return K.get_value(sentence_embedding_model(K.cast(padded_sequence,float32)))[0]

結果

以下はサービスにおける利用率です。例えば、3 out of 5 suggested ingredients matches actual は 5 個 suggest したうち 3 個が利用された割合です。

3 out of 5 suggested ingredients matches actual(%) 2 out of 5 suggested ingredients matches actual(%)
Average of word embeddings 53% 80%
Bi-LSTM Sentence Embeddings 50% 76%

Average of word embeddings(これはBag-of-Wordsに似ています)はBi-LSTM Sentence Embeddingよりもこの問題に適しています。これは、レシピのタイトルは短いテキストであるために、単語順序の情報は材料を予測するのにはあまり役に立たないからだと思われます。

まとめ

  • レシピのタイトルから材料を予測できるモデルを作りました。
  • 投稿開発部と協力してレシピエディタに材料提案機能を追加しました。

いかがでしたでしょうか。 Cookpadでは、機械学習を用いて新たなサービスを創り出していける方を募集しています。 興味のある方はぜひ話を聞きに遊びに来て下さい。

春のインターンシップ自作キーボードコースのカリキュラムをちょっとだけ見せちゃいます!

$
0
0

Cookpad Spring 1day Internship 2019 自作キーボードコース、講師の KOBA789 です。

(インターンシップの正式名称長いな)

私事ですが、最近全自動洗濯乾燥機を買って生活が変わりました。

さて、募集開始時、にわかに界隈をお騒がせしておりました自作キーボードコースですが、気づけばもう当日まで1ヶ月を切ってしまいました。

講師の私は今必死に講義内容の準備をしているわけですが、今回は特別に、そのカリキュラムの一部をご紹介します。

そもそもどんな内容のコースなの?

募集ページには

このコースでは、キーボードの仕組みをハードウェア・ソフトウェアの両面から解き明かし、究極のキーボードをゼロから自作できる技術を身につけます。

Cookpad Spring 1day Internship 2019 | クックパッド 採用情報

と、大変抽象的な記述をしていました。

"ハードウェア・ソフトウェアの両面から解き明かし"って具体的に何よ、と疑問に思われたかと思います。

本コースでは、自作キーボードを設計するにあたり、「自作キーボード」という切り口ではたどり着きづらい知識・技術にフォーカスを当てて講義を進めていきます。

自作キーボードの設計方法については、他の方が書かれたよい資料がすでにいくつかあります。 キースイッチ・キーキャップの種類やキーのレイアウト、自作キーボードで一般的なマイコンやそのファームウェアなどの情報についてはそれらの資料で知ることができます。

実際、私もいくつか読ませていただき、大変参考になりました。

しかし、電子回路一般の知識やマイコンの仕様、USB のプロトコルなどについては、初学者にとってはどこから学んでいいものか皆目見当のつかない世界となっています。 このコースでは、自力だと入り口すらもわかりづらいような領域へ受講者のみなさんをご案内します。

はんだ付けをします

ほとんどの方の予想どおり、はんだ付けをします。

私の持論では、自作キーボードにとってはんだ付けは特に本質的な作業ではないと思っているのですが、とはいえ電子回路であるキーボードを組み立てるための手段としてのはんだ付けは便利であり、大切です。

部品を手にとってひとつひとつはんだ付けしていく中で、個々の部品をよく観察することにもなり、ハードウェアとの心の距離を近づけるための最初の一歩として適切でしょう。

また、なんであれ自ら苦労して組み立てたものに対する愛着というのは特別です。

キースイッチを分解します

キーボードの本質は何かと聞かれたら、それはやはり指先と電子回路の間にある部分、つまりキートップとキースイッチということになります。

メカニカルキーボードにおいて、我々の指と電子回路の世界をつなぐのはキースイッチしかありません。

そのキースイッチをよく観察することはキーボードを理解することであり、巨大な電子回路であるコンピュータと我々の関係性を見つめ直すことであります。

簡単な電子回路を学びます

キーボードは電子回路としてはとても簡単な部類ですが、とはいえ電子回路であることに違いありません。

いくら既存の事例を知っていたとしても、自分で設計をするときに最後に頼りにできるのは原理・原則だけです。

自分で回路を引くときに自信を持って引けるように、しっかり基礎を押さえます。

基板の設計を学びます

プリント基板を設計・製造してそこに部品を実装する、というのは自作キーボードのもっとも簡単な実装方法だと思います

なぜなら、多くのキースイッチがその実装方法を想定して設計されているからです。

本コースでもそれに倣い、KiCad を使って基板の設計をします。

USB HID について学びます

現代でキーボードを接続するといったらやはり USB を用いることになるでしょう。

USB の信号線はなぜ2本なのか、どんなプロトコルなのか、なぜ USB キーボードは6キーまでしか同時押しできないと言われているのかなど、様々な疑問にロジックアナライザと仕様書で答えていきます。

ファームウェアを自作します

本コースでは既存のファームウェアを利用しません。

講師がスクラッチから書き上げた USB プロトコルスタック込み1000行程度の小さなコードをベースに、自分だけのファームウェアを開発します。

講師が私(KOBA789)なので、実装言語は当然……わかりますよね?

これで全てではありません

以上だけでも既に1日で終わるのか怪しいボリュームになっていますが、ここに書いていない内容も計画しています。

最終的にどんなインターンシップになるのか(なったのか)は、きっと開催後のレポートでお伝えすることができると思います。

それではまた!

おまけ

先日、インターンシップ当日に使う部材を買い出しに秋葉原に行ってきました。

中央で後頭部が写っているのが私です。

f:id:koba789:20190220172616j:plain
みんな大好き秋月通商

試作用のキースイッチやキーキャップを買うため、遊舎工房にもお邪魔してきました。

f:id:koba789:20190220174958j:plain
遊舎工房の看板

そして、最後の1枚はチームメンバー4人で試作をしている様子です。

f:id:koba789:20190222123337j:plain
試作の様子
講義で当日使う基板が写っている部分はぼかしています。どんなキーボードを作るのかは当日のお楽しみ!

4人ともちゃんと動かすことができたため、きっと当日も大丈夫だと思います。


【開催レポ】Cookpad.apk#2を開催しました

$
0
0

こんにちは、Androidエンジニアしている吉田です。 2019/02/18(月)に弊オフィスで開催されたCookpad.apk#2の様子についてご紹介します。前回の様子については 下記のエントリを御覧ください。

techlife.cookpad.com

今回は弊社のエンジニア6名が発表致しました。本ブログを通して当日の様子をご来場いただけなかったみなさまにもお届けしたいと思います。

児山 千尋 「Androidアプリエンジニアの基礎知識」

f:id:kazy1991:20190225125527j:plain
Androidアプリエンジニアの基礎知識

はじめはモバイル基盤部のこやまカニ大好きからAndroidアプリエンジニアが知っていて欲しい基礎知識についての発表がありました。 Androidアプリエンジニアの知識とはFragmentのライフサイクルを正確に理解しているか?といった開発に直接関わる所だけに限らず、

  • Androidそのものに関する知識
  • Androidアプリの実装する上で必要な知識
  • ビジネス的な判断をサポートできる知識

の3つの観点からAndroidエンジニア全員に知っていてほしい知識が詰め込まれた内容が紹介されました。

篠原 弘光「Android Things Overview」

f:id:kazy1991:20190225134427j:plain
Android Things Overview

二人目はクックパッドマートshanonからAndroid Thingsについての発表がありました。 Android Thingsといえばつい最近衝撃的なニュースで話題になりましたが、詳細についてはご自身の目でスライドをお確かめください。

柴原 直也「cookpadTV のモジュール構成について」

f:id:kazy1991:20190225140349j:plain
cookpadTVのモジュール構成について

三人目はcookpadTVのFireTV/AndroidTVアプリを担当する梨原からモジュール構成について発表がありました。 モダンなAndroidアプリではマルチモジュールが当たり前になりつつありますが、その目的は様々です。cookpadTVでは様々なプラットフォーム対応しつつ高速に開発する手段としてマルチモジュールが採用されました。こちらも詳細は資料をご参照ください。

加藤 恭平「Espresso Driver を用いた Appium テストとその仕組み」

f:id:kazy1991:20190225142140j:plain
Espresso Driver を用いた Appium テストとその仕組み

四人目は技術部品質向上グループの加藤からテスト周りの技術であるEspresso DriverとAppiumの紹介がありました。 Appiumは人気の高いモバイルアプリケーション向けのE2Eテストフレームワークです。以前はUIAutomatorと組み合わせて使うことが一般的でしたが、内部状態に干渉できないため通信が終わるまでsleepを入れるなどのノウハウが必要でした。Espresso Driverが登場したことでContextにアクセスできるなどより高度な操作が可能になりました。

安部 建二「Dynamic feature moduleの基本」

f:id:kazy1991:20190225142537j:plain
Dynamic feature moduleの基本

五人目はKenji AbeからDynamic feature moduleについて発表がありました。 Dynamic feature moduleはアプリの機能の分割することでアプリ本体を小さく提供し、ユーザーが必要となったタイミングで各機能の実装を配信する仕組みです。本スライドでは実際にDynamic feature moduleをアプリに組み込んだ際のプラクティスや課題点が詳しく書かれています。

宇津 宏一「Kotlin Multiplatform Libraryのあれこれ」

最後は決算基盤グループのuzzuからKotlin Multiplatform Libraryについて発表がありました。 Kotlinにはマルチプラットフォーム構想がありJVMやAndroid環境以外でも動作する環境が提供されています。kotlinベースの複数環境を想定したプロジェクトを通称MPP(Multi Platform Project)と呼びます。クックパッドでは一部機能において「まずはAndroid向けのライブラリを開発できる事」をターゲットにMPPを利用し始めており、本スライドではMPP対応ライブラリを作るためのTipsや通常のAndroidプロジェクトからの利用にどう対処するのかなどコアな内容が紹介されています。

おわりに

ここ数年でAndroid開発は比較的安定した時期に入ったのかなと言う印象でしたが、今回のcookpad.apkではかなり尖った内容が多かったと思います。私達も試行錯誤を繰り返している最中で課題も多くありますが、スライドだけでは十分に伝わらなかった部分を議論したい、クックパッドの技術に興味を持ったという方がいらっしゃればぜひ弊社に遊びに来てください。
またクックパッドではAndroidエンジニアを随時募集しています。ご興味を持って頂けた方のご応募をお待ちしています。

マイクロサービス化を支える継続的切り替え術

$
0
0

こんにちはこんにちは。技術部のクックパッドサービス基盤グループのシム(@shia)です。グループ名が大きいですね。

クックパッドで運営しているサービスの中、一番古くから存在しているレシピサービス (cookpad.com) ——以下このサービスのコードベースを cookpad_all と呼びます——があります。 クックパッドサービス基盤グループはこのレシピサービスの運用及び改善という責務を持つグループとして今年の2月に発足しました。 わかりやすい業務の一つとしてはお台場プロジェクトが挙げられます。 お台場プロジェクトに関しては昨年12月の最後を飾った青木さんの クックパッド基幹システムのmicroservices化戦略 〜お台場プロジェクト1年半の軌跡〜という素晴らしい記事があるので紹介は省きます。

お台場プロジェクトの一つとして、僕は最近 cookpad_all からフィーチャーフォン向けのサービスである「モバれぴ」を分離するという作業をしています。このサービスはコードベースが大きいので、どうやって元のコードベースから新しい方に切り替えて行くのかが非常に大事です。この記事では現在進行形である、モバれぴの切り替え戦略に関して紹介してみようと思います。

始める前に

この記事でのサービス分離作業は巨大な一つのコードベースから、他に対する依存が少ない、もしくは関係のない機能・サービスなどをマイクロサービスとして分離することを意味します。ですので

  • コードベースが完全に分離される
  • 内部的には別のサービスとして提供する
  • 切り出し元と通信が必要であれば HTTP API や gRPC などの方法を用いる

を前提にしてお話していきます。

コードベースの切り替え戦略を考える

コードベース切り替えには大きく2つの戦略があります。 いわゆるビックバンデプロイと小分けにして継続的にデプロイしていく、というものですね。

モバれぴは完全に作り直すことにしたため、まずビックバンデプロイは無理だと判断しました。 仕様が同じとはいえ実装詳細が変わる以上、必ずなにかの問題が起こります。問題が起きると全体をロールバックするしかないので、全体をリリース & ロールバックが繰り返される状況になり作業は進まなくなるし、リリースのたびにユーザーに迷惑をかけることになります。 であれば取れる戦略としては後者のみですね。

昨年に取り組んだよく似ている事例として、iPhone/Android アプリ向け機能「料理きろく」のコード(API エンドポイント)を cookpad_all から1つのマイクロサービスとして切り出す(分離する)作業を行いました。 その時もビックバンデプロイは絶対避けたかったので、切り出し元であるサービスの手前でリバースプロキシとして存在している NGINX を利用し、新しいコードベースへ流すエンドポイントを指定できるようにし、これを増やしていくという方法を取りました。

NGINX の location で上流を切り替える戦略

f:id:riseshia:20190305074302p:plain
location を利用する切り替え戦略

この図では赤い矢印が切り替え完了したエンドポイントに対するリクエストで、青い矢印が切り替えが終わってないエンドポイントに対するリクエストです。 まずリクエストは ELB を通じてその後ろのリバースプロキシの役割を担当する NGINX へ流れます。そこでリクエストのパスを見て切り出し元にリクエストを流すか、切り出し先のサーバに流すかを決めるわけですね。 一つのエンドポイント(ここでは /v1/recipes)の実装を終えて、本番に投入したい状況になったとしましょう。このエンドポイントに対して以下のような NGINX 設定を書きます。

location ~ ^/v1/recipes {
  proxy_pass <切り出し先>;
}
location / {
  proxy_pass <切り出し元>;
}

このように location で新コードベースに流したいエンドポイントのパスを追加し、proxy_pass でリクエストを流せばいいわけです。

NGINX の location を利用した切り替え戦略の問題

すでにお気付きの方もいると思いますが、この方法はインフラ環境を頻繁にいじる必要があるという問題があります。

まず、本番環境のインフラをいじるだけで障害が発生するリスクが生じます。 そして特定のエンドポイントを切り替えるのにインフラ側とアプリ側のオペレーションが両方発生すること自体が不便です。 エンドポイントを一つ切り替えるだけなのに、新アプリをデプロイして、 NGINX をリロードして……エンドポイントが 20個ある場合、真面目にやっていくとしたらこれを 20回しないといけません。気が遠くなる面倒さです。 ということで手抜きをし始めると、1回に1個以上のエンドポイントを切り替えるようになるわけですが、これはビックバンデプロイに近づく結果になります。言い換えると、ロールバックの確率が高くなるとも言えるでしょう。 今考えるとエンドポイントの数が多くなかったとはいえ、よく頑張ったな〜という気持ちになります。

前回はインフラの作業コストとコピペに近い移植方式を考慮した結果、コントローラー単位で切り替えるようにしていました。 ですがモバれぴはビューの存在とすべてのリソースを API 経由で取得するというポリシーにしたため、作業コストが跳ね上がっていました。結果、いちいち移行していたらインフラ作業コストも跳ね上がるはずなので、いつまで経っても作業が進まなくなるのは明らかでした。

モバれぴでの切り替え戦略

前回、料理きろくの分離作業から得た教訓は

  • 切り替えでインフラオペレーションを使いたくない
  • ロールバックが簡単にできるようにしたい
  • 1アクション単位で切り替えたい

というものでした。つまり、切り替え作業コストを最小限に減らしたい。 これらの問題はすべてのリクエストを新コードベースに送り、新コードベースの方で、未実装の場合は切り出し元に問い合わせるよう、503(Service unavailable) ステータスコードを返すことで NGINX に要求することで解決できます。実際どんな感じにリクエストが処理されるのか図を見てみましょう。

f:id:riseshia:20190305074333p:plain
503 & proxy_next_stream を利用する切り替え戦略

赤い矢印が切り替え完了したエンドポイントに対するリクエストで、青い矢印が切り替えが終わってないエンドポイントに対するリクエストです。 図からもわかるように NGINX は 503 ステータスコードを受け取ったら切り出し元へリトライするだけです。どのリクエストを旧コードベースに流すか決めるのは切り出し先なので、こっちのコードを変更するだけで切り替え作業を完了します。

この方式は upstream の server の backup モードと proxy_next_stream 設定を利用すれば実装が可能です。具体的な実装方法の前にこれらの機能に関して説明します。

NGINX upstream のサーバ状態

まずは NGINX の upstreamに対して簡単に説明をしたいと思います。

NGINX の upstream は幾つかのサーバ群を一つのグループとして定義することができ、その内部でよしなにロードバランシングを行うことができます。さらに、グループとして定義した各サーバに対してどういうロードバランシング戦略をとるか、ラウンドロビンならどれくらいの重みでリクエストを流していくかなど、それなりに細かい設定を行うことができます。そしてここにはサーバの状態の扱いも含まれています。

upstream 内部で定義されたサーバの状態は2つ存在します。リクエストを受けられる状態(available)、リクエストを処理できない状態(unavailable)ですね。これはどういう条件で変化するのでしょうか? 答えは server ディレクティブで設定した fail_timeout(デフォルトは 10s) と max_fails(デフォルトは 1) にあります。 サーバに対して max_fails 回リクエストが失敗したら fail_timeout の間、そのサーバを unavailable とします。具体的な例を見てみましょう。

upstream backend {
    server backend1;
    server backend2;
}

このような設定があり、2つのサーバが仲良くリクエストを処理している状態から backend2 が何かしらに理由でリクエスト処理に失敗し、 unavailable 状態になりました。その後の 10秒の間は backend に送られるすべてのリクエストは backend1 が処理します。 ちなみに max_fails=0 にする場合、リクエスト処理に失敗したサーバは unavailable にはならず、常に available 扱いされるようになります。

ここまでの説明で質問が2つくらい浮かび上がると思います。

  • 失敗したリクエストはどう扱うのか?
  • そもそもここでの失敗は何を意味するのか?

失敗したリクエストは利用可能なサーバがあればそこへ再度流されます。これは NGINX の内部の挙動で、ユーザー側からは1リクエストとして認識される、ということを忘れないようにしましょう。もし利用可能なサーバが存在しなければ?バックアップのサーバが存在するのならそれを使います。バックアップは名前からも推測できると思いますが、普段は使われません。

upstream backend {
    server backend1;
    server backend2 backup;
}

このような設定がある場合、正常な状態では backend1 のみにリクエストを流します。もし backend1 が unavailable になるとその時初めて backend2 が利用されるようになります。 backend1 が available 状態に戻ったら backend2 はまた利用されなくなります。

そして NGINX はどのようなレスポンスを失敗として扱うのか。 これに関する回答は proxy_next_upstream (脚注: 使ってるモジュールによっては使える設定が違います。 http://nginx.org/en/docs/http/ngx_http_upstream_module.html#serverの max_fails オプションの説明に一覧が乗っているので参考してください)にあります。 この設定からはなにを持って失敗と定義するかを決められます。

デフォルトでは大雑把に言うとサーバにリクエストを流して NGINX がレスポンスを受け取りヘッダーを確認するまでの通信の中、なにか問題が起きれば失敗です。つまり、アプリが側の処理結果ではなく、通信がちゃんとできているかで判定している感じですね。 もちろん設定を変更することで 500、502、503 などのステータスコードを受け取った場合も失敗として扱うことが可能です。例えばサーバが 503 を返した場合を失敗扱いしたいのであれば

server {
  proxy_next_stream http_503;
}

のようにすればいいです。他のオプションや詳しい説明が必要であればドキュメントを参照してください。

503 と proxy_next_stream を組み合わせる

前述した方法を具体的に説明すると

  • 切り出し先で未実装なエンドポイントに対してすべて 503 を返す
  • 手前の NGINX では backup として切り出し元のサーバを指定し、 proxy_next_stream で 503 をリクエスト失敗と判定し、切り出し元にリトライする

ということをすればいいです。コードを見ていきましょう。

切り出し先では実装されてないエンドポイントなら形だけ作り 503 を返すようにします。 Rails であればコントローラに以下のような感じで書けます。

class RecipesController
  prepend_before_action :not_implemented, except: %i[edit]
  def create
    # ...
  end
  def show
    # ...
  end
  def edit
    # 移植完了したアクションでは普通のレスポンスを返せる
  end

  def not_implemented
    head :service_unavailable
  end
end

次は NGINX の設定ですね。

upstream backend {
    server new_backend max_fails=0;
    server old_backend backup;
}
server {
  proxy_next_stream http_503 non_idempotent;

  location / {
    proxy_pass backend;
  }
}

まずは max_fails を 0 にすることで、 new_backend が unavailable 扱いされることなくすべてのリクエストを処理するようにします。 そして proxy_next_stream で 503 ステータスコードを受け取ったら次のサーバにリトライするようにします。 old_backend を backup として設定するのは、リクエストに失敗したときのみ使われるようにするためです。 non_idempotent オプションを使うのは冪等ではないリクエスト(POST、PATCH など)の場合でもリトライを有効にするためです。 [脚注: non_idempotent を使っていいのか、の質問があると思いますが、この場合起りえる 503 は二種類があり、一つがこちらで意図して返す 503(未実装だよ〜)、新サービスの方から応答がないという本当の意味での 503 ですね。どちらの場合もリソースの処理をしないのでおかしな実装をしないかぎり問題ありません。]

ちなみになぜ 503 なのかといえば、

  • アプリケーション側から返す可能性が低い
  • proxy_next_stream でリトライすることができる
  • コードの意味がそれなりに自然に見える

という基準で選択しました。

503 と proxy_next_stream を利用した切り替え戦略の問題点

さて、最初のお気持ちをもう一回思い出してみます。

  • 切り替えにコストを使いたくない
    • head :service_unavailableを削除してデプロイすれば勝手に切り替わるので切り替えにかける追加コストはほぼゼロになりました
  • ロールバックを簡単にしたい
    • 通常のロールバックをするだけなので追加コストはゼロです
  • 1 アクション単位で切り替えたい
    • アクション単位で切り替えられるようになりました

すべての問題点を解決できていますね。ただ全てが便利になるのか?というと違います。例えば、以下のような不便さがありえます。

継続的な切り替えの場合は対象エンドポイント周りの切り替え作業中にはウェブ上のインターフェース(i.e. フォームで渡す引数の名前、ルーティングパスなど)を変えたい場合は慎重になる必要があると思いますが、この戦略の場合は特に切り替えの単位が細かいので、もっと気をつける必要があります。例えばフォームは旧サービスから返すのに、提出は新サービス、という状況もあり得るためですね。 同じ理由で、両サービスのルーティングテーブルは常に同期しておくのをおすすめします。

ちなみにモバれぴの場合、前述したようにアクティブな開発は行われていないため、この制約によるデメリットはありませんでした。

最後にレイテンシーに対する心配もあると思います。移植されてないエンドポイントの場合、 NGINX から新サービスへの 1RTT が無駄になるからですね。しかも Rails のミドルウェアを一周します。モバれぴでは、

  • リクエストの処理時間は ActiveRecord などの IO や、ビューレンダリングが支配的なのでこのように 503 を返すだけなら、そこまでは影響しないはず
  • ある程度のレスポンスの遅れは許容する
  • 移植作業が進むにつれ、その遅れは自然解消する

ということからこの方式で問題ないと判断しました。そして以下がその結果です。

f:id:riseshia:20190305074353p:plain
導入前後のレイテンシーの変化

これはモバれぴの一番前にある ELB の平均レイテンシーのグラフです。 1/17 の昼にてこの戦略のための NGINX 設定変更が行われますが、誤差の範囲に収まってるような印象を受けるので、大抵の場合は問題がないと思っても良いでしょう。

まとめ

この記事ではモバれぴを cookpad_all から分離する中、切り替え作業を継続的かつ小コストで進めるために選択した戦略、そしてそこに至った経験談、それに必要な背景知識などを説明しました。

もしこの話でお台場プロジェクトに興味が湧いたのであればぜひご連絡ください!

NPSアンケートを自動分類した話

$
0
0

研究開発部に2月18日から3月15日までアルバイトとして参加している岩手県立大学修士1年の橋本(@b_b_134)です。大学では画像認識について学びを深めています。

本稿では、アルバイトで行ったNPS®アンケート1の機械学習を用いた分類と、その結果を社内で活用してもらうための取り組みについて紹介します。

NPSアンケートについて

クックパッドではイベントやアンケートなどを通じてユーザーからの声を集計しています。その一環として、2年前からNPSアンケートを実施しています。

NPSアンケートはサービス(今回はクックパッド)を他者にオススメするかを調査するのが目的です。アンケートでは必須回答の「スコア」と任意回答の「コメント」を頂いており、スコアは0~10の値を取ります。

この「スコア」が9、10で回答してくださった方を推薦者、7、8で回答してくださった方を中立者、6以下で回答してくださった方を批判者と定義します(下図参照)。

f:id:kazuyuki-hashimoto:20190315174636p:plain

「NPS」の定義は推薦者の割合から批判者の割合を引いた値になります。

「コメント」は実に様々な意見が寄せられます。従業員全員が全て目を通せると良いのかもしれませんが、難しい話です。しかし、サービス改善に役立てられる貴重な声を無視することはできません。

現在は、社内の担当者が分析し社内で報告しています。社内ではこのアンケートをより一層活用したいという気運が高まっていました。私個人としてもユーザーがサービスについてどう思っているのかということに興味があったため、アルバイトで取り組むタスクに定めました。そこで、現在分析に携わっているスタッフとミーティングし、要件をまとめていきました。要件としては以下が挙がりました。

  • 頂いたコメントが何を話題にしているかを分類したい
  • ある話題に関するコメントのNPSの遷移をグラフなどで見たい

「推薦者はどこに魅力を感じているか」「批判者はどこに不満を感じているか」をより定量的な評価が可能になります。また、機能やサービスに関して早くフィードバックを得ることができます。

これまでも「頂いたコメントが何を話題にしているか」についてはルールベースおよび目視で分類してきました。しかし同時に、目視での作業量の多さなどから分類コストが大きいという問題がありました。そこで我々は機械学習によって分類コストを低減しました。

分析とデプロイ

では、NPSに関する課題を機械学習でどう解決したかを説明します。本プロジェクトは分析とデプロイ、二つのステージに分けられます。

ここで分析はコメントがどういった話題に関するものなのか予測するモデルを作成することを指します。 デプロイは作成したモデルをもとにアプリケーションを作成することを指します。ではまず分析でやったことを紹介します。

分析

今回は各NPSアンケートに含まれるテキストがどういった話題に関するものなのか機械学習を使って自動でラベル付けするモデルを作ります。過去に「検索」「レシピの量」など18種類に社内で人手でラベル付けしたデータがあったため、それを教師データとして教師あり学習を行いました。教師あり学習とは、あるデータに対して正解がわかっているとき、モデルがデータをもとに予測できるようにし、未知のデータが渡されたときに正しく予測出来るようにモデルを訓練することです。

構築したモデルを利用して、ラベルの付いていないコメントにラベルを付けます。今回はマルチラベル問題として取り組み、二値分類器を各話題ごとに構築して独立したラベルを付与しました。 このやり方は以前のtechlifeの記事ご意見分類業務と同様です。(下図参照)

f:id:kazuyuki-hashimoto:20190315174630p:plain

文章のベクトル化

まずは、アンケート文章を分かち書きし、ベクトル化します。 分かち書きは、以下のように単語ごとに文章を分割することです。

(元の文)
毎日の料理を楽しみにする。

(分かち書き後)
毎日 の 料理 を 楽しみ に する 。

導入の容易さから分析時点では分かち書きのフレームワークとしてJanomeを選択しました。今回はベクトル化ではTF-IDFを採用しましたが、将来的にfastTextやWord2Vecとの比較も考えています。

分類

機械学習でよく使われるscikit-learnの公式ドキュメントにはChoosing the right estimatorというページがあり、今回モデルの選定に利用しました。具体的には対象となるデータはラベルつきのデータがあり、かつ量が多くないためLinear SVCを選択しました。

ここでデータの内容を見てみましょう。

ラベル件数
機能 / 検索 251/1143
レシピ/量 341/1143
レシピ/難易度 152/1143
レシピ/味 177/1143
サービス全般/有料 124/1143
サービス全般/ユーザビリティ 45 /1143
サービス全般/役立ち度 463 /1143

上の表は全コメントに対して各ラベルが付与された比率が記述されています。なお、ラベルは1つのコメントにつき複数のラベルが付いていることがあります。サービス全般/役立ち度のように、正例が多い話題(463件)もあります。しかし、50件に満たないラベルもある不均衡なラベルもあります。

分析をし始めた当初、評価指標については、最初はAccuracyを見ていました。 しかし後日になって、社内のNPS担当者はラベル付けされたデータをさらに人手でチェックして、NPSデータを分析することが判明しました。 そのため、Accuracyを評価指標として使うより、関連する可能性のあるコメントを確実にラベル付けができるようにRecallを重視するほうがよいと考えました。 ちょうどscikit-learnにはclass_weightというパラメータがあり正例と負例の重みを調整できるパラメータが提供されています。 今回の実験ではこのパラメータ class_weightを利用してRecallの重みを大きくした場合についても合わせて検証しました。 単純なグリッドサーチを用いた場合とclass_weightを用いた場合の結果を以下の表にまとめます。

ラベル Recall
(GS)
Recall
(GS+CW)
f1
(GS)
f1
(GS+CW)
機能 / 検索 0.7500 0.8064 0.8413 0.8650
レシピ/量 0.7311 0.7999 0.8051 0.8241
レシピ/難易度 0.7804 0.8555 0.8707 0.8750
レシピ/味 0.5420 0.6600 0.6863 0.6000
サービス全般/有料 0.6712 0.6835 0.7967 0.7999
サービス全般/ユーザビリティ 0.4482 0.6071 0.6190 0.7555
サービス全般/役立ち度 0.8507 0.8505 0.8718 0.8868

※GS: GridSearch, CW: class_weight

上の表にはRecallとF1スコアが記述されており、それぞれ、GridSearchのみ、およびGridSearchとclass_weightを組み合わせた結果です。 上の表をみるとラベルによって、性能が出ているものと出ていないものがあるのがわかります。 十分なデータを確保できていないラベルに関しては性能が十分に出せていません。 しかし、自動分類の効果を確認してもらうため、まずは性能の高いラベルについてのみデータを分析者が見られるようにデプロイすることにしました。

デプロイ

前節で、プロジェクトの分析ステージが終わったので、デプロイに取り掛かります。 クックパッドでは研究開発部の各メンバーがデータの分析からモデルのデプロイまでの責任を持ちます(詳しくはこの記事を参照してください)。

今回のタスク、もともとの要件は以下の通りでした。

  • 頂いたコメントが何を話題にしているかを分類したい
  • ある話題に関するコメントのNPSの遷移をグラフなどで見たい

分析が終了しモデルが構築できたので、分類したデータをRedShiftに入れ、担当者が閲覧できる状態にすることを目標とします。

機械学習の結果をシステムに組み込む方法は逐次処理、バッチ処理に分けられます。 今回はリアルタイムに結果が分かる必要はないため、バッチとしてシステムに組み込むことにしました。 クックパッドではHakoというECS上に展開されるコンテナオーケストレーション環境があり、この上に先に構築した機械学習モデルをデプロイします。 システムの全体構成は以下のようになります。

f:id:kazuyuki-hashimoto:20190315174653j:plain
システム構成図

まずDockerコンテナ環境で実行できるCUIアプリケーションにします。 今回の分析ステージではJupyter Labを使いながら分析を進めたので、関数やクラスなどを抽出する作業が発生します。 このときデータ読み出し時のカラム名や前処理後のベクトルの形などに関するテストを順次追加しました。

次にCI上でテストを動作させる設定を追加しました。このときローカルファイルに依存したテストがFailしていたので、順次ファイル依存の問題を解決しました。 また、前処理の速度が気になる問題がありました。そのため分かち書きの処理をJanomeからMeCabに変更する修正も加えました。

次にHako上でアプリケーションとして動かせるようにします。クックパッドでは、バッチを実行する環境はスポットインスタンスを利用する環境と通常のインスタンスを利用する環境があります。 今回はHakoからS3へCSVの転送が途中で止まってもきちんと再実行できる設計にした上で、スポットインスタンスを利用する環境を選びました。

以上の取り組みから、毎月のデータをロード、NPSでの話題を分析し、RedShiftに保存するバッチフローが完成しました。

ここまでの機能を追加したあと、分析担当者に機能の共有をしました。ぜひこの内容を常に見られるようにしてほしいというフィードバックをもらいました。 そこで、より多くのスタッフにNPSのデータに興味を持ってもらいたいと考え、作成したグラフをSlackのボットで月次で投稿するようにしました。

ふりかえり

機械学習をプロダクションに組み込む作業は初めてでした。アルバイトを通じて機械学習の知識だけではなく、システム設計について考える点がたくさんあることがわかりました。

私は修士の学生として在籍している研究室では画像処理に取り組んでいます。 今回、はじめて自然言語処理のタスクを扱いましたが、思っていた以上に楽しかったです。 画像処理では慣れていることもあり、違和感なくベクトル化できますが、言語は一筋縄でいかないイメージがありました。 今回、自然言語処理タスクに取り組んだことで、この分野でも様々なツールが提供されかなり気軽に始められることが分かったのは収穫です。

今回のタスク(NPS)には自然言語の不均衡データという特徴がありました。 画像であれば、少ないデータに対して回転やクリッピングなどのデータオーギュメントを気軽に適用できます。 離散的な値を特徴とする自然言語では大きく意味が変わりかねず、類語を用いた言い換えは容易ではなく、目視で確認する作業が必要です。 このあたりについて、今後調べて見たいと考えています。


  1. Net Promoter®およびNPS®は、ベイン・アンド・カンパニー、フレッド・ライクヘルド、サトメトリックス・システムズの登録商標です。正味推奨者比率などと訳されます。

【RubyKaigi 2019 参加者に捧ぐ】福岡で起業した男が本気で書いた福岡グルメまとめ

$
0
0

f:id:kazzwatabe:20190319180852j:plain

CEO室で新規事業立ち上げをやりつつ、昨年子会社になりましたウミーベ株式会社の代表取締役をやっているカズワタベ(@kazzwatabe)です。

さて、来月には待ちに待った RubyKaigi 2019が開催されるんですが、クックパッドもRuby Committers' Sponsorとして関わっていたり、たくさんのエンジニアが現地参加するようです。

そんな RubyKaigi 2019 、今年の会場はウミーベが拠点とする福岡! そして福岡と言えば飯が美味い。これは他県から訪れる社内外のみなさんにグルメ情報を提供せねばという思いから、非エンジニアにも関わらず開発者ブログに登場する運びとなりました。

RubyKaigi 2019 に限らず、福岡を訪れる機会があったらぜひ行って欲しいお店ばかりなのでよかったら参考になさってください。

目次

海鮮

福岡では国内有数の漁場である玄界灘の海の幸が楽しめます。鮮度の高い刺し身を、甘み、旨味の豊富な九州醤油で食べるのがおすすめ。また、九州では関東では馴染みの薄い生のサバをよく食べます。特に胡麻だれに和えた「胡麻サバ」は絶品なのでぜひ食べてください。

サバ(きはる、独酌しずく)

tabelog.com

福岡の中でもサバがダントツで美味しいのがこちらの「きはる」。長崎県五島列島のサバを刺し身、炙り、胡麻サバにしていただくことができます。その他に、対馬の穴子の刺し身、天ぷら、焼きサバチャーハンなどがおすすめです。

tabelog.com

きはるが満席の場合は、同じサバが食べれる系列の「独酌しずく」へ。こちらの方が予約が取りやすい印象があります。

イカ(河太郎、表邸)

tabelog.com

佐賀県の呼子の名物である「イカの活造り」を食べれるお店が福岡にもあります。中でも有名店が「河太郎」。透き通ったイカの刺身はもちろん、後づくりとして出てくるエンペラやゲソの天ぷらは絶品です。

tabelog.com

他にイカの活造りが食べれるお店としては「表邸」が挙げられます。こちらは後づくりの天ぷらの衣にイカ墨を混ぜ込むのが特徴的。イカ以外も絶品ですよ。ほぼ全室個室なので、落ち着いた雰囲気で食事を楽しみたい場合におすすめです。

海鮮全般(ふじけん、兼平鮮魚店)

tabelog.com

tabelog.com

海鮮全般を楽しもうと思ったら「ふじけん」「兼平鮮魚店」がおすすめです。どちらも魚屋さんが営むお店で、その日仕入れた生きのいい魚を食べることができます。個人的には以前「ふじけん」で食べたサワラが奇跡的な美味しさだったのでまた食べたいです。

ランチにおすすめ(小野の離れ、梅山鉄平食堂、よし田)

ランチでも海鮮を楽しめるお店がたくさんあります。僕がよく行くのはこちらのお店です。

tabelog.com

僕が「福岡最強ランチ」と呼んでいるのが、こちらの「小野の離れ」。とりあえずヤバいんですが、どうヤバいのかは「小野の離れ ランチ」でググれば分かります。ランチも予約制で、12時スタート、13時半スタートの2回転のみです。予約取れたらラッキーなのでいますぐ電話かけましょう。ちなみに夜は夜でいい店です。

tabelog.com

もう少し気軽に海鮮ランチを楽しみたい方には「梅山鉄平食堂」がおすすめです。その日仕入れたたくさんの種類の魚を、塩焼きな煮付け、唐揚げなどにした定食を食べることができます。

tabelog.com

夜は割烹ですが、ランチはリーズナブルに「鯛茶(鯛茶漬け)」が楽しめるのが「割烹よし田」です。ぷりっぷりの真鯛と胡麻だれ、出汁の相性は格別。こちらも予約可能です。

もつ鍋(やま中、田しゅう、楽天地)

福岡の名物のひとつが「もつ鍋」。ぷりっぷりのもつに旨味たっぷりのスープ。ちゃんぽん麺や雑炊で〆まで楽しむことができます。がっつり食べたい方におすすめです!

tabelog.com

「やま中」は地元の人が観光客をよく案内する、高級もつ鍋屋の代名詞です。すでに行ったことがある方もいそうですね。

tabelog.com

やま中に負けず劣らず、より安く味わえるのが福岡で一番好きなもつ鍋屋が「田しゅう」。清潔感のある店内で、女性のみのグループもよく見られるのが特徴です。みそ味からのチーズリゾットで〆るのがおすすめです。

tabelog.com

最後に安く済ませたいときに使えるのが「楽天地」。食べ放題・飲み放題でもリーズナブル。ぷりぷりというよりは、コリコリしたもつを使った、B級グルメ感のあるもつ鍋の定番です。こっちはこっちで美味しいんですよね。

水炊き(とり田、とりぶどう HANARE)

もつ鍋と対を成す福岡名物が「水炊き」。鶏を長時間煮込んだコクのあるスープは絶品です。

tabelog.com

数ある水炊き屋の中でも大好きなのが「とり田」。2店舗ありますが、博多本店の方が広いので予約が取りやすいです。ここのスープは水筒に入れて持ち帰りたくなること間違いなしです。

tabelog.com

以前はとり田ばかり行ってたんですが、最近は「とりぶどう HANARE」もよく使います。焼き鳥の有名店「とりぶどう」の系列です。こちらはハツや砂肝など、一般的には水炊きに入れない部位が出てくるのが特徴。さらに本店の名物「幻の白レバー」も食べることができるんですが、その臭みのなさには驚きです。

焼き鳥(かわ屋、とりかわ粋恭)

tabelog.com

tabelog.com

福岡は実は人口10万人あたりの焼き鳥店店舗数が日本一で、美味しいお店がたくさんあります。中でも有名なのは「とり皮」が名物の「かわ屋」「とりかわ粋恭」の2店です。

少しずつ脂を落としながら、6日かけて焼いたカリカリのとり皮は絶品。人数×5〜10本くらいオーダーするのが福岡スタイル。こちらは会計も1人2〜3000円とリーズナブルです。

ラーメン(ShinShin、海鳴、おいげん、梟)

福岡でおすすめのラーメン屋の話をし始めると宗教戦争が始まるんですが、独断と偏見で選びます。

tabelog.com

週末は行列ができる「ShinShin」はクセの強くない豚骨ラーメンが名物。深夜まで営業しているので〆にも最高です。高菜トッピングがおすすめ。

tabelog.com

ちょっと変化球な豚骨ラーメンが食べたい方におすすめなのが、「ラーメン海鳴(うなり)」。こちらでは魚介とんこつや、豚骨をベースにイタリア風にしたジェノバ味などが楽しめます。魚介とんこつのスープは最後の一滴まで飲んでしまう魔力があります。

tabelog.com

天神から家に帰る途中にあったので、酔って帰る途中によく吸い込まれてたのが「ラーメンおいげん」。ShinShinに比べるとワイルドな豚骨スープが特徴です。炙ったチャーシューの香ばしさも相まって食が進みます。飲みの〆には最高です。

tabelog.com

こちらも〆によく使ってました。担々麺の名店「梟」。朝の5時までやってるのがありがたいです。辛さよりは旨味を感じるタイプの担々麺です。サイドメニューの串ホルも美味しいのでぜひ。

うどん(牧のうどん、釜喜利うどん、弥太郎うどん)

福岡の名物で、県外の人が真っ先に挙げるのはラーメンです。しかし住んでみるとそれ以上に食べる機会が多いのはうどんだということに気づきます。

うどんはコシがある讃岐うどんとは対照的で、柔らかい麺が特徴的。薄口しょうゆと出汁の透き通ったつゆでとても優しい味がします。具材はごぼ天(ごぼうの天ぷら)、丸天(さつま揚げみたいなやつ)が主流です。だいたいのうどん屋にはサイドメニューにかしわ飯(鶏の炊き込みご飯)があるんですが、こちらも美味しいのでぜひご一緒に。

tabelog.com

福岡らしい柔らかい太麺を楽しめるのが「牧のうどん」。福岡のソウルフードです。少し前まで中心部にはなかったんですが、博多駅に店舗ができて行きやすくなりました。麺がスープを吸ってしまうので、追加のスープがやかんが出てきます。

tabelog.com

個人的に一番好きなうどん屋が「釜喜利うどん」。薬院の有名なうどん居酒屋「二◯加屋長介」の姉妹店です(こちらもおすすめ)。スープも麺も具もすべてが美味しくて、今までの人生で食べた中でも一番のうどん屋です。意外と知られてないのですが、肉うどんにの具をご飯に載せた「和牛丼」が絶品なので、一度うどんを食べた人はぜひお試しください。

tabelog.com

福岡で飲み歩いてたら、みんな一度はお世話になっているであろうお店が「弥太郎うどん」。メイン通りである国体道路沿い、24時間営業という利便性は圧倒的です。〆にどうぞ。

その他(天ぷらのひらお、五穀、ヌワラエリヤ、たんか)

tabelog.com

名物ではないけど有名なのが「天ぷらのひらお」。あの味の天ぷら定食が700円ほどで食べれるのは奇跡です。東京だったら1500円でも安いくらいの味と量。テーブルに置いてある、食べ放題のイカの塩辛が隠れた人気メニューです。これが美味しすぎて天ぷら出てくる前に白飯がなくらないように注意が必要です。

tabelog.com

最近ドラマでも話題になった博多の明太子。その明太子をふんだんに使ったオムライスが食べられるのが「五穀」です。ふわふわのオムレツと、生明太子を和えたご飯の相性は抜群。行列必至なので余裕をもって行きましょう。

tabelog.com

住んで初めて知ったんですが、実は福岡はカレー文化が豊かです。中でも足繁く通っていたのが、スリランカカレーの名店「ヌワラエリヤ」。特にライスではなくビーフンを使った「ヌードルカリー」は絶品です。カレー好きの方はぜひ。

tabelog.com

最後になりますが肉系で一番好きなのが「たんか」です。牛タン、牛さがりの串焼きが出てくるんですが、その柔らかさと旨味にびっくり! これは言葉では説明しきれないのでぜひ行ってください。肉ならここです。

まとめ

他にも美味しいお店が無限にあるんですが、パッと浮かんだものを羅列しただけでこの量になってしまいました。福岡は罪深い土地です。RubyKaigi 2019にご参加の方はぜひ食も楽しんでください!

質問などありましたらTwitterで(@kazzwatabe)までお気軽にどうぞ。

福岡で働きたいエンジニア募集しています

また、そんな住環境最強都市福岡で働きたいエンジニアの方を、クックパッドの子会社であるウミーベで募集しております。ご興味お持ちいただけたら、ぜひウェブサイトからご連絡ください!

umee.be

入力時間11%減!書きやすいエディタのUIデザイン

$
0
0

こんにちは、投稿開発部の佐野大河(@sn_taiga)です。 先日、クックパッドのiOSアプリでレシピのエディタ画面をリニューアルしました。今日はそのUIデザインの設計についてお話します。

方針は「簡素化」

エディタ画面は、レシピを考えて記録・投稿する人にとって重要な機能の一つです。レシピには材料や作り方、料理写真、タイトル、紹介文などさまざまな項目があり、頭の中にある料理をこれらの形に落とし込んでいくのはなかなか大変な作業でもあります。なので、レシピを書く際の手間を減らし、ユーザーがストレスなくレシピを書けることを目的に「簡素化」という方針を定め、改善に取り組みました。具体的に行ったことは大きく以下の二つです。

1.入力や編集のステップを少なくする

以前のエディタ新しいエディタ
f:id:sn_taiga:20190329095727g:plain:w300

以前のレシピエディタはひとつの項目を選択するとモーダルが開き、入力を終えたら元の画面へ戻ってくるウィジェット型のUIでした。これはこれで、一つの項目に集中でき、シンプルな画面から徐々に要素を埋めていくため圧迫感が少ないというメリットがある一方、レシピを書き上げるまでのステップは多くなります。今回このウィジェット型から、一画面で入力・編集を完結できる「インライン型」のUIに変更し、各項目の入力の行き来をしやすくサクサク書き進められる構成に変更しました。

2.入力をアシストする

(A)作り方から材料の自動入力(B)タイトルから材料のサジェスト(C)分量の単位補完
f:id:sn_taiga:20190328220514p:plainf:id:sn_taiga:20190328220534p:plainf:id:sn_taiga:20190328220553p:plain

新しいエディタでは、レシピを書く人の入力をアシストする機能を充実させました。 料理の作り方を書いたら自動で材料欄が入力されます(A)。例えば「鮭を一口大にカットし、玉ねぎをスライスしておく」と書いたら材料欄に「鮭」と「玉ねぎ」が追加されます。 先にタイトルを入力すれば、その内容から材料を予測しサジェストもしてくれます(B)(こちらは研究開発部の機械学習の技術を用いて予測しています。予測のモデルについてはこちらの記事で紹介されていますので興味がある方はぜひ読んでみてください。) また、入力されている材料名から適切だと予測される単位を、分量の入力中にサジェストしてくれます(C) 。

このように「簡素化」という目的に対して「インライン型」「入力アシスト」という手段を用いた中、UIを設計する上での"デザインの方針"を定めました。どのような方針で、具体的にどういう工夫をしたのかについてここからはお話します。

入力量に圧倒されないように

ウィジェット型からインライン型にすることによって入力や編集の行き来がしやすくなる一方、画面内の情報量が増えます。元のUIをそのままインライン型に変更すると、画像のような文字量の多い入力フォームになり、ユーザーが画面を開いたときに「うっ、大変そう...」とレシピを書くハードルを高く感じてしまう危険性があるためこの方針を定めました。

f:id:sn_taiga:20190328220420g:plain:w300

具体的に行ったことをいくつか紹介します。

入力エリア、見出し、プレイスホルダーの同居

f:id:sn_taiga:20190328220720g:plain:w300

各項目の要素は入力エリア、見出し、プレイスホルダーの3つです。自分が今何を入力しているのかわかるように見出しは入力中も表示しておく必要があり、プレイスホルダーは「こういうものを書けばいいんだ」と理解するための手助けになります。ただ、これら3つを常に同時に表示しておく必要はないと考え、初めは入力エリアに重ねる形で見出しのみ表示し、フォーカス時に見出しを上側に移動させプレイスホルダーを表示するようにしました。これにより画面全体の文字量や高さを抑え圧迫感を減らすだけでなく、フォーカス前と後の状態をアニメーションでシームレスに繋ぐことで視覚的な負荷も軽減させました。

スケーラブルな入力エリア

f:id:sn_taiga:20190328221756g:plain:w300

レシピの紹介文やコツポイントといった長めの文章を書く箇所は、入力したテキストの行数に合わせて高さが変わるようにしました。高さを固定させる場合は、決めである程度の領域を確保しなければならず、テキストをあまり書かない人にとっては余分なスペースに、たくさん書く人にとっては狭いスペースになるのに対し、入力量に合わせて変化させることで必要十分な領域にすることができました。

入力中の項目に集中できるように

どこにもフォーカスしてないときは、入力されたテキスト以外は全体的に薄いグレーの配色で、最低限項目を認識できる存在感にし(入力可能な項目だとわかるように見出しの横に鉛筆アイコンを置いています)フォーカスした箇所のみをオレンジ色で強調し対象に集中しやすくしました。

f:id:sn_taiga:20190329091909g:plain:w300

元のUIをそのままインライン型にしたものと比べて画面全体の圧迫感が軽減しました。

自分の手で書き上げているように

入力のアシスト機能は便利になり得るものですが、タイミングや見せ方次第でユーザーの作業の妨げにもなりかねません。レシピを書く一連の流れに溶け込み余計な混乱を招かず、あくまでユーザー自身がアシスト機能を使いこなしながら「自分の手で書き上げた実感」を得られるようにとこの方針を定めました。

自動入力されたものをハイライト

f:id:sn_taiga:20190328224428g:plain:w300

作り方から自動入力された材料には背景色が付き、材料にフォーカスするか分量を入れるかすることで色が消えます。作り方はユーザー自身が書くものでもあり予測の精度はある程度高いためサジェストではなく自動入力という形で表示しているのですが、このような見せ方にすることで「自動で入力されたものだけど最終的にそれを採用するのは自分」だと思えます。

タイトルから材料のサジェストのタイミング

材料予測の通信が行われるのはタイトルを入力したときですが、このタイミングではサジェストせず、ユーザーが材料を入力する際に視界に入る位置に置いています。

タイトル入力直後にサジェスト(不採用)材料欄の上でサジェスト(採用)
f:id:sn_taiga:20190328222001g:plainf:id:sn_taiga:20190328224423g:plain

タイトル入力直後のサジェストは、自分のアクションに対するフィードバックが明確で驚きが生まれやすい一方、サジェストされた材料を追加したらそのまま足りない材料の入力へ移りたくなります。これはユーザーのエディタの使い方を制限することになっていて、レシピをタイトルから書き始める人もいれば作り方から書き始める人、材料から書き始める人、全部を一気に書いて最後に微調整する人など様々な人がいます。どのような書き方の人でもエディタの使い方を制限されることのないように、材料欄の上でサジェストするUIを採用しました。材料より先に作り方を書く人も上述の補完の恩恵を受けやすくなります。

f:id:sn_taiga:20190328224006g:plain:w300

また「使っている材料はこちらですか?」という投げかけのテキストと顔アイコンを置き、クックパッド側が提案してる風にすることで、ユーザーがサジェストの棄却をしやすいようにしました。合っている材料を追加したあとは右上の×ボタンからサジェスト自体を閉じることができます。

投稿にかかった時間11%減

今回、エディタの「簡素化」という目的に対し「レシピを書き上げるまでにかかった時間」を指標の一つとして置きました。ここまでの改善を行いリリースした結果、リニューアル前のエディタと比べて平均11%の減少率が見られました。画面全体の構成の変更やアシスト機能の追加によって、リニューアル前のエディタに比べて手間が少なくサクサクレシピを書き上げることができるようになったと言えます。

まとめ

具体的なUIを設計していく際に、目的を最大限達成するために何が重要なのか、ユーザーにどう感じてもらうのが良いのかを「デザインの方針」として言語化しておくことで、狙いから逸れることなくUIを設計できました。個人的にUIを作っている最中はついあれもこれもと横道に逸れがちなのですが、あらかじめ方針を定めておくことで、なぜやってるのか、何を狙っているのかでブレることなくUIの案出しや判断がしやすくなります。

最後に、クックパッドでは「より良い体験をユーザーに届けていきたい!」というメンバーを募集中です。興味を持っていただけた方は採用ページをご覧ください。

クックパッドが今、エンジニアとデザイナーを採用したい理由

$
0
0

こんにちは、クックパッドの冨永(@mamiracle__)です。

突然ですが現在クックパッドでは、サービス開発に携わるエンジニアとデザイナーの採用に注力しています!

しかし、社外の方とお話をしていると「クックパッドはひとつの完成したレシピサービスをやっている会社だよね。本当にエンジニアやデザイナーを積極採用しているのですか?」と聴かれることがよくあります。

答えは「YES!!!!!!」です。

わたしたちが積極採用をするのは、ミッションである「毎日の料理を楽しみにする」の達成に向けてサービスを生み出し、成長させるため。このことには大きな意義があるとクックパッドは信じていて、そのためにやるべきことが(レシピサービスでもそれ以外でも)たくさんあります。

そのことを皆さまに伝えるために、採用サイトを大幅にリニューアルしました。

今日はリニューアルに際してお伝えしたかった想いを、ブログに綴りたいと思います。ぜひ温かい眼差しでお読みいただけたら幸いです。

リニューアルした採用サイトはこちらです👇✨

クックパッド株式会社 | クックパッド株式会社 採用サイト

はじめに

食事が心とからだの健康をつくる、ということは誰もが認めていることです。

わたしたちにとって食事は、単なる栄養摂取の方法ではありません。一緒に食事をする家族や友人や、日常生活の中で接する人々とのつながり、さらに視野を広げてみれば地球環境を見直すことにまで密接に関係しています。(ある調査では、生産されたものの食べられていない食品ロス分だけでも、世界の温室効果ガスの8%に相当することがわかっているのだとか!)

わたしたちにとって「自分たちのための食事を料理すること」は、自分の意思(や食べる人のことを想って)で自分たちの食事をコントロールできる、とても身近で創造的で健康的な手段だとクックパッドは考えています。

ですが、料理することや食べることは日常生活に密着しすぎていて、まさにつくる・食べるその瞬間に大切さを改めて考えることは少ないかもしれません。わたしたち人間にとって不可欠に重要だけれど、考えずに「こなす」ことも容易なのが、料理や食べるという行為です。

クックパッドはそのことについて世界中の人々が、自然と考えたくなる世界、行動したくなる世界を目指しています。

「技術」で「料理の課題」を解決する

「料理」とひとことで言っても食材や道具などの生産、物流、流通、調理、食卓、洗い物、記録、実はさまざまなシーンが存在します。これを世界中の人々に向けて価値を届けようとすると、やりたいこと・やらなければならないことが溢れ出てくるのです!

いまは下記の三つの領域で、ミッションの実現に向けた挑戦をしています。

■つくり手を増やす

毎日の料理の課題を技術で解決し、つくるハードルを最小限に。つくる楽しさは最大限に。毎日の料理を楽しみにしし、つくり手を増やします。 f:id:mamiracle:20190402123438p:plain

各サービスの紹介はこちら

Our Challenges | 私たちのチャレンジ | クックパッド株式会社

関連するTechLife記事

■つくり手をつなげる

料理、道具、食材など、さまざまなつくり手をつなげることで、すべてのつくり手が継続的に収入を得ることができる仕組みをつくります。 f:id:mamiracle:20190402123444p:plain

各サービスの紹介はこちら

Our Challenges | 私たちのチャレンジ | クックパッド株式会社

関連するTechLife記事

■世界70億人のインフラへ

料理は、万国共通の習慣。だから、私たちの挑戦は国内にとどまりません。英国ブリストルに構えたグローバル本社を起点に、世界各国へとクックパッドを広げていきます。71カ国26言語にて展開しています。(2018年12月現在)

世界各地域のクックパッドはこちら

Our Challenges | 私たちのチャレンジ | クックパッド株式会社

英国ブリストルチームの開発者ブログ

sourcediving.com

一緒にやっていきませんか!

毎日の生活のちょっとしたワクワクと喜びの連続を生むために、わたしたちはサービスに触れていない時間も含めて、料理に関する全てを楽しみにする一連の体験をデザインします。

そして、料理の中にある様々な課題を、スケーラブルに解決するために技術の力を活用します。課題に向き合い、最善な手段を取り続けること。

そんな姿勢に共感してくださる方がいたら、ぜひ私たちに会いに来てください!

リニューアルした採用サイトには、お伝えしきれなかったことを詰め込んでいます。こちらもぜひご覧いただけたら幸いです。

Let’s make everyday cooking fun together!

info.cookpad.com

SRE Lounge #8 でスポンサー &登壇をしました

$
0
0

こんにちは。技術部 SRE グループの吉川 ( @rrreeeyyy ) です。

少し遅くなってしまいましたが、先日の 3/13 に行われた SRE Lounge #8の、 会場・フード・ドリンクスポンサーをクックパッドで行いました!

また、スポンサーだけでなく Cookpad Microservice Architecture Overviewというタイトルで登壇もしましたので、簡単に紹介させて頂きます!

SRE Lounge について

SRE Lounge は、UZABASE さんのSRE チームが中心となり発足した勉強会で、 現在は SRE 同士での情報共有や交流の場として有志の方により定期的に開催されています。

SRE Lounge は有志の方が開催しているため、会場の提供やドリンク・フードなどの支援を行うスポンサーを募集しており、 今回は、主催の方とのご縁もあり、クックパッドで会場提供ならびにドリンク・フードのスポンサーをさせてもらいました。

f:id:rrreeeyyy:20190403220609j:plain
乾杯の様子

各発表について

ソラコムAPIの裏側で運用チームは何をやってきたのか

最初は、株式会社ソラコムの五十嵐様より、「ソラコムAPIの裏側で運用チームは何をやってきたのか」というタイトルで発表がありました。 ソラコム様でのシステム構成や、内部での DevOps の取り組みや苦労についてお話されたあと、 SRE の採用について、各社の Job Description の分析についての調査結果を踏まえてお話されていました。

f:id:rrreeeyyy:20190403220725j:plain

資料はこちらです。

Cookpad Microservice Architecture Overview

次に、私、吉川 ( @rrreeeyyy ) が、 「Cookpad Microservice Architecture Overview」という題でお話させてもらいました。

クックパッドでのマイクロサービスへの取り組みに関して、 Cookpad Tech Kitchen #20などでもお話させて頂いてますが、 今回は、これらの全体像がどうなっているのかについて簡単に紹介しました。

発表は少し駆け足になってしまったのですが、この発表をベースに、懇親会で気になったことについて深い議論を交わすことができ、 非常に有意義な時間を過ごすことができたと感じています。

f:id:rrreeeyyy:20190403220833j:plain

資料はこちらです。

また、マイクロサービスへの取り組みのさらなる詳細は、 先ほど紹介した Cookpad Tech Kitchen #20などの資料も併せてご覧いただければと思います。

割れ窓理論をWebインフラの改善に活用し、チーム内の知識共有を促進している話

最後に、株式会社はてなの @hokkai7go 様より「割れ窓理論をWebインフラの改善に活用し、チーム内の知識共有を促進している話」というタイトルで発表がありました。

技術的負債や軽微な問題などを、Issue にしておき、週に 1 度、1 時間程度取って作業をしているそうです。 今回の発表にもある通り、こういった取り組みに対して実施してみてどうだったかの振り返りや改善をしっかり行えており、学びのあるお話でした。

f:id:rrreeeyyy:20190403220901j:plain

資料はこちらです。

おわりに

class SRE implements DevOpsと言われているように、 会社の事業や、チームとして実現したい信頼性のあり方によって SRE の振る舞いや、目指すべきゴールはある程度変わってくると考えています。

その際に、SRE Lounge のような場で、他社の SRE の方々と意見を交換し、どういったゴールを目指しているのか、 どういった取り組みを行っていて、どういった結果になったのかを知るのは、非常に有意義なものだと感じました。

SRE Lounge から派生して、テーマを決めてディスカッションを行う SRE Session #1というのも開催されるそうです。興味がある方は是非参加してみてください。

また、クックパッドでは SRE に限らず、SRE と一緒にサービスの信頼性に取り組んでくれる開発エンジニアも募集しています。 興味がある方は https://info.cookpad.com/careers/をご覧頂いたり、Twitter などでお声がけ頂ければと思います。


1日でSwiftコンパイラを作る!Swiftコンパイラインターンを開催しました

$
0
0

こんにちは、モバイル基盤部の @giginetです。

去る3月28日、Cookpad Spring 1day Internship 2019の一環として、Swiftコンパイラコースを開講しました。

最近のSwiftコンパイラ

近年、iOSエンジニアの間ではOpen Source Swiftがホットトピックとなっています。

ここ1年ほど、わいわいswiftcというSwift言語処理系に関する勉強会が盛り上がっていますし、 先日のtry!Swiftでは、参加者がSwift自体にcontributionするOpen Source Swiftワークショップが開かれました。

Swiftコンパイラに用いられているLLVMという技術は今、多くの言語処理系で利用されています。これを学ぶことで、さまざまな言語処理系に応用することができます。

このインターンは、Swiftコンパイラを例に、LLVMに触れ、コンパイラの動作を理解することを目的に構成しました。 後半のワークショップでは、実際にMinSwiftという簡易的なSwiftコンパイラをSwiftで開発してみます。

講義

まず最初の30分は講義パートです。Swiftコンパイラの構成を見ていき、LLVMの仕組みを学びます。

Swiftコンパイラ(swiftc)の構成

f:id:gigi-net:20190404183119j:plain

まず、Swiftコンパイラがどのようなフローを経て、動作するかを見ていきました。 コンパイラと一口で言っても、swiftcはパーサーや意味解析、中間言語など、様々な要素技術から構成されています。

1日でSwiftコンパイラ全てを理解することはできないので、今回はそれぞれの役割は説明するだけで留め、このインターンでは、現在、多くの言語処理系の要となっているLLVMに焦点を当てていきました。

LLVMを学ぶ

Swiftコンパイラのうち、LLVMとの橋渡しを行うIRGenに注目し、LLVMとは一体どのようなもので、どのように動作しているかについて学びました。

LLVMとは

LLVMは、コンパイラ基盤と呼ばれるもので、機械語の生成や最適化など、コンパイラに必要なものを共通化して作れるようにした仕組みです。

この仕組みを用いれば、一から作るよりは非常に少ない労力で汎用的なコンパイラが作成できます。 現に、Swiftのみならず、非常に多くのコンパイラがLLVM上で実現されており、LLVMについて学ぶことで多くの言語処理系について理解することができます。

LLVM IR

LLVM IR(LLVM Intermediate Representation)は、LLVMで使われる中間表現です。

どの言語であっても、最終的に適切なLLVM IRを生成することで、バイナリの生成や複数アーキテクチャへの対応、最適化などをLLVMの提供する仕組みに任せることができます。

LLVM IRは、ヒューマンリーダブルであるという特徴があり、例えば以下のような C のコードを例に挙げます。

int main(void) {
    return42;
}

このコードは、以下のような LLVM IR で表現することができます。

definei32@main() {
  reti3242
}

この講義では、LLVM IRの簡単な読み方や、LLVMの最適化がどのように動作するかを扱いました。

MinSwift

簡単な講義を経て、SwiftでSwiftをコンパイルするコンパイラ、MinSwift*1を製作しながら、コンパイラの構成や、LLVMの扱いについて学びました。

このワークショップは、LLVMが公式で提供しているLLVM Tutorialを参考に構成されており、ここで実装している架空の関数型言語Kaleidoscopeと同程度の表現力を持つコンパイラをSwiftで実装しました。

MinSwiftは以下のように動作します。

  1. Swiftのコードをパース
  2. Abstract Syntax Treeに変換
  3. LLVM IRを生成
  4. ビルドして実行オブジェクトを生成
  5. SwiftやC++からリンクして呼び出す

多くはSwiftコードのパーサーをSwiftで書いたり、LLVM IRの生成部分を実装しますが、他にも実装を通して、受講者は非常に多くのことを学ぶことができます。 例えば必要となったのは以下のようなトピックです

  • Swift Package Managerを使ったコマンドラインツールの開発
  • Swiftにおけるユニットテストの実行とTDD
  • Swiftによるパーサーの実装
  • LLVMSwiftを用いたlibLLVMの利用
  • LLVM IRの読み方
  • XcodeやLLDBの扱い方

ワークショップ

f:id:gigi-net:20190404183144p:plain

ワークショップは、予め用意されているユニットテストを通しながら、ドキュメントを参考に実装を進めていく形式となっていました。

f:id:gigi-net:20190404183201p:plain

テストケースが全て通過するように、1ステップずつ実装を進めていくと、最終的にSwiftコードからLLVM IRを生成し、LLVMを使ってコンパイルできるコンパイラ、MinSwiftが完成します!

最後まで実装を行うと、以下のようなコードをコンパイルすることができるようになります。少しは実用に耐えるコンパイラになったでしょうか?

funcfibonacci(_ x:Double) ->Double {
    if x <3 {
        return1
    } else {
        return fibonacci(x -1) + fibonacci(x -2)
    }
}

応用課題

最後に、それぞれ好きなテーマを探して、MinSwiftの改善に取り組んでもらいました。

例えば以下のような課題です。

  • 関数定義の拡張
  • 数値リテラルの改善
  • for文の実装
  • 変数の実装
  • 文字列リテラルの実装

課題を完了させて、独自の実装まで到達できた参加者は極僅かでしたが、一方で短い時間の中、上記のような言語機能を追加できた参加者もいました。

f:id:gigi-net:20190404183220j:plain

まとめ

普段Swiftを書き慣れていても、アプリ開発と言語処理系の開発では全くノウハウが違い、戸惑った方も多かったようです。

コンパイラインターンという割には、パーサーの実装量が多くなってしまったのは反省点です。

未経験の方には多少難しかったようですが、概ね好評を頂けたようで嬉しく思っています😊

クックパッドではSwiftコンパイラや言語処理系に興味があるエンジニアを募集しています。

*1:これはRubyコミッターの @mametterが行ったRuby処理系のワークショップ、MinRubyのオマージュとなっています https://techlife.cookpad.com/entry/2018/10/16/131000

クックパッドマートにおける実世界での配送を意識した注文の検証処理【連載:クックパッドマート開発の裏側 vol.1】

$
0
0

はじめに

こんにちは、買物事業部の勝間(@ryo_katsuma)です。 今日から5日間は、買物事業部のメンバーで連載記事を書かせていただきます。

買物事業部は、クックパッドの生鮮食品ECサービスの新規事業「クックパッドマート」の開発を行っている事業部です。 クックパッドマートのサービスについての説明や、立ち上げ期の舞台裏については昨年の長野によるエントリをご参照ください。

クックパッドマートは、iOS、Androidアプリのリリース、商品受け取り場所におけるスマートロックの設置、注文当日配送の実現など、 サービスをより多くの人に便利に使っていただくためのいろいろな新しい取り組みを行ってきました。 今回はこれらの取り組みについて多くの方にぜひ知っていただきたいと思い、連載形式で紹介させていただきます。

ちなみに明日以降は、以下のような内容を予定しています。

  • vol.2 クックパッドマートiOSアプリを楽しく新規開発した話
  • vol.3 1枚のラベルの向こうには、1人のユーザがいる
  • vol.4 クックパッドマートAndroidアプリ開発の裏側
  • vol.5 スマートロック開発におけるPDCA

注文の検証処理

vol.1の本稿では、クックパッドマートにおける注文の検証処理(validation)について紹介します。 注文の検証処理とは、クックパッドマート内での用語になりますが、その名の通りユーザーがカートに入れた商品について、「注文可能かどうか」を判定するための検証処理になります。

iTunes Storeのようなデジタルコンテンツの販売の場合、クレジットカードなど決済手段に問題がない限り「注文できるかどうか」の検証処理に複雑なケースはあまり多くなく、せいぜい販売上限数を考慮するくらいではないかと思います。 一方で、物流が絡むECにおいては、現実世界での非常に細かな制限が多く絡み、注文の検証処理には多くのことを考慮する必要があります。

ここからは、クックパッドマートにおける注文の検証において、どのようなことを考慮しているかをご紹介します。

前提

まず、クックパッドマートにおけるデータの関連性についてご紹介します。 説明を簡略化するために実際の概念とは一部異なるものもありますが、注文処理においては以下の概念が重要な登場人物になります。

product

  • いわゆる「商品」の概念
  • ユーザーが購入し、販売店や生産者の方に用意いただく食材を指す

location

  • クックパッドマートにおける「受け取り場所」の概念
  • ユーザーは常に1つの受け取り場所を指定している

delivery

  • location毎の注文締切、受け取り開始、受け取り終了などの「配送スケジュール」
  • たとえば「4/8 2:00注文締切」「4/8 16:00 ~ 23:00 商品受け取り可能」などのデータを持つ

order

  • いわゆる「注文」の概念
  • 注文毎に配送スケジュールのdeliveryに紐付き、各orderは複数のproductを持つ

データ間の関連性

rails的に表現すると、このような関連性を持っています。

classDelivery
  belons_to :locationendclassLocation
  has_many :deliveriesendclassProductendclassOrder
  has_many :products
  belongs_to :deliveryend

また、商品の集荷配送については、後ほど詳細を述べますが下記のような流れになります。

  • 販売店は注文情報に従い、コンテナに商品を入れて準備
  • ドライバーは指示書に従い、複数の販売店から商品が積載されたコンテナを集荷し、温度管理された配送資材(以下、シッパー)に入れる
  • ドライバーは指示書に従い、複数の受け取り場所でシッパーからコンテナを取り出し、冷蔵庫に設置する
f:id:ryokatsuma:20190405192518p:plain

注文の受付可能数

前述の通り、在庫の概念が無いデジタルコンテンツではなく、実在する販売店や生産者の方に用意いただく商品なので扱える数は有限です。 また、商品の多くはクックパッドマートだけではなく、実店舗でも販売されているので、その数には上限が設けられている必要があります。

そこで、商品には購入可能数が設定されてる必要があります。ここではsales_limit_per_dayとすると、購入可能数を超えているかどうかのチェックはこのようになります。

product.sales_limit_per_day < delivery.orders.where(product: product).count

これで購入可能数の確認は十分でしょうか?答えはNoです。

すべての配送場所を考慮

ある販売店の商品は、1つの受け取り場所だけではなく、N箇所の受け取り場所に配送されます。 言い換えると、あるdeliveryの配送日と同じ配送日の別のdeliveryが存在することになります。 たとえば、4/8配送分の販売店Aの豚肉は、受け取り場所1のユーザーで購入されていなくても、受け取り場所2のユーザーで購入されている可能性があります。

そこで、ある配送日における全配送場所のdeliveryを考慮すると、受付可能数のチェックはこのようにする必要があります。

deliveries = Delivery.find_by(date: delivery.date)
product.sales_limit_per_day < Order.where(delivery: deliveries, product: product).count

これで購入可能数のチェックはOKになりましたが、注文の検証としてはまだ不十分です。

受け取り場所のスペースを考慮

受け取り場所において、注文した商品は冷蔵庫の中のコンテナ内に設置されます。

f:id:ryokatsuma:20190405192532j:plain

受け取り場所の冷蔵庫設置面積は基本的に広げることはできないので、冷蔵庫の数は簡単に増やすことがはできません。言い換えると、受け取り場所における冷蔵庫内のコンテナの数も有限になります。 例えば、注文商品の種類が多く、コンテナに空きスペースが存在しない場合は、冷蔵庫に商品を設置することができなくなるので、配送しても冷蔵できない状態になるので注文を受け付けてはいけないことになります。

そのため、「注文しようとしている商品は受け取り場所のコンテナに設置できるかどうか」を判定する必要があります。 そこで、クックパッドマートでは、販売商品1つづつ簡易的に体積を測定し(縦x横x高さ)、注文しようとしている商品の体積は、受け取り場所のコンテナの総体積に収まるかどうかを確認しています。

f:id:ryokatsuma:20190405192553p:plain

実際は、コンテナは配送のオペレーションの観点で販売店や生産者ごとに分けられている(複数の販売店, 生産者の商品が1つのコンテナに混在することは無い)ので、冷蔵庫の中で

  • 空きコンテナを確保できるか
  • コンテナの中に空き容量があるか

を判定しています。

ContainerBox.find_available(
  shop: product.shop,
  delivery: delivery,
  capacity: product.volume
).any?

ちなみに、コンテナの空き容量確認で現在は体積を利用していますが、ここは議論の余地があると考えています。 たとえば、商品は上積み(= 商品の上に商品を載せる)を禁止させたほうが商品が痛むリスクを減らすことができるはずですが、「商品に上積みすれば体積的にはコンテナに積載可能」な状態になってしまいます。そのため、本質的には「商品の接地面積を測定し、その総数がコンテナの設置面積を超えなければ積載可能」とでもしたほうが良いと考えています。 ただ、実際は商品それぞれ接地面積を簡単に測定することは難しいため、オペレーションコストを考えて体積を利用して運用を行っています。

さて、これで注文の検証は十分でしょうか?

配送時に温度を考慮

クックパッドマートで扱う商品は、商品の品質を下げないようにするために「予冷品(肉や魚など)」「未予冷品(野菜など)」「常温品(パンなど)」と複数の温度カテゴリに属し、各温度帯の商品はそれぞれ別のシッパーで配送を行っています。

f:id:ryokatsuma:20190405171806j:plain

たとえば、(これは実際に自分たちで配送実験を行うことで初めて理解したことなのですが、)夏の朝採れ野菜は相当高い温度に上がっています。この野菜を肉や魚など予冷品と同じシッパーで配送した場合、シッパー内の温度が上がってしまい、予冷品の品質に大きな影響が出ることが分かりました。部内では夏の朝採れ野菜温度高すぎ問題として有名な問題です。

また、パンも配送時に冷やしすぎると(約-2度〜5度)、小麦のデンプンがアルファ化して品質に大きな影響が出ることが知られています。 このように、商品の配送時にはその商品の「温度帯ごとに分けたシッパー」で配送することが、商品の品質保持の観点で必要になります。

一方で、各ドライバーが配送できるシッパーの積載量は有限です。 先の冷蔵庫の例と同じく、ドライバーも当日急に多く確保することは難しいので、あらかじめ確保したドライバーたちの車の積載量を超えるシッパーは配送できません。 つまり、1ドライバーが配送できるコンテナの総量も有限になるので、注文対象商品の温度カテゴリのシッパーに余裕があるかどうかを判定する必要があります。 注文対象商品の集荷配送を行うドライバーはあらかじめ決定できるので、このように書けます。

Shipper.find_available(
  driver: routing.driver,
  delivery: delivery,
  temperature_category: product.temperature_category
).any?

これらすべての確認を行うことで、「この商品は注文可能かどうか」が判定できます。なかなか道が長いですね。

まとめ

クックパッドマートにおける現在の注文の検証処理について解説を行いました。 実際はさらに細かな考慮がもう少しありますが、概ねこのような確認を注文処理の直前に行っています。 「かなり複雑すぎる処理をしているな、、」と思われた方もいるかと思いますが、実際に実装している僕もそう思います。

また、これらの処理はどんどんアップデートを行っています。 実際、温度カテゴリも最初は全く考慮せずにすべて冷蔵で配送していたものが、パンの扱いが出てきたことで2つの温度カテゴリになり、 夏の朝採れ野菜温度高すぎ問題に対応するために3つの温度カテゴリに対応しました。 このように現実に起きる問題に柔軟に対応するためは、的確にオブジェクト指向プログラミングで設計、実装を行う必要があり、ここは難しくも楽しいところでもあります。

一方で、このような複雑な処理も、おいしい食材をユーザーさんに確実に届けるために必要な条件だと考えています。 実際、リリースしてから半年近く経過していますが、冷蔵庫に設置できないなどの配送事故や、商品が傷んでいたといった事故は起きずに済んでいるので、引き続きおいしい食材を安全に届けるために技術によるサポートを行っていきたいと思います。

お知らせ

クックパッドマートでは、4/24(木)に、買物事業部のエンジニアによる発表とエンジニアとのミートアップを開催予定です。

cookpad.connpass.com

今回の記事のような生鮮ECそのものの仕組みや、流通の仕組みの開発に興味がある方、クックパッドマートのエンジニアと直接話してみたい方はぜひご応募ください!お待ちしています!

クックパッドマートiOSアプリを楽しく新規開発した話【連載:クックパッドマート開発の裏側 vol.2】

$
0
0

こんにちは。
連載シリーズ2日目を担当します、クックパッド買物事業部 iOSアプリエンジニアの中山(@LimiterJP)です。
早いもので入社して一年が経ちました。

私は去年4月にクックパッドへ入社しました。
その後6月にアプリ開発を始め2018年9月に「クックパッドマート」のiOSアプリをリリースしました🎉

クックパッドマートは
生鮮食品をスマホアプリから簡単に注文することができる生鮮食品ECサービスです。
従来のネットスーパーや生鮮宅配サービスとは異なり、街の精肉店や鮮魚店などの販売店や地域の農家といった生産者など、小規模事業者や個人事業主が参加できるプラットフォームです。

今日はクックパッドマートのiOSアプリの立ち上げを爆速で行った舞台裏について、

  • 新規開発で大切にしたこと
  • 開発速度を上げるための考え方
  • 新規開発に特化した具体的な手法

の観点でお話します。

新規開発で大切にしたこと

ここでは、私が普段から新規開発を行う際に心がけていたことをいくつか紹介します。

開発スピードは大切

開発スピードは早ければ早いほど失敗から機能改善・成功までの速度も上がると考えます。

「ユーザーが実際にアプリを使用し、内容に共鳴・共感しリテンションを保てるか?」
という仮説検証を行う際、
「実際にアプリをリリースし、ユーザーが使用して操作してみないとわからない」
ということを認識する必要があります。

ユーザーがアプリをダウンロードしたときの印象にはレベルが存在すると考えます。
レベル1 使えない・よくわからないアプリ
レベル2 何に使うのか?を理解できる内容のアプリ
レベル3 なかなか使えるので使いつづけようと感じるアプリ
レベル4 神アプリ!シェアしよう!拡めたいと思うほどのお気に入りのアプリ

f:id:degikids:20190408134227p:plain

そのうち最低でもレベル3以上を目指さないと結果を出すことは難しいでしょう。
そして瞬間的にレベル4でも使い続けてもらえない内容だと別の問題にも悩むことになります。
アプリ開発って本当に難しい。

アプリリリース前にしっかり使用するシーン・ストーリー・コンセプト・カスタマージャーマップを組み立て入念な企画を立案します。
デザインを当ててみて機能を設計するなど頭の中でアプリを開発する。
ここまでの工程を私達のチームでは「論理できた」と呼んでいます。

ところがこの「論理できた」の状態でいざ実際にリリースしてみると
「実際のユーザーが継続的に使用することはなくニーズがずれて失敗する」
ことが多いのではないでしょうか?

これらは機能単位で起こっているなんてこともあります。
時間をかけて作った機能が使われないなんてことよくありますよね。

そのため、速く改善してレベル3, 4を目指せる状態を作ることでリテンション(継続率)を高められる状態になると考えます。

スピード重視とはいえ、雑に実装してバグを出してでもスピードを求めるとか
「コードレビューを全く行わないぞ、テストコードを書かないぞ!」とかそういうことではありません。
行う箇所を選定・判断することが大切です。
大切なのは効率化して工夫できることの選択肢を増やし開発スピードを向上させることです。
(後述する「開発速度を上げるための考え方」など)

チームメンバーとよく笑いよく話す

クックパッドマートのチームでは、基本的に誰かが面白いことを言っている現場なので笑いが絶えません。 (誰か反応するまで喋り続けるスタイルです!)

デザイナー、エンジニア、ディレクターなど職域を超えた人同士の雑談も多く、 Slackでやり取りすれば良いようなこともあえて喋るように、人間関係も良好で良いチームだと感じています。
そのおかげか、メンバーがどういう気持ちで仕事に向き合っているか、何を考えているのかも知ることができています。 業務で疑問に思ったことは身近な人に聞けば一瞬で解決することも多く、様々な仕事が円滑に進むので、会社に来ることに楽しさも感じます。

「普段から高い頻度で雑談ができるチームは強い。」 その結果、開発速度も改善速度も上がり、品質も高くなると考えています。   

圧倒的当事者意識

「圧倒的」とつけるとなんだかすごい感じがするのでつけてみたのですが当事者意識は非常に大切です。
自分が作っているサービスを使い倒す。使わないと問題点や改善点は理解できないですよね。
クックパッドマートで販売されているパンはとても美味しいのです🍞
エンジニア自身がアプリの課題を把握することで、自分が使いやすくするためには?という意識が自然に芽生えるものです。
よってアプリをしっかり普段使いすることは非常に重要です。

開発速度を上げるための考え方

ここでは、具体的にどのように開発速度を上げていくか、私なりの考え方について紹介します。

パレートの法則(2:8の法則)で物事を考える

パレートの法則とは全体の数値の大部分は全体を構成するうちの一部の要素が生み出しているという理論です。

例えば「アプリ利用者のうち8割は、全機能の2割しか使わない」とすると、

  • すべての機能のうち2割の重要な機能に集中する
  • 2割のユーザーしか使わない機能はほどほどに作る

などのヒントが得られそうです。

何が正しいかはチームで決めると良いでしょうし、実際にそのまま採用するのではなく頭で考える作業をします。 明らかに仕様頻度が少なく重要度の低いものに時間をかけない選択やphase分けをして最初のリリース時には実装しないなどの判断をすればいいと思います。

パレートの法則に当てはめることで、

  • 開発の優先順位をつけることでリリースまでの最短距離の見通しが良くなる
  • なにか取り掛かる際にこれらを意識することで開発スピードが目に見えて上がる

などの効果があると考えています。 やる事とやらない事をはっきりさせ、見通しをよくする事が大切ですね。

何事もphaseで分けた考え方を持つ

なぜならはじめから大きなものを作ろうとすると疲弊し、その他がおろそかになる可能性があります。
「段階を経て完成を目指す」というのでしょうか。
この時点ではその「作ろうとしているもの」がユーザーに受け入れられるかはわかりません。

そこで比較的工数がかかる実装はphaseで分けて考える場面があると思います。 具体的には複数の緯度経度と場所情報の情報を持つリスト構造の情報があったとします。

リスト構造の情報を地図上にピンが立てタップし直感的に俯瞰できると見やすいでしょう。 しかしリリース序盤だと受け取り場所の情報が少なすぎて 逆に見づらい上に寂しい感じもします。

f:id:degikids:20190408134619p:plain

そこでリリース時はただのリスト構造からタップして選択するUIを選択し、ロケーションが増えたらMapからピンをタップして選択するUIへ再構築するという判断が生まれます。

新規開発に特化した具体的な手法

ここでは、iOSアプリで新規開発を行う際の具体的な手法について紹介します。

同じコードを極力書かないようにコードスニペットは磨いておく

高い頻度で使用するコード記述をスニペットに登録しておけばいちいちGoogle検索したり、昔書いたソースコードを探してみたりすることなく、 使いたい時に正確な記述をサッと呼び出して使うことが可能です。

存在は知っていてもあまり使用されていないのが現実です。 私はどの言語を書くときでもIDEに付属しているコードスニペットを活用しています。

f:id:degikids:20190408142542p:plain

コードスニペットは定期的に磨いておくと開発効率が抜群に上がります。
iOSではXcodeのsnippetを活用しおり開発をしているとこれらは何度も登場します。

  • TableView, CollectionViewの最低限動くdelegateメソッド一式
  • GoogleMapの最低限動くdelegateメソッド一式
  • 地図からルート探索
  • 緯度経度のリスト構造と現在の位置情報からdistanceが近い順に並べ替え
  • UIActivityやShareなどのイベントハンドラ
  • 位置情報取得(パーミッションdelegate含む)
  • アラートやフルスクリーンの透過ダイアログ・モーダルウィンドウ各種
  • WebView・SafariViewの設置
  • チュートリアルなどのスライドをcollectionViewのpagingを使用して実装
  • カメラ起動 最低限動くdelegateメソッド一式
  • ライブラリから写真選択、アルバムから画像抽出
  • プッシュ通知の実装
  • GCD各種

登録時はCompletion Scopesでしっかり分類しcompletion shortcutは検索性を保った名称設計を心がけます。 たったこれだけでも、手を抜いてしまうと開発効率は落ちます。

人にもよりますが私の場合すべてのsnippet の completion shortcut prefixにsw_をつけています。(sw_はswiftを表していますObjective-C時代からの歴史的経緯もあります)

sw_から始まるものはすべてsnippetであるという分類ルールを持ち通常の補完と区別しやすいようにします。

f:id:degikids:20190408142347p:plain

普段SwiftでXcodeを使用してコーディングする時は

sw_xxxxで補完

または

 command + shift + 「l」

検索

↑↓cursor

Enter

の順でコードを書いていきます。

その結果、通常のコード補完よりも早く正確に書けるようになります。 初見のコードも一旦雑に書き、使い回せるようにコードレビューを繰り返し、磨き上がったらsnippetに丁寧に放り込みます。

チーム開発するときは
~/Library/Developer/Xcode/UserData/CodeSnippetsを共有しておくととても便利です。

例えばアプリ上でGoogleMapを設置しピンを立てて位置情報取得して画面でみて見ましょうか?という場面があったとします。 プロトタイピングツールでは難しい地図のモック作成の場面でも短時間で実装しディレクターやデザイナーと実際に地図を動かし良い悪いの議論することができます。この時点でプロトタイピングツールすら必要なく開発を進めることができます。

Xcodeのプロジェクトテンプレートを磨いておくと結果的にすべての開発効率が向上する

私はxcodeのプロジェクトテンプレートを利用しています。(具体的な用意の仕方はここでは割愛します)

ですが単純に自分の雛形プロジェクトテンプレートを用意しておけば良いと思っています。(新規プロジェクトで雛形アプリを作成)

以下のような機能を雛形の中に含めています。

  • 設定画面
  • TabBarController
  • お問い合わせ(WebView or SafariView)
  • アプリの使い方(WebView or SafariView)
  • プッシュ通知
  • 利用規約(同意の機構)
  • プライバシーポリシー
  • アプリバージョンの表記
  • ライセンスの表記
  • レビュー催促の仕組み
  • ログイン画面
  • Cloud FirestoreのCRUD

その他たくさん

まず上記を含んだ雛形のプロジェクトを複製し、その案件で必要ない機能は削除していきます。
上記の実装がXcodeから新規作成したときに既に実装されていたらどうでしょうか?
それはもう「強くてニューゲーム」です。

f:id:degikids:20190408140326p:plain

最初のスタートダッシュで大きな差がつきます。

Storyboardベースでつくる

Xcode6以前はコードベースで進めるほうが効率的でしたが、
iOS9.0以降からはStoryboardの分割が容易になりコードベースで作るより圧倒的に開発が楽になりました。

Storyboard Referenceの登場によりファイルの分割や関連付けも容易に。
以前と比べてConflictの可能性も低くくなったと感じます。

f:id:degikids:20190408140945p:plain

Storyboardを活用しデザイン確認が行えると開発が円滑に進む場面が多いように思えます。
例えばちょっとしたボタンの位置、配色などデザイナーと机を合わせて確認・議論ができます。簡単なモックもコードを汚さず作ることが可能です。

SwiftGenを活用する

SwiftGenとはXcodeで使用される画像イメージ、フォント、 カラー、segue等のリソース名を自動的に生成し型付を行ってくれるライブラリです。 https://github.com/SwiftGen/SwiftGen

Storyboardの遷移はコードで行っています。
理由として再利用率が高い画面を切り離して考えられるからです。

簡易的な値渡し例えばWebViewで開く情報を segueで渡すとシンプルかつ直感的に実現できます。

画面遷移では performSegueを使用しており、prepereで値渡しを行っています。
そのためにはSegueIDをStoryboardから設定する必要がありこれを手作業で管理するのは厳しい。
Segueのデメリットは「いろんな場所に設定が散らばっていて情報が隠れやすく流用がしづらい」ということだと思います。
コードからSegueIDやStoryboardを利用しようとした場合に、補完が効かないためハードコードをするか、手動で管理をする必要があります。

そこでSwiftGenの登場です。
簡単にStoryboardの名前からStoryboardのSegueのIDで名まで自動で生成し型付してくれます。

f:id:degikids:20190408142658p:plain

buildしなくても角丸とかボーダーなどを確認できるようになります。

class CustomView: UIView {
    @IBInspectable var customBool: Bool = false
    @IBInspectable var customInt: Int  = 0
    @IBInspectable var customFloat: CGFloat = 0.0
    @IBInspectable var customDouble: Double = 0.0
    @IBInspectable var customString: String = ""
    @IBInspectable var customPoint: CGPoint = CGPointZero
    @IBInspectable var customSize: CGSize = CGSizeZero
    @IBInspectable var customRect: CGRect = CGRectZero
    @IBInspectable var customColor: UIColor = UIColor.clearColor()
    @IBInspectable var customImage: UIImage = UIImage()
}

まとめ

クックパッドマートのiOSアプリはまだまだ発展途上で完成はしていません。
現在もコツコツと開発を進めており日を重ねるごとに利便性は向上しています。
使用できるエリアの皆さんは是非使っていただけると幸いです。ありがとうございます!
この記事を通して、クックパッドマートのサービス開発にご興味を持っていただけた方がいらっしゃいましたら、ぜひ一緒にサービスを作りましょう!

www.wantedly.com

お知らせ

クックパッドマートでは、4/24(木)に、買物事業部のエンジニアによる発表とエンジニアとのミートアップを開催予定です。

cookpad.connpass.com

今回の記事のような生鮮ECそのものの仕組みや、流通の仕組みの開発に興味がある方、クックパッドマートのエンジニアと直接話してみたい方はぜひご応募ください!お待ちしています!

1枚のラベルの向こうには、1人のユーザがいる【連載:クックパッドマート開発の裏側 vol.3】

$
0
0

こんにちは。クックパッドマート連載3日目を担当します、買物事業部エンジニアの今井(@imashin_)です。

去年の10月ごろから、生鮮食品ECサービスクックパッドマートの販売者向けサービスの開発を行っています。クックパッドマートを利用するのは、商品を買うユーザだけではありません。商品を販売する方々にも簡単に利用できるよう開発を進めています。

今回は、どのようにして商品を販売者からユーザまで届けられるように開発しているかを紹介します。

クックパッドマートではどうやって商品をユーザに届けているのか

まず、今どのように商品を届けているのか、商品の注文から受け取りまでの流れを紹介します。

発注

f:id:ima_shin:20190410170609p:plain

販売者は、四六時中クックパッドマートだけを利用しているわけではありません。これまで通りの生産、販売業務が忙しい中で、クックパッドマートも利用していただいています。

そのため、販売者に合わせた方法を開発し、負担にならないようにしています。

みなさんの近くにある精肉店、青果店を思い出してもらうとイメージが湧くかもしれないのですが、販売者はFAXや電話で注文を受けていることが多いです。必ずしもIT、インターネットに慣れているわけではありません。そのためクックパッドマートは、毎日FAXでの発注書の自動送信を行っています(FAX送信にはTwilioを利用しています)。一方でスマホから見たいという要望の販売者向けに、LINE WORKS経由でも発注書をPDFにて送付しています。利用者の多いLINEと同じUIを提供しているLINE WORKSを利用することで、利用障壁を大きく下げることができています。

仕分

販売者は発注で受けた商品の発送準備を行います。この準備段階で、ユーザが受け取り時に目印とするラベルの貼り付けを行います。

  • 07:00商品に貼るラベルを遠隔で自動印刷する
  • 07:00-14:00販売者が注文を受けた商品にラベルを貼る。 商品をコンテナごとに仕分けする

f:id:ima_shin:20190410170612p:plain

商品ラベルについても、完全に操作不要で発行できる構成で設置し、必要な時に必要なラベルを発行しています。また、商品へのラベル貼り間違いが発生せず仕分けが素早くできるよう、コンテナ別、商品別でラベルが発行されるようにソートしています。こうすることで、負担をかけないように工夫しています。

配送

  • 11:00-14:00配送員がコンテナを受け取りにくる
  • 14:00-17:00受け取り場所にコンテナを配送する

f:id:ima_shin:20190410170536p:plain

配送員は指定のコンテナを受け取り、冷蔵状態を保ちながら商品を集荷し、受け取り場所まで配送します。

受け取り

  • 17:00-ユーザが受け取り場所にて、自分の注文した商品をピックアップする

ユーザには配送が完了すると通知が送られます。受け取り場所に行き自分が注文した商品のIDを確認し、コンテナからピックアップしていきます。

どのようにして今の配送を作ったのか

クックパッドマートはまだまだ完成していないサービスです。今の配送フローがベストだとは考えていません。これからも日々、改良を続けていきます。

ですが、リリース当初の状態からはかなり改良されています。今回はどのように改良、開発を行っているかを商品の受け取りに必要なラベルの発行にフォーカスして紹介していきたいと思います。

クックパッドマートでは、基本的に

  • 初めから実装せず、頑張る運用からやってみる
  • 頑張る運用の知見を元に、プロトタイプを試験運用する
  • 利用者に当てたプロトタイプの知見を元に、スケール可能なプロダクトを作る

の段階を踏んでサービス開発を行っています。(ex サービスリリース初期の話

今回は商品ラベルの発行にフォーカスして、実際に行った開発をお伝えしたいと思います。

頑張る運用をやる

初期の段階ではコストをかけてでも(後々自動化可能な)配送を行えるのか検証を行いました。商品ラベルの発行は人の頑張りで次のような運用をしていました。

  • 注文の締めとともに社内に設置されている複合機でラベルを印刷する
  • 配送業者にラベルを配送してもらう

f:id:ima_shin:20190410170808p:plain
複合機でのラベル印刷

検証結果としては、商品へのラベル貼りを販売者が問題なく行えることを確認できました。加えて、ラベルに印字したIDを元にユーザが自分の受け取るべき商品をピックアップできることも確認できました。

プロトタイプを販売者にあてる

ラベルを毎日郵送するにはコストが莫大にかさみますし、スケールさせることも困難になります。そこで次の段階として、販売者にラベルを印刷してもらう方向でプロトタイプを作成しました。

安価に、素早く開発できることを基準に技術選定を行い、iPadとiOS用のSDKを提供しているラベルプリンターを採用しました。

  • ラベル発行用iPadアプリを開発し、ラベルプリンターにて印刷できるようにする
  • プロトタイプを店舗に設置し、試験運用してみる
    • ただし、問題発生時にはバイク便にてすぐラベルを届けられるようにバックアップを用意

f:id:ima_shin:20190410171717j:plain

f:id:ima_shin:20190410171833p:plain
ラベルの印刷フロー

このラベル発行アプリとiPadとラベルプリンターを販売者に提供することで、ラベルの配達をなくすことに成功しました。しかしながら、多くの問題点も浮き彫りとなりました。

  • ラベルプリンターの紙詰まりによる故障
    • 耐久性に特化したラベル発行機でないと長期の運用は保守が大変だった
  • 操作可能な画面は不要
    • 導入当初は、発注内容の確認や商品の情報入力をiPadからできるのではと思われたが、実際には設置場所の狭さや操作する余裕がないことがわかった
  • 通信環境の不良
    • iPadが安定してIPアドレスを払い出せない
    • iPadとプリンターの接続状態を安定させることが難しかった
  • OS、アプリの管理
    • 販売者の操作なしにOS、アプリを常に最新状態に保たせる仕組み、運用を作ることが難しかった
  • 販売者ごとのITリテラシーの差異
    • 必ずしも全ての販売者がiPadやプリンターの操作に慣れているわけではなかった

このように実際にプロトタイプで試験運用した結果、多くの問題点を洗い出すことができました。ラベルは商品を販売者からユーザに届けるために必要不可欠なものです。毎日必ず発行できる安定性を実現させる必要があります。

スケールできるプロダクトを作る

安定してサービスを運営するためには、ラベル発行に高い安定性が必要だということを認識することができました。また広くスケールをさせるためには、誰でも簡単に設置、管理できる必要があります。そこで、以下のような要件を元にスケール可能なプロダクトの開発を行いました。

  • 安定してラベルを発行できる構成と設計
    • 完全に遠隔でラベル発行をコントロールできる
    • ラベル発行が可能か把握するための死活を監視する
    • ラベル切れ等によるラベル発行不可能状態になる事前に検知する
  • 簡単に導入、運用できる設計
    • 電源を刺すだけ利用できる
    • 複雑な操作なしに運用できる

以上を満たすように開発を行い、つい2週間ほど前に新たな構成でプロダクトをリリースしました。

今の状態

では、今の構成がどのようなものかを紹介しようと思います。

ハードウェア

安定した稼働を実現するために、以下の機器でラベル発行機を組みました。

  • LTE ルーター UD-LT1/EX + SORACOM Air
    • 定期リブート
    • ネットワーク断絶時のリブート
    • Syslog
    • 外部ネットワークからの設定変更
    • SNTPによる状態監視
  • TSP743IIE3-24J1 JP
    • 通電すると常にONの状態に固定可能
    • 紙詰まりしにくい
    • ネットワーク経由でコントロール可能
    • カバーが開いている、紙が詰まっている、ラベルが切れかかっている等の状態を取得可能
    • SNTPによる状態監視
  • Raspberry Pi Model B+
    • デバッグ、キッティング、監視用

f:id:ima_shin:20190410171946p:plain
ラベル発行の構成

各機器の安定性、死活監視を利用することで、ラベル発行を安定して行うことができるようになりました。Raspberry PiでLTE通信を行うこともアイデアとしては挙がっていましたが、リリース速度を重視し、一旦既存のルータ製品を採用することにしました。

ソフトウェア

以上のハードウェアを稼働させるために、主に3つの開発を行いました。

star_ethernet

スター精密製プリンターを制御するiOSやAndroidのSDKは提供されていたのですが、サーバから直接利用するケースが少ないのか、Rubyはサポートされていませんでした。しかし、ソケット通信によるプリンターのプロトコルについて、細かな仕様が提供されていたため、Rubyからスター精密製プリンターを制御するgemを作成しました。

基本的にはTCPソケットで制御コマンドを送信し、プリンターを制御します。公開されている全てのコマンドをラップし、ラベル発行に必要なハンドリングを可能にしています。

例えば、文字を大きくしたりレイアウトを変えたりする、QRコードを印字する、線を引くといった印字内容の操作もこれを用いて行います。ラベル台紙のカットやラベル送り、ビープ音を出すこともできます。プリンターの細かな状態を取得することもできます。

f:id:ima_shin:20190410172040p:plain
https://www.starmicronics.com/Support/Mannualfolder/UsersManual_IFBD_HE0708BE07_EN.pdf

mart_server

プリンターへのラベル発行命令はECSから送信します。

mart_server(クックパッドマート全体を支えるRailsアプリケーション)に発行すべきラベルの情報を集約し、日次バッチにて発行するラベル情報をstar_ethernetを利用してプリンターに送信します。

バッチにはkuroko2を利用し、barbequeで各プリンターへのラベル発行ジョブの管理しています。何かしらのトラブルでラベル発行に失敗した時は、原因を調査しジョブを再実行することで全てのプリンターで確実にラベルが発行されるようにしています。またラベル残量が少なくなっていたり、紙詰まりの発生を検知しています。

mart_shepherd

配布端末、ネットワークの管理を新たなのアプリケーションとしてmart_shepherdに切り出しました。

mart_shepherdはSORACOMプラットフォームとの間に立ち、mart_serverからgRPC経由のリクエストに応じて端末の管理を行います。また、ルーター、プリンター、ラズパイ各端末との通信時にはプロクシを行い、通信路を確立します。

アセンブル

実際にこれらの構成を設置するためには、機器を一つの什器にまとめてコンパクトにする必要があります。また、電源を刺すだけで簡単に運用を開始できるようにすることを目指しました。

そこで、一つのボックスに全ての機器を収納し、プラグを刺すだけで全ての機器の電源がONになり、即座に運用状態になるようにしました。

弊社には、ハードウェアを加工できる「工房」と呼ばれるスペースが存在し、そこで全ての加工、組立を行いました。

f:id:ima_shin:20190410172304j:plain
加工中の様子

f:id:ima_shin:20190410172230j:plain
加工済みのボックス

f:id:ima_shin:20190410172532p:plain
アセンブル済のボックスとプリンター

改善点

以上のように、安定して稼働するプロダクトを完成させることができました。2週間運用している限りでは、何かトラブルが発生しても遠隔で復旧することに成功していて、ラベルの発行ができない問題にぶつかったことはありません。

しかし、まだ改善点は残っています。

  • 低コスト化、小型化
  • 輸送可能な構成、耐衝撃性
  • 熱制御

これらを実現するために、引き続き開発を行っています。

まとめ

たかが商品のラベル1枚と思いがちですが、ラベルが発行されないとユーザーに正確に商品を届けることができません。1枚のラベル発行に失敗すると、1人のユーザが料理を作れない状態に陥ってしまいます。

そのようなことが決して起きないよう、たかがラベルの発行であっても真剣に開発に取り組んでいます。

これからもクックパッドマートは、素早いサイクルでの開発の元、安定したサービスの提供と、スケールを実現していきます。

この記事を通して、クックパッドマートのサービス開発にご興味を持っていただけた方がいらっしゃいましたら、ぜひ一緒にサービスを作りましょう!

www.wantedly.com

お知らせ

クックパッドマートでは、4/24(木)に、買物事業部のエンジニアによる発表とエンジニアとのミートアップを開催予定です。

cookpad.connpass.com

今回の記事のような生鮮ECそのものの仕組みや、流通の仕組みの開発に興味がある方、クックパッドマートのエンジニアと直接話してみたい方はぜひご応募ください!お待ちしています!

クックパッドマートAndroidアプリの画面実装を最高にした話

$
0
0

こんにちは。 連載シリーズ4日目を担当します、買物事業部 Androidエンジニアの門田(twitter: @_litmon_ )です。

↓↓↓以前の3日分のエントリはこちらから参照ください↓↓↓

買物事業部では、クックパッドの生鮮食品ECサービス「クックパッドマート」の開発を行っています。 今日は、先日リリースしたばかりのクックパッドマートAndroidアプリを開発する上で、画面実装の工夫について紹介しようと思います。

クックパッドマートAndroidアプリはこちらからダウンロードできます。ぜひ実際に触りながら記事を読んでみてください。 play.google.com

クックパッドマートAndroidアプリの画面実装

クックパッドマートAndroidアプリの主な画面は、大きく分けて3つに分類されます。この分類は、多少の違いはあれど一般的なAndroidアプリに対しても同様のことが言えるのではないでしょうか。

  • 一覧画面: データのリストを一覧表示する画面
  • 詳細画面: 一覧画面の特定のデータに対して詳細を表示する画面
  • 入力画面: データを登録したり追加したりするために入力を行う画面
一覧画面詳細画面入力画面
f:id:litmon:20190411123251p:plainf:id:litmon:20190411123345p:plainf:id:litmon:20190411123414p:plain

現代のAndroidアプリ開発において、一覧画面にはRecyclerViewが使われるのが一般的です。RecyclerViewは、同一のレイアウトを複数持つ一覧画面において非常に高いパフォーマンスを発揮するViewですが、AdapterやViewHolderなど実装するものが多く、若干扱いにくいのが難点です。

詳細画面の実装に関しては、スマートフォンのディスプレイは縦に長く、スクロールの方向も縦になるアプリが多いため、 ScrollViewやNestedScrollViewを使ってその中にレイアウトを組むのが一般的だと思います。

入力画面には、EditTextやCheckBoxなどを利用して入力欄を用意すると思います。また、入力項目が多くなった場合には詳細画面同様にScrollViewなどを使ってスクロール出来るように実装することが多いのではないでしょうか。

クックパッドマートAndroidアプリでは、上の例に漏れず一覧画面ではRecyclerViewを使い、詳細画面ではScrollViewを使うというスタイルを取っていたのですが、 このレイアウト手法で開発を進めていくのが大変になっていきました。 特に、詳細画面の実装をScrollViewで行っていくことに関して非常に苦しい思いをした例を紹介します。

レイアウトエディタでのプレビューが活用しづらい

ScrollViewを使って縦に伸びるレイアウトを組む場合、縦に伸びれば伸びるほどレイアウトエディタのプレビュー機能が使いにくくなっていきます。 また、レイアウトファイルも肥大化し、非常に見通しの悪い実装になりがちです。

詳細画面の実装がActivity, Fragmentに集中して肥大化しやすい

RecyclerViewを使うと、Viewの実装の大半はViewHolderクラスに分離することが出来ます。 しかし、詳細画面ではScrollViewを使っているため、データをViewに紐付ける処理をどうしてもActivity, Fragment内に書くことが多くなります。 DataBindingやMVVMアーキテクチャなどを使ってViewの実装をActivity, Fragmentから分離する手法などもありますが、プロジェクトによってはあまり適さないケースもあるでしょう。 また、RecyclerViewを使う一覧画面と実装差異が出てしまい、処理の共通化などが難しくなってしまいます。

なにより実装していて苦しい

詳細画面のような複雑なレイアウト構成を1つのレイアウトファイルに対して上から順に実装していくのは、精神的にも苦しいものがあります。 長くなればなるほどレイアウトエディタ, XML両方の編集作業が難しくなっていくため、細かい単位でレイアウトを分割できるRecyclerViewのような仕組みが欲しくなってきます。

includeタグ?知らない子ですね……

すべての画面をRecyclerView化する

そこで、RecyclerViewをうまく使うことで詳細画面もうまく組み立てることが出来るのでは?と考えました。RecyclerViewは、レイアウトを行ごとに分割して作成することが出来るし、ViewHolderへViewの実装を委譲出来るため、ActivityやFragmentの肥大化を防ぐことが出来ます。 ただ、RecyclerViewの実装には、AdapterとViewHolderの実装が必要で、特に複雑な画面になるほどAdapterの実装が面倒になっていきます。

class DeliveryDetailOrderItemsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    overridefun getItemCount(): Int =
        1 + 1 + items.size + 1overridefun getItemViewType(position: Int): Int {
        if (position == 0) {
            return R.layout.item_delivery_detail_header_label
        }

        if (position == 1) {
            return R.layout.item_delivery_detail_order_item_note
        }

        if (position == itemCount - 1) {
            return R.layout.item_delivery_detail_order_item_footer
        }

        return R.layout.item_delivery_detail_order_item
    }
}

RecyclerViewで受け取り詳細画面を実装したときの一部を抜粋してきました。 表示するpositionに応じてそれぞれのitemViewTypeを変える必要があるのですが、全く直感的ではなく、頭を使って実装する必要があります。 また、Viewを追加したいという変更があったときに、他の部分にも影響が出る場合があるので、保守性も高くありません。

すべての画面でこのような複雑なRecyclerView.Adapterを実装するのは気が滅入りますし、現実的ではありません。 しかも、読み込んだデータに応じて表示を変えなければいけない、となるとより面倒になるのは必至です。 そのため、RecyclerView.Adapterの実装を簡素に行うためのライブラリを導入することにしました。

Groupieを使う

RecyclerView.Adapterの面倒な実装を便利にしてくれるライブラリは巷にいくつかありますが、今回はGroupieを使うことにしました。 同様の仕組みを持つEpoxyというライブラリも候補に上がっていましたが、判断の決め手となったのは以下の点でした。

  • EpoxyはannotationProcessorを使ったコード生成機構が備わっており、DataBindingと連携させるととても便利だが、クックパッドマートAndroidアプリではDataBindingを使っておらずオーバースペックだった
  • GroupieはRecyclerView.Adapterを置き換えるだけで使えるので非常に簡素で、今回のユースケースにマッチしていた

例えば、Groupieを使って一覧画面のようなデータのリストを表示させるために必要なコードは以下です。

dataclass Data(val name: String)

class DataItem(valdata: Data) : Item<ViewHolder>() {
   overridefun getLayoutId(): Int = R.layout.item_data

   overridefun bind(viewHolder: ViewHolder, position: Int) {
       viewHolder.root.name.text = name
   }
}

val items = listOf<Data>() // APIから返ってきたリストとするval adapter = GroupAdapter<ViewHolder>()
recycler_view.adapter = adapter

adapter.update(mutableListOf<Group>().apply {
    items.forEach {
      add(DataItem(it))
    }
})

たったこれだけです。とても簡単ですね。

詳細画面の場合、データの有無で表示する/しないを切り替える必要があったりしますよね。 例えば、クックパッドマートAndroidアプリのカート画面では、カートに商品が追加されていない場合と追加されている場合で表示が異なります。

f:id:litmon:20190411123600p:plain:h300f:id:litmon:20190411123620p:plain:h300

このようなレイアウトになるようにRecyclerView.Adapterを自前で実装しようとすると、 getItemViewType()メソッドの実装に苦しむ姿が簡単に想像できますね……絶対にやりたくありません。

しかしこれも、Groupieで表現すると以下のように簡単に表現することができます。簡略化のため、表示が変わる部分のみを表現します。

class Cart(
    val products: List<Product>
) {
    class Product
}

class CartEmptyItem : Item<ViewHolder>() { /* 省略 */ }
class CartProductItem(val product: Cart.Product) : Item<ViewHolder>() { /* 省略 */ }

val cart = Cart() // APIから返ってきたカートオブジェクトval adapter = GroupAdapter<ViewHolder>()
recycler_view.adapter = adapter

adapter.update(mutableListOf<Group>().apply {
    if (cart.products.isEmpty()) {
        add(CartEmptyItem()) // 商品が追加されていない旨を表示する
    } else {
        cart.products.forEach {
            add(CartProductItem(it)) // カートの商品をリストで表示する
        }
    }
})

非常にコンパクトな実装に収まります。 前述の例とあわせて見ると、getItemViewType()を実装するのに比べて直感的になることも理解できると思います。

LiveDataと組み合わせて使う

LiveDataと組み合わせて使う場合もとても簡単です。Fragment内で使うことを例に挙げてみましょう。

class CartFragment : Fragment() {

    class CartViewModel : ViewModel {
        valdata = MutableLiveData<Cart>()
    }

    val viewModel by lazy {
        ViewModelProviders.of(this).get<CartViewModel>()
    }

    val adapter = GroupAdapter<ViewHolder>()

    overridefun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_cart, container, false)
    }

    overridefun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        recycler_view.adapter = adapter

        viewModel.data.observe(this, Observer {
            it?.let { cart ->
                adapter.update(mutableListOf<Group>().apply {
                    cart.products.forEach { product ->
                        CartProductItem(product)
                    }
                })
            }
        })
    }
}

非常に簡単ですね。 Groupieはupdate時に内部でDiffUtilsを使って差分更新を行ってくれるため、APIリクエストを行った結果をLiveDataで流すだけで簡単に更新が出来ます。

その際、GroupieのItemに対して以下の2点を見ておく必要があります。

  • id が同一になるようになっているか
  • equals が実装されているか

idの設定は、getId()メソッドをoverrideすると良いでしょう。 もしくは、Itemクラスのコンストラクタ引数にidを渡すことも出来ます。

equals()メソッドの実装は、Kotlinならばdata classで簡単に実装することが出来ます。 また、引数を持たないようなItemで、特に中の内容も変わらないような場合は自前で実装してしまっても良いでしょう。

dataclass CartProductItem(val product: Cart.Product) : Item<ViewHolder>() {

    overridefun getLayoutId(): Int = R.layout.item_data

    overridefun getId(): Int = product.id

    overridefun bind(viewHolder: ViewHolder, position: Int) {
        viewHolder.root.name.text = name
    }
}

// idをコンストラクタで指定class CartEmptyItem : Item<ViewHolder>(0) {

    overridefun getLayoutId(): Int = R.layout.item_data

    overridefun hashCode(): Int = 0// 同じItemなら同じと判定して良いoverridefun equals(other: Object): Boolean =
        (other instanceOf CartEmptyItem)

    overridefun bind(viewHolder: ViewHolder, position: Int) {
        // ignore
    }
}

これらの設定がうまくいっていない場合、更新されたときに無駄なアニメーションが走ってしまうため、できるだけ全てのItemに実装しておくことをオススメします。

実際にクックパッドマートAndroidアプリでは、ほぼすべての画面がこの構成を取って実装していて、画面回転時やFragmentのView再生成にも問題なく状態を再現してくれるのでとても便利な構成になっています。

Groupieを使うことで良くなった点

Groupieを使うことで、RecyclerViewの面倒な実装を簡略化でき、かつすべての画面の実装を定型化することが出来ました。 これにより、以下のような効果が生まれました。

  • Fragmentの実装をすべての画面でほぼ定形化出来るため、精神的に楽に実装できるようになり、かつ処理の共通化が簡単になった
  • RecyclerView.Adapterに比べて、複雑なレイアウトを組むのが非常に簡単なので、実装工数を大幅に削減することが出来た
  • レイアウトが強制的にItem単位になるため、シンプルにレイアウトを作成することが出来るようになった

1つ目の処理の共通化には、例えばエラー画面が挙げられます。 読み込みエラー時の画面表示をGroupieのItemで用意することで、非常に簡単に全ての画面で同一のエラー画面を用意することが出来ます。

class ErrorItem(val throwable: Throwable): Item<ViewHolder>() {
    /* 省略 */
}

apiRequest()
    .onSuccess { data->
        adapter.update(mutableListOf<Group>().apply {
            add(DataItem(data))
        })
    }
    .onError { throwable ->
        adapter.update(mutableListOf<Group>().apply {
            add(ErrorItem(throwable))
        })
    }

また、アプリ内のItemの間に表示されている罫線も、RecyclerViewのItemDecorationを使うことでアプリ全体で簡単に共通化することが出来ました。 RecyclerViewにLinearLayoutManagerとあわせてdividerを設定することがとても多かったため、RecyclerViewに以下の拡張関数を実装して使うようにしています。

fun RecyclerView.applyLinearLayoutManager(orientation: Int = RecyclerView.VERTICAL, withDivider: Boolean = true) {
    layoutManager = LinearLayoutManager(context).apply { this.orientation = orientation }
    if (withDivider) {
        addItemDecoration(DividerItemDecoration(context, orientation).apply {
            ContextCompat.getDrawable(context, R.drawable.border)?.let(this::setDrawable)
        })
    }
}

Groupieで難しかった点

Groupieを使っていて、難しかった点もいくつかあります。 例えば、受け取り場所の詳細画面には地図を表示しているのですが、MapViewにはMapFragmentをアタッチする必要があります。 単純にaddするだけの実装だと、スクロールして戻ってきたときにクラッシュしてしまうので、unbind時にFragmentを取り除く必要がありました。 苦肉の策ですが、現状は以下のような実装になっています。

dataclass SelectAreaDetailMapItem(
    val item: Location,
    val mapFragment: SupportMapFragment,
    val fragmentManager: FragmentManager
) : Item<ViewHolder>() {
    overridefun getLayout(): Int = R.layout.item_select_area_detail_header

    overridefun getId(): Long = layout.toLong()

    overridefun bind(viewHolder: ViewHolder, position: Int) {
        val markerPosition = LatLng(item.latitude, item.longitude)
        fragmentManager.beginTransaction()
            .add(R.id.item_select_area_detail_map, mapFragment)
            .commit()
        mapFragment.getMapAsync { map ->
            map.addMarker(MarkerOptions().position(markerPosition))
            map.moveCamera(CameraUpdateFactory.newLatLng(markerPosition))
            map.setMinZoomPreference(15f)
        }
    }

    overridefun unbind(holder: ViewHolder) {
        super.unbind(holder)
        fragmentManager.beginTransaction()
            .remove(mapFragment)
            .commit()
    }
}

すべての画面でGroupieを使うことで実装が簡単になった、アプリ全体で処理を共通化出来たというメリットはありましたが、こういう風に扱いに困るケースもあるため、用法用量を守って正しくお使いください。

まとめ

  • Androidアプリ開発において主要な画面はだいたいRecyclerViewで表現できる
  • Groupieを使うと実装も簡単になって最高
  • 難しい画面もあるので適材適所で使い分けよう

おしらせ

4/24(木)に、買物事業部のエンジニアによる発表とエンジニアとのミートアップを開催します!!! cookpad.connpass.com

そこでは、今回語らなかったAndroidアプリの技術的な部分を紹介していこうと思います。 実際のソースコードも見せたり……あるかもしれませんね。 ぜひぜひ!!ご興味のある方は参加お待ちしています!

Viewing all 802 articles
Browse latest View live