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

CoffeeScript スタイルガイドの公開とその目的

$
0
0

こんにちは、クックパッド編集室の太田(@os0x)です。 普段は料理動画やクックパッドニュースなど、メディア寄りのサービスを担当しながら、社内のCoffeeScriptを中心としたウェブフロントエンドのコードレビューなどを行っています。 今回は、そのCoffeeScriptのレビューを円滑に行うためのコーディングスタイルについてお話したいと思います。

Style guides in Cookpad

クックパッドでは、github.com上でスタイルガイドを公開しているのをご存知でしょうか? cookpad/styleguide

これまで、Ruby / Objective-C / Java のコーディングスタイルが公開されていました。そして、本日 CoffeeScript のコーディングスタイルを追加しました。

さて、そもそもスタイルガイドとはなんでしょうか?コーディング規約とも言われたりしますが、スタイルガイドとコーディング規約はどう違うのでしょうか?

コーディング規約(ルール)とは

コーディング規約は、こう書かなければいけないというルールを定めたものです。それはルールなので、必ず従わなければいけません。 例えばJavaのコーディング規約はIDEの構文チェックツール*1とセットになっていて、その規約に従わなければわかりやすいエラーメッセージを表示してくれるといった具合です。 Javaような静的型付け言語では、言語の特性に加えてIDEの強力なサポートもあるので、ルールがあることでコードが書きやすくなる上に、レビュー時も細かいスペースについて指摘し合うような時間を節約することができます。

スタイルガイドとは

スタイルガイドは、ガイドという名前の通り、ルールほど強制力が強くないところがコーディング規約との大きな違いといえるでしょう。 こう書いたほうが読みやすいという部分を定めたものなので、ガイドに従っても読みやすくならない場合などはガイドに従わないということもあります。

RubyやJavaScript、CoffeeScriptのような動的型付けでインタプリタで実行される言語の場合、その動的さ故にIDEによるサポートは十分ではありません。 そういった環境で厳密なルールを決めて強制した場合、コードを書くことが窮屈になり、楽しくなくなってしまいます。 RubyやCoffeeScriptのような言語は書いていて楽しいというのが最大の長所と言っても過言ではないと思う*2のですが、そこに厳密なルールを適用してしまうと、その長所を潰してしまいかねません。 だからこそ、スタイルガイドという形がこれらの言語には最適なのです。

コードレビューとガイド

そもそも、なぜコーディング規約・スタイルガイドが必要なんでしょうか。 ガイドがあることで、

  • スペースの取り方などを統一されてコードがキレイになる
  • 議論になりそうな部分を定めておくことで、議論の時間を節約できる(文字列リテラルにシングルクォートとダブルクォートのどちらを使うかとか)
  • 書きやすさより、読みやすさを優先したコードにできる

といったメリットがあります。これらのメリットはコードのメンテナンス性を上げ、同時にレビューの効率をあげることに貢献します。

「書きやすさ」より「読みやすさ」

さて、スタイルガイドのメリットとして「書きやすさより、読みやすさを優先したコードにできる」という項目を挙げましたが、これについて少し掘り下げたいと思います。

コードの書き方というのは人それぞれですが、その書き方は、意識して書き方を変えていたりしない限りは、自分が書きやすい書き方で書いているはずです。 自分が書きやすい書き方が他の人、もしくは3日後の自分に取って読みやすい書き方とは限りません。 コードを書くのは一瞬(完成するかは別の話)ですが、一度書かれたコードはその瞬間から自分自身が繰り返し読むはずです。例えば、1行追加するだけの修正でも、前後の行はしっかり見てから追加しますよね。そういったことを繰り返し行なっているはずです。 さらにレビューなどで他人も読むのであれば、コードは書かれている時間より読まれている時間のほうが圧倒的に長くなります。

だからこそ、クックパッドのスタイルガイドは、書きやすさより、読みやすさを優先しています。 例えば、CoffeeScriptのガイドでは、「[MUST]メソッドの呼び出しの括弧は基本的に省略しない」という項目があります。 書きやすさだけを考えたら、括弧は省略したほうが書きやすいことはまず間違いないでしょう。しかし、読みやすさについて考えてみると、括弧がしっかり書いてあったほうが読み間違いをしにくく、レビューなどもしやすいと考えているので、括弧を省略しないというガイドを作りました。

最後に

スタイルガイドを定めておくと、レビューを円滑にすることができるので、より本質的な部分に集中しやすくなります。今回公開したCoffeeScript以外にも、Ruby / Objective-C / Javaのコーディングスタイルも公開していますので、この機会に目を通して頂ければ幸いです。

クックパッドでは一緒にレビューしあう仲間を募集しています。是非ご応募ください。

ソフトウェアエンジニア | クックパッド株式会社 採用情報

*1:CheckStyleやFindBugsなど

*2:あくまで個人的な意見です


Android開発でRxJavaをチームに導入した話

$
0
0

買物情報事業部の八木(@sys1yagi)です。

Android界隈でRxJavaが話題になっていますね。クックパッドアプリ(以後、「本体」と表現します)でも先日ついにRxJavaの導入を果たしました。本エントリではRxJavaをチームに導入する為に行ったいくつかの取り組みを紹介します。

目次

  • RxJava導入の失敗
  • どのような課題を解決するのか
  • 導入の為に機能を分解し、学習コストを考える
  • ブログを書く
  • 低コスト、低リスクに導入する
  • 勉強会を開く

RxJava導入の失敗

2014年11月にRxJavaの1.0.0がリリースされました。遂に実用段階かという事で個人的にあれこれ触り、本体に導入する機会を伺っていました。ある日、bug fixの為にRxJavaを使うと簡潔になるのではないかと思い気軽にPull Request(以後、PRとします)を送った所、「このタイミングで急に導入する意図はなにか?」「RxJavaでなければいけない理由は何か?」「きちんと学習しているメンバーが少ない中で導入してメンテは大丈夫か?」といった突っ込みが入り、PRからおよそ2時間ほどでRxJavaとお別れとなりました。

f:id:sys1yagi:20150415155611p:plain

レビューでの突っ込みはもっともで、明確な理由もないままチーム全体として学習が不十分なライブラリを持ち込むのはまずいですよね。メリットよりリスクの方が高そうです。

どのような課題を解決するのか

導入に失敗した私は反省し、まずはRxJavaによって本体開発のどのような課題を解決できそうなのかを洗い出しました。

No課題どのように解決できるか
1非同期処理系の書き方に統一性がない。複数の非同期処理を合成する為のうまいやり方が定まっていないRxJavaをPromiseとして使う事で単一の機能を持つ非同期処理群を構成できる。それをドメインに合わせて合成するスタイルにすれば、AsyncTask内でCountDownLatchで待ち合わせるようなスタイルを駆逐できる
2FragmentでgetActivity() == nullなどの状態チェック漏れによってクラッシュする問題Fragmentのライフサイクルに合わせてunsbscribeする機構を作るかRxAndroid(RxJavaを使ってAndroid用に書かれたコンポーネント群です)等を使う事で状態チェックが不要になる

また、導入によって期待できる効果についても検討しました。

No期待できる効果理由
1複雑なロジックを統一的な記述にできるObservable化した後の記述はRxJavaに沿うので統一的になる。また不自然な書き方や無理やり書いた場合などの検出がしやすい
2メンテナビリティの向上各処理をOperator単位で記述するので処理の意図が理解しやすくなる
3アプリケーションアーキテクチャの改善現在のアーキテクチャを改善していく際にRxJavaの思想が応用できるのではないか

「期待できる効果」の方はともかく、課題の解決に有効という事であれば学習コストを支払う価値は十分あるでしょう。なぜなら上記の2つの課題は本体開発においてずっと有効な手段が得られないままでいたからです。すぐにでも導入したい所ですが本当に課題の解決に有効かどうかについてどのように判断すればよいでしょうか。それはRxJavaを学習し適用しながら検証するしかないですね。この辺りが学習コストの大きいRxJavaのジレンマとなります。気軽に始められてやめたければ引き返せるような入り口を探す必要があります。

導入の為に機能を分解し、学習コストを考える

そこで、機能を分解して学習コストを抑えられるアプローチがないか探す事にしました。

RxJavaの機能の活用方法は概ね以下の様に分解できます。

  • List処理の抽象化・ストリーム化
  • Optional
  • Promise
  • Data Binding
  • Event Bus

これらのうち、学習コストを抑えられそうなのはList処理ではないかなと思います。Optionalはアプリケーションの設計に大きく関わってくるのでOptionalそのものの議論が必要になりますし、Promiseはスレッド操作周りを意識する必要がありいきなり取り組むにはコストが高そうです。Data BindingやEvent BusはSubjectを利用するので、Observableを知る前に触れるのはオススメできません。コードを書きながらObservableとOperatorの関係に触れつつ、低コスト・低リスクに学ぶにはList処理が一番よさそうです。もし「RxJavaはダメかもね」という判断になった時も、List操作での適用だけなら比較的簡単に戻せるでしょう。

ブログを書く

RxJava導入によって解決出来そうな課題と、低コストで学習できそうな方法が定まったので次にこれらをまとめたエントリを社内ブログに書きました。社内のブログなのでここには載せられませんが、概ね同じような内容を個人ブログ(RxJavaをコレクション操作ライブラリとして捉えれば学習コストと導入リスクを低減できるのではないか)に書いています。

社内ブログのエントリでは、本体開発においてRxJavaを導入する事で期待できそうな点について言及しつつ、Observableの概要と基本的な使い方をList操作をベースに解説しました。またエントリの末尾でRxJavaを導入するPRを送るという宣言もしました。

f:id:sys1yagi:20150415155644p:plain

低コスト、低リスクに導入する

社内ブログを周知したあと、ブログでの宣言通りList操作周りの実装を置き換えたPRを送りました。変更対象はListUtilsというリスト操作周りのユーティリティクラスで、T head(List<T>), List<T> tail(List<T>)などの実装をRxJavaを使って置き換えました。

これらの実装は元々あったものなので戻そうと思えばいつでも戻す事ができ、低リスクです。同時に特にメリットが無いのですが、「RxJavaを適用したコードをレビューする」という機会を作るのには最適です。

f:id:sys1yagi:20150415155714p:plain

このPRをマージした後、ビジネスロジック部分のList操作周りを中心にRxJavaを適用を進めていきました。その中で冗長な部分やわかりにくい部分をレビューで指摘してもらい改善していきました。

勉強会を開く

ある程度皆がRxJavaコードのレビューに慣れて来た段階で勉強会を開く事にしました。発表内容は初級から応用まで様々で、発表者の多くが発表の為にRxJavaの調査に力を入れていました。いわゆる勉強会駆動ってやつですね。当初は関係者6,7人で静かにやろうと考えていましたが、どんどん参加者が膨らみ最終的には20名くらいまでになりました。

f:id:sys1yagi:20150415160210p:plain

この勉強会によって、発表者はよりRxJavaの学習がより進み、参加者も基本的な部分の理解や学習のきっかけをつかめたように思います。

以下は勉強会の際に発表した資料です。公開されているものについて列挙します。

おわりに

勉強会後、本体開発にRxJavaが用いられる機会が増えました。現在ではAPI call周りのObservableを使ったPromise化が進んでいます。API callの並列化、直列化の書き方や、実行スレッドの設定をどこで書くかといった話や、可読性などについて日々レビューの中で議論しています。ゆくゆくはData BindingやEvent Bus等の適用についても検討されていくでしょう。また、解決できる課題として挙がった非同期処理やライフサイクルの問題に対しても徐々にRxJavaを適用しつつあります。今のところ大きな問題はでておらず期待通りの効果を得られています。

以上がAndroid開発でRxJavaをチームに導入する為に行った取り組みです。本エントリがRxJava導入の検討の役に立てば幸いです。

そんなクックパッドではユーザーの課題をどういう風にモバイルで解決するかを考え、実現するために情熱を注げる方を募集しています!興味がある方はぜひご応募ください。

Docker を利用した Web アプリケーションのデプロイ

$
0
0

技術部の鈴木 (id:eagletmt) です。

クックパッドでは一部の Web アプリケーションサーバで Docker が使われており、今回はそのデプロイ方法について紹介します。

Docker で Web アプリケーションをデプロイするときには、まだまだベストプラクティスがある状況ではありません。 たとえば、どのように無停止でデプロイするか、どのようにコンテナと通信するかといった問題があります。 最初に Apache Mesos と Marathon などのツールを検証しましたが、クックパッドの環境において使いやすそうなものはなく、最終的に自前でデプロイのしくみを作ることにしました。 しかし Docker 周辺のツールは様々な新しいものが出てきている最中です。 今はまだベストなものが無いけれども、近いうちによりよいものが出てくるかもしれません。 そのため、できるだけ単純なしくみにしておくことで、後から別のしくみに移行しやすいようなものにしようと意識しました。

デプロイの流れ

Docker 単体では無停止でのデプロイを実現するのは難しいため、各ホストに nginx を立てて、デプロイ時に nginx の設定を更新する形にしています。 全体の構成は下図のようになっています。

デプロイは以下の流れで進みます。 既に v1 が稼動しており、新しく v2 をデプロイするとします。

  1. docker pull v2-image
  2. docker run --detach v2-image
  3. v2-container のヘルスチェックが通るまで待つ
  4. nginx の設定を v1-container から v2-container へ書き換えて nginx を reload
  5. コネクションのタイムアウト分だけ待つ
  6. docker stop v1-container

Docker を利用した Web アプリケーションのデプロイ方法としてはわりと一般的な方法かと思います。 とはいえ、この中でいくつか考えなければならない問題があります。具体的には、通信方法、設定値、権限の問題です。

通信方法

一つのホストに複数のコンテナを立てられるようになっており、リクエストをどのコンテナにプロキシするかはバーチャルホストで行っています。 nginx とコンテナ間の通信方法として、2つの方法を用意しています。

UNIX ドメインソケットを利用する

docker runのときに適当なディレクトリを --volumeでマウントし、コンテナ内の Web アプリケーションはそのディレクトリに UNIX ドメインソケットを作成して listen します。 するとホスト側にもその UNIX ドメインソケットが見えるため、そこへプロキシする方法です。 コンテナのネットワークを分離でき、オーバーヘッドが小さく、ポートが被る問題に悩まなくてよいため、基本的にはこの方法を使うようにしてもらっています。

Docker の bridge networking を利用する

docker run--publishオプションを渡し、そこで指定したポートへとプロキシする方法です。 この方法は Docker イメージが公開されているソフトウェアをそのま利用するとき等に使います。 ホスト側のポート番号は、現在のホスト内で動いている Docker コンテナが使っているポート番号のうち最も大きなものに 1 を足したものを使うようにしています。 雑な割り当て方法ですが、これでもほとんどの場合でうまく動きます。

設定値

Docker コンテナに設定値を外から与えたい場合、環境変数が最も楽な方法です。 問題はその環境変数をどのように管理し、どのように与えるかです。

環境変数の管理には etcd を利用しています。 etcd にアプリケーション毎に環境変数の値を保存し、etcenvを使って環境変数を一時ファイルへ出力し、それを docker run --env-fileで読み込ませる、というようにしています。

権限

Docker は root ユーザで動いており、Docker コンテナを操作するためには root 権限が必要です。 Docker デーモンの UNIX ドメインソケットを root 以外も書き込めるようにしたり、あるいは tcp で bind すれば root 権限がなくてもコンテナを操作できますが、 docker run --volumeオプションを自由に使われてしまうとホスト側で root しか読み書きできないファイルもコンテナ内から閲覧できてしまう等の問題があります。

権限の問題を解決するため、デプロイに必要な操作をすべて行うデプロイスクリプトをホスト側に用意し、そのデプロイスクリプトに対してのみ全ての開発者に sudo を許可する、という形にしています。 またデプロイがホスト側で完結するので、何かのオペレーションでデプロイが必要になったときに、コマンド一つで再デプロイできるのも利点です。 開発者がデプロイするときは、Capistrano 経由でこのデプロイスクリプトを実行しています。

今後

このしくみでデプロイ自体はうまくできているものの、全体で見ると完全には自動化できていない部分があります。 たとえば現状では、デプロイ先のホストを決定するために EC2 のタグを使っており、このタグは運用者が設定しています。 つまり、どのアプリケーションをどのホストにデプロイするかを運用者が指定している状況です。 Docker によって得られたポータビリティを活かして、必要なメモリ量やコンテナ数などを設定すれば適当なホストにデプロイされるようになるとよさそうです。 さらに動的にコンテナ数を変えると自動的にスケールイン・スケールアウトしてほしいです。 その実現のために、Amazon EC2 Container Serviceがいいのか、swarmがいいのか、あるいは自前で何か作ったほうがいいのか、色々と検討中です。

まとめ

できるだけ単純なしくみにして後からもっとよいツールが出てきたときに対応しやすいようにしつつ、現状ではどのようにして Docker を利用した Web アプリケーションをデプロイしているか紹介しました。 Docker を利用した大まかなインフラ構成や利用しているミドルウェアの話はよく見ますが、Web アプリケーションのデプロイ方法についての具体的な話はあまり見ないので、一つの参考になればと思います。

200万品のレシピデータから感謝を伝えるインフォグラフィックを制作した話

$
0
0

f:id:transit_kix:20150421180149p:plain

ユーザーファースト推進室のデザイナー倉光です。

クックパッドではユーザーさんから寄せられた多くのレシピが公開されていますが、先日レシピ数が200万品に到達しました! 特設ページ「ありがとう!みんなのレシピが200万品♪ 」では、レシピが200万品に至るまでの歩みを紹介するインフォグラフィックを公開しています。

今回はこのインフォグラフィックの制作事例を紹介したいとします。

インフォグラフィックとは?

f:id:transit_kix:20150421134809p:plainインフォグラフィックとは、情報、データ、知識を視覚的に表現したものです。図やグラフなどを用いることで、データを目で見てわかりやすく伝える際などに用いられます。

クックパッドが持つデータといいますと、一例ですが「今日このレシピが何回表示されました」「今月はこのキーワードの検索回数が急上昇中です」といったものが挙げられます。(ちなみに一部のデータは研究者の方向けに公開も行っています)。膨大な数のデータは、それ自体は事象や数値の集合体に過ぎません。これらに対して視点を与え、情報を再構成し、受け手にメッセージを伝えるのがインフォグラフィックの役目です。

視点を与える

今回のインフォグラフィックの制作時のエピソードに移りましょう。

今回の企画は、投稿推進部とユーザーファースト推進室のメンバーで協力して制作を行いました。まずは200万品レシピにまつわる面白そうなデータを選定します。

f:id:transit_kix:20150421134816p:plain

まずはこのようなデータが集まりました。さて、これから何を伝えるか?どう伝えるか?その視点を見つけるのがデザイナーの仕事です。

今回、私たちが一番伝えたかったことは「レシピ作者さんへの感謝の気持ち」でした。そこで各データをただ羅列するのではなく、クックパッドにおけるユーザーコミュニケーションの仕組みも紹介し、レシピ作者さんへの感謝を伝えて、未来のレシピ作者さんへ投稿のきっかけを与えられるようなストーリーにしようと考えました。

ストーリーを具現化する

ストーリーが決定すると、次のフェーズでは「データをどう構成するか」のアイデアを考えます。

f:id:transit_kix:20150421134826p:plain

私はふせんと太めのペンを使用して、手書きで思考を整理することが多いです。今回は一枚のイメージとして表現することが決まっていたので、情報を1ブロックごとに1枚のふせんに記入して、ふせん同士を貼り付けて構成を検討しました。この手法の利点は、気軽に持ち歩けるのでいつでもどこでも情報設計のプロトタイピングが開始できること、ふせんを動かすだけで情報の順序の入れ替えや修正が容易にできること、デザイナー以外のスタッフと議論する際も気軽にデザインに参加してもらいやすくなること… などが挙げられます。また、簡易的ではありますがPC/スマートフォンで情報の構成をどう変化させるかもこの時点で検証しておきます。

この時点でそれぞれのデータに対し、どのようなデザインパターンを適用するかも考えます。今回は、クックパッドにおけるユーザーコミュニケーションの説明にはサイクル型を採用。

f:id:transit_kix:20150421135600p:plain

レシピを書くと、そのレシピを見た他のユーザーさんが料理を作り、感謝の気持ちがつくれぽとして作者さんに届く仕組みを目で見てわかるように図解することにしました。

楽しく、わかりやすく、一目瞭然に

このあと、実際にグラフィックデザインの作業に移行します。 f:id:transit_kix:20150421135300p:plain

この時に気をつけるのは「楽しく、わかりやすく、一目瞭然であること」。大きさ、色、形状などの対比で情報に強弱をつけることで、全体を見たときのまとまりも意識します。また料理に関するデータなので、食や料理にまつわるモチーフを使用することで、情報に対して親しみやすさを感じられるように心がけました。

f:id:transit_kix:20150421135323p:plain

ちなみに今回のインフォグラフィックでも使用しているクックパッドオリジナルのピクトグラム。こちらは社内でオリジナルフォントとして管理されており、サービス全体で統一感を保ちつつも効率的にデザインすることが可能になっています。

またクックパッドの文化として「デザインレビュー」と呼ばれる仕組みがあります。デザイナーはGitHubのIssue機能を利用し、担当する仕事について仮説設計にはじまり最終的な表層部分のスタイリングに至るまでの過程を言語化し、それを社内のデザイナー全員でレビューし合います。Issue上では「このビジュアル表現では言いたいことが伝わってこない」「そもそもの仮説設計がおかしいのでは?」といった議論が日々行われています。

今回もデザインレビューの結果、情報が伝わりづらかった一部のモチーフの選定を変更したり、順序の入れ替えが行われた後に最終版が完成しました。

1つのレシピデータの向こうには1人のユーザーがいる

公開後から2週間で、インフォグラフィックのページには約30万PVがありました。更に嬉しい反応として、レシピ作者さんがいちおしレシピを投稿できるコーナーを用意していたのですが、約1700件ものいちおしレシピが寄せられています。

レシピ作者さんのいちおしレシピが大集合!

それぞれのレシピに添えられているエピソードも「母が作ってくれた思い出の味です」「このレシピがきっかけで沢山の友達が出来ました」といった素敵なものばかり。まさに珠玉のレシピ集となりました。1品のレシピデータの向こう側には、それぞれの想いを持って投稿してくれた1人のレシピ作者さんがいます。今回の企画を通して、改めて200万品という数字の重みを実感しました。

今回の企画に限らずクックパッドではユーザーの本質的欲求に応えるべく、データに対し視点を与え、問いを立てることで日々サービスの改善・運用活動を行なっています。

クックパッドでは、日々の料理を楽しみにするためのデザインを本気で考えたい仲間を募集中です!

クックパッド株式会社 採用情報

Droidcon Italy 2015でアプリのパフォーマンスの話をしました

$
0
0

海外向けのAndroidアプリを開発している @rejasupotaroです。 4/12、13にDroidcon ItalyというAndroidのカンファレンスがありました。

Droidcon Italy 2015

私はそのカンファレンスでパフォーマンスの話をしてきました。この記事の前半では、カンファレンスで発表したパフォーマンスの話を、後半に海外のカンファレンスに参加してみてどうだったかということを書きます。

パフォーマンスの話

以下のような3部構成で話をしました。

  • HTTPと通信を行う環境
  • 画像の読み込みと最適化
  • UXを高めるAPI設計

以降の記事はスライドの補足が主になりますので、スライドを一読したあとに読み進めていただくとより実りがあると思います。

HTTP通信を見直す

通信の中身を見る

まず、通信を見ることができるようにします。以前は mitmproxyCharlesなどのプロキシツールを使っていましたが、今は Stethoを使っています。 USBに繋ぐだけで簡単にネットワークを見ることができるようになるのでAndroidに詳しくない人でも「挙動がおかしいのですが」「リクエストとレスポンス送ってもらえますか?」というやりとりをすることができるようになりました。

Gzipを有効にする

サーバー側でGzipが有効になっているからといってクライアント側で有効になっているとは限りません。Stethoを使ってGzipが有効になっていることを確認しましょう。 もし対応していなければ自分でリクエストヘッダに Accept-Encoding: gzipを付けて、レスポンスヘッダに Content-Encoding: gzipが含まれていたときにデコードする処理を書く必要があります。

OkHttpはデフォルトでGzipが有効になっています。HttpUrlConnectionや、その他のHTTPクライアントを使っている場合は実装を確認してください。 私は以下の理由からAPIや画像のリクエストにOkHttpを使うことをおすすめしています。

  • HTTP/2.0やWebSocketに対応している
  • コネクションをプーリングしたり、バッファをセグメントのリストとして実装して、拡張するときにはセグメントプールからメモリを再取得するなど、パフォーマンスに配慮した設計になっている
  • A Few 'Ok' Libraries (Droidcon MTL 2015) // Speaker Deck
  • Interceptorという仕組みによって柔軟なネットワーク処理を行うことができる
  • たとえば、NetworkInterceptorを使って手元でのレスポンスタイムを計測するモジュールを簡単に作ることができる

Interceptors · square/okhttp Wiki

Cache-Controlを有効にする

OkHttpはCache-Controlをサポートしています。OkHttpClientにCacheクラスのインスタンスをセットするだけで有効にすることができます。

// アプリ側
OkHttpClient client = new OkHttpClient();
Cache cache = new Cache(cacheDir, MAX_CACHE_SIZE);
client.setCache(cache);
# サーバー側
expires_in(1.hour, public: true)
# => Cache-Control: max-age=3600, public

2回目以降の表示を早くすることができるのもそうですが、ローカルにキャッシュを置くことでオフラインでもコンテンツを見れるようになるのが大きなメリットです。

有効にするのは簡単ですが、端末のキャッシュはサーバー側から消すことができないので、適切なポリシーを設定する必要があります。

Reactive Data Store

このアプリはRxJavaをベースに作られています。基本的な方針は以下の記事にある通りです。

RxJavaはデータの取得からスケジューリング、パイプライン処理、ビューへのバインディングまで、Androidフレームワークを補うように使っています。

画像のリクエストを見直す

画像はJSONのレスポンスに比べるとサイズが大きいので、ちょっとした変更で通信量を大きく減らすことができる可能性があります。そのため、アプリの通信量の大半を占める画像の読み込みライブラリは重要な役割を担っています。

パフォーマンスを左右する要素

画像読み込みライブラリでは以下の要素がパフォーマンスを左右します。

  • 内部で使用されるHTTPクライアントの種類と設定
  • キャッシュの管理
  • Executorの設定
  • リクエストのスケジューリング
  • Bitmapの管理

なのでこれらを中心に、どのように画像が読み込まれるかというのを見ていきました。何を使うにしても、キャッシュが有効になっているかと、適切な優先順位が設定しているかは見ると良いと思います。

Facebookが F8Frescoという新しい画像読み込みライブラリを発表しました。I/Oバウンドの処理とCPUバウンドの処理でExecutorを使い分けたり、WebPに対応していない端末にはJPEGにデコードするようにしたりするなど、PicassoやGlideや他の画像読み込みライブラリを研究して作られているので、これから検証しようと思っています。

画像のフォーマットとサイズ

FacebookのiOSアプリはProgressive JPEGを採用している(Faster Photos in Facebook for iOS | Engineering Blog | Facebook Code)ようですが、AndroidはProgressive JPEGには対応していませんでした。しかし、FrescoでProgressive JPEGに対応するようにしたようなので、Facebookは両方プラットフォームともProgressive JPEGに移行するのかもしれません。 私たちはWebPで画像を配信しています。

また、スマートフォンのディスプレイサイズは様々で、安価で小さい端末と、Nexus 10のような大きな端末では表示に必要な画像サイズが違います。 そこで、私たちは動的画像変換サーバーをS3の前に置いて、実際の画面サイズに合わせて変換するようにリクエストをしています。 それに加えて、回線の品質に応じて係数を掛けて画像を縮小するということをしています。

回線の品質を判定するのに、私たちのアプリでは通信の規格を見ています。日本では4GよりWiFiの方が速いのですが、海外では4Gの方が速かったりすることがあります。そのため規格を見るのは厳密な方法ではありませんが、お手軽な方法ではあります。

画像はURLでキャッシュされるので、回線の品質の判定が変わったときにキャッシュにヒットしなくなるということを心に留めて置く必要があります。品質の判定をするときにはスパイクに対応する必要がありますし、そうでなくてもオフライン時に画像が取得できるように、前回の回線の品質判定を保存しています。

係数をいくつに設定するかは、画像がそのサービスにおいてどれほど重要かに依存しています。クックパッドでは過去の調査から、レシピ画像はレシピを決めるときの重要な情報であるということが分かっていたので、画像が荒いと感じられない程度の値に設定しています。

API設計を見直す

レスポンスにBase64でエンコードされた10px四方のサムネイルを含めるようにしています。それによってレスポンスを受け取ったら直ちに画像のようなものを表示することができるようになります。

レスポンスを受け取った直後の状態の比較

ネットワークが不安定だったり遅いところでは、APIをリクエストする回数を減らすのが重要になります。 このアプリでは、レシピを探すという行動は重要な体験になってきます。ユーザーはレシピ間を行ったり来たりするので、そこのストレスをなるべく少なくするために、レシピ検索のAPIでレシピ詳細画面で必要なデータを返すようにして、ユーザーの行動を遮らないようにしています。 この最適化は部分レスポンスによる最適化とのトレードオフですか?という質問がありましたが、必要な情報は取得するようにして、不必要な情報は削るようにするということなので、トレードオフということではありません。

これまで説明してきた最適化は、UXを最大化することを目的で行っています。 どのようにデータを表示するか、どのデータが必要でどのデータが必要でないかは、実際のユースケースに依存するので、ユーザーに一番近いところで開発しているモバイルエンジニアにとって、APIやネットワークのスキルがこれからますます重要になってくるという話で締めました。

海外のカンファレンスに参加する

私のチームは世界各地に分散しており、朝会、チャット、GitHubはどれも英語で行われていますが、私はチームの中でも私が一番英語が苦手だったので、英語の勉強をしにいくつもりで応募しました。

Droidcon Italyについて

参加者数は、去年が400人くらいだったのに対して、今年は800人以上に増えたそうなので、開発者の注目が集まっているということが分かります。 セッションの内容を振り返ると、DIやMV*やリアクティブなどのアーキテクチャの話、EspressoやRobotiumやRobolectricなどのテストの話、プロトタイピングやマテリアルデザインやブランディングなどのデザインの話など、界隈のホットな話を聞くことができました。 イタリアでも「iOSと同じデザインで作ってくれと言われて困っている」と言っていて、日本とあまり変わらないんだなと思って親近感が湧きました。

Keynoteの様子

カンファレンスで発表するためにしたこと

Abstractをちゃんと書く

勢いで書いたら通ったー!という訳ではなく、海外で話すのは今回が初めてだったので慎重に調べて応募しました。

英語論文の書き方やテクニカルライティングの本を読んで勉強して、英語が得意な人に何回か添削してもらいながら、一ヶ月ほど掛けてAbstractを書きました。

後で知ったことですが、トークを採択するときは、その人のTwitterやブログやGitHubや他のカンファレンスの登壇の経験の有無から判断するらしいです。 私はTwitterやブログは日本語でやっているので不利であるということと、採択されたあとも参加者はAbstractを読んでセッションを決めるので重要(なんとか採択されてもAbstractが微妙だと部屋が埋まらなくて寂しい思いをしたり)なので、Abstractは時間を掛けて作る価値があったと思いました。

発表の練習をする

まず、Abstractをもとに原稿を書き起こしました。そのあとにスライドを作り始めると同時に英語の発音の練習も始めましたが、すぐに発音がうまくなるとは思わなかったので、なるべくスライドに文字を入れつつも、文字で埋めすぎないような調整をしました。 喋り以外の方法を使ってでも伝えなければと思って、たとえばキューに入れられたタスクがライフサイクルの変化とともに、どのようにスケジューリングされるのか、の説明でアニメーションを使ったりしています。 そして、スライドが出来たら頭に入るまで繰り返し声に出して練習をしました。

当日は部屋に入りきらないくらいに人が来てくれて、終わったあとに「素晴らしいトークをありがとう」と言ってもらえたので良かったです。

おわりに

細かいパフォーマンスのtipsを共有しました。パフォーマンスチューニングに没頭すると局所最適にハマってしまいがちなのですが、実際にはユーザーが使うときにどう見えるかと考えながら全体の設計をしていくという話をしました。

弊社には海外のカンファレンスでの発表の経験者が多くいる中で、経験の浅い私が知見と称してブログを書くのは気が引けましたが、最初のハードルを越えるのは大変だと思うので、そういう人のためになればと思います。

ちなみに日本では今週末の4/25に DroidKaigiですね。運営に携わっている人からは日本でもDroidconのようなコミュニティを作っていきたいという話を聞いておりますので、参加して一緒に盛り上げていきましょう。

既存のObjective-CアプリケーションをSwiftで書き換えた話

$
0
0

 海外事業向けのiOSアプリケーション開発を担当している西山(@yuseinishiyama)です。クックパッドは現在、海外複数カ国に向けてサービスを展開しています。

 主にObjective-Cで記述されたアプリケーションを全面的にSwiftに書き換える機会があったので、その際に得た知見や書き換えるに至った動機を共有します。

書き換えに至るまでの経緯

 この項では、書き換えに至るまでの経緯について説明します。

Objective-C期

 アプリケーションの開発は2014年7月頃にスタートしました。Swiftの発表直後でしたが、時期尚早ということもあり、Objective-Cで実装することになりました。

Objective-C、Swift混在期

 2014年10月頃から、Swiftへの段階的な移行のために、新規のコードをSwiftで書くようになりました。Swiftの記述力や、ヘッダと実装を行き来しなくて良いことなどにメリットを感じたためです。ここから、Objective-CとSwiftが混在するようになります。

Swift期

 Objective-CとSwiftが混在している状態では、後述するSwiftによるメリットを完全には受けることができないと考え、一部を除いてほぼ全てのコードをSwiftに書き換えました。

なぜ書き換えたか

 書き換えには、エンバグやスケジュールの遅れなどのリスクが伴ないます。また、新規の言語に対する純粋な興味から、業務で使用する言語を選択することも当然すべきではありません。しかし、それでも尚、書き換えたほうが良いと思われる十分な理由がありました。この項では、その理由について説明します。

Objective-CとSwiftの混在によって生じる制約

 AppleはSwiftとObjective-Cを相互に運用する方法について、詳細なドキュメントを提供しています。しかし、単に相互利用できるというだけで、実際には様々な制約があります。

 Objective-CからSwiftを参照する場合、Swift側の一部のコードは参照することができません。Swiftでのenumstructのような概念はObjective-Cには無いため、これらをObjective-Cから利用することはできません。また、Optional型の利用にも制限があります。例えば、下記のようなクラスとそのプロパティ群があった場合、プロパティaはObjective-Cから参照できますが、プロパティbはできません。

class SampleClass : NSObject {
    var a =0var b: Int?

 SwiftのInt型はObjective-CではNSIntegerとみなされます。SwiftのIntstructとして宣言されている一方、NSIntegerの実態はプリミティブな型なのでnil値をとることができません。そのため、OptionalなInt型をObjective-Cで扱うことができないのです。このようなケースでnilを許容する数値を使用したい場合は、NSNumberとして宣言せざるを得ません。

 以上のような事柄を含め、Objective-Cから利用される限り、Swiftの仕様をフルに生かすことはできず、また、Objective-Cからの利用を意識したコードを書き続ける必要があるということが分かります。

Objective-CからSwiftを参照しないようにする?

 前項の問題はObjective-CからSwiftを参照するが故に、起こりうる問題です。極力、Objective-CからSwiftを呼び出さないようなポリシーで混在させるのはどうでしょうか?

 確かに、SwiftからObjective-Cライブラリを参照する、SwiftからObjective-Cで記述されたモデルを参照する、などのSwiftからObjective-Cを参照するケースでは、これらの問題を気にする必要はありません。実際、当該プロジェクトでも、当初はビュー関連のコードだけSwiftで記述していたので、Objective-CからSwiftを参照するケースは殆どありませんでした。

Swiftのメリットを享受するには

 ところで、Swift化によるメリットを最大限に受けることができるのは、モデルやAPIクライアントです。これらのレイヤーは、Objective-Cにおいては、その動的特性のために、バグの温床となっていました。JSONをオブジェクトにマッピングする際に、予期していた型と違う型が入っていたというようなことは、皆さんも度々経験されているのではないでしょうか。

 一方、Swiftでは静的型付けやnullabilityのコントロールによって、これらのバグを解消することができます。長期的に運用されるであろうアプリケーションにおいて、こうした機能を活用してその安定性を高めることには多大なメリットがあります。

 しかし、モデルやAPIクライアントをSwiftに置き換えると、それらを呼び出していた既存のObjective-Cコード(主にViewControllerなど)がSwiftを参照することになります。モデルやAPIクライアントをSwiftの機能をフルに利用して実装すると、結局、アプリケーション全体をSwift化する必要がでてくるのです。

Swift化のメリット

 前項で、静的型付けやnullabilityのコントロールといった、Swiftのメリットについて簡単に触れました。ここでは、そうしたメリットについて、実際のコードを参照しながら、具体的に述べます。

ジェネリクスの活用

 ジェネリクスを活用することで、より安全で表現力の高いコードを記述することができます。ジェネリクスを利用することが好ましい典型的なケースについて説明します。

モデルとAPIクライアントの表現

 ジェネリクスを用いて、レスポンスの型を静的に決めることができます。これによって、前述した、予期しない型が代入されることによって生じるバグを防ぐことができます。

 まず、単一のAPIを表すためのプロトコルを以下のように宣言しました。ちなみに、現在のプロトコルの仕様ではデフォルトの実装を定義することができないので、プロトコルではなくクラスを採用するという考えも十分に有り得ます。

protocol API {
    typealias ResponseType
    typealias ResultType = Result<Response<ResponseType>, Response<CommonError>>typealias ResponseParserType: ResponseParservar method: Method { get }
    var pathString: String { get }
    var parameters: [String : AnyObject]? { get }
    var parameterEncoding: ParameterEncoding { get }
    var responseParser: ResponseParserType { get }
}

 そして、準拠しているAPIの実態は以下のようになります。

extension APIs {
    class Recipes {
        class Get: API {
            typealias ResponseType = Recipe

            let id: Intinit(id: Int) { self.id = id }
            var method: Method=.GET
            var pathString: String { return"/recipes/\(id)" }
            var parameters: [String : AnyObject]? =nilvar parameterEncoding: ParameterEncoding=.Default
            var responseParser = DefaultResponseParser<ResponseType>()
        }
        class Post : API {
            typealias ResponseType = Recipe

            let recipe: Recipeinit(recipe: Recipe) { self.recipe = recipe }
            (以下省略)

 このように記述することで、それぞれのAPIの仕様が一眼で分かります。また、クラスのネストを利用して、APIの階層構造も表現することができます。

 次に、APIクライアントのインターフェースです。

protocol APIClient {
    (省略)
    func sendRequest<T: API where T.ResultType == T.ResponseParserType.ResponseType>(API: T, handler: T.ResultType -> ())
}

 ジェネリクスを活用することで、T.ResultTypeとしてレスポンスの型がAPIから一意に決まります。実際にこれらを使用するコードは下記のようになります。

let api = APIs.Recipes.Get(id: 42)
SharedAPIClient.sendRequest(api) {
    println("the title is \($0.value?.bodyObject.title)")
}

 GET /recipes/:idのレスポンスの型がRecipeであるということが静的に決まります。そして、そのプロパティにtitleがあることもコンパイラは知ることができるのです。

 ちなみに、これらの実装に当たっては、MoyaAPIKitが大変に参考になりました。

Eitherによるエラーハンドリング

 Objective-Cにおいて、APIコール時のコールバック関数の型は下記のようなものが一般的でした。

typedefvoid (^CompletionBlock)(id result, NSError *error);

 errornilかどうかをチェックし、nilでなければresultを参照して結果を受け取るというパターンです。しかし、resulterrornilになり得る訳で、厳密には下記4パターンが存在します。

result == nilresult != nil
error == nil?成功
error != nil失敗?

 この場合、「?」にあたる箇所では、果たしてそのリクエストが成功したのか、失敗したのか分かりません。もちろん、そうならないように実装するわけですが、厳密に起こり得ないことを強制する術はありません。

 一方でジェネリクスを使用して、Eitherと呼ばれる2つの可能性を表現する型を実装すれば、より明確に成功と失敗のコンテキストを表現することができます。Eitherの実装に関しては、こちらの実装が参考になります。

publicenum Result<T,E> {
  case Success(Box<T>)
  case Failure(Box<E>)

(BoxはSwiftの値型の制限を回避するために存在しています。詳細については、Boxを参照してください。)

 これを利用すると、エラーハンドリングは下記のように記述することができます。

let api = APIs.Recipes.Get(id: 42)
SharedAPIClient.sendRequest(api) {
    switch $0 {
    case.Success(let box):
        // box.unbox <- 成功時の型Tが入っているcase.Failure(let box):
        // box.unbox <- 失敗時の型Eが入っている// ここでエラーをハンドリングする
    }
}

 このようにジェネリクスを活用することで、より厳密なエラーハンドリングを行うことができます。

Enumの活用

 SwiftのEnumには計算型プロパティや関数を定義することができます。そのため、Enumの値によって振る舞いを変える場合、その振る舞い自体をEnum側に実装することができます。

enum MyTableViewSection: Int {
    case A =0, B, C, D

    var heightForCell: CGFloat {
        switchself {
        case A:
            return30case B:
            return44case C:
            return80case D:
            return44
        }
    }
}

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return MyTableViewSection(rawValue: indexPath.section)!.heightForCell
}

 Objective-Cではswitch文の中が煩雑になりがちでしたが、SwiftのEnumを活用することで、より可読性の高いコードを書くことができます。

Swiftは実用段階か

 この項では、新規のプロジェクトをSwiftで始めるべきか、また、既存のプロジェクトをSwift化するべきなのか、ということについての私見を述べます。

Swift1.2の登場

 先日、Xcode6.3が正式にリリースされ、このアップデートにはSwift1.2も含まれています。正直なところ、Swift1.2がでるまでは、IDEの安定性、コンパイル時間などに大変な不満があり、Swiftへと移行したことを後悔することもありました。コードを記述すればするほど、ジェネリクスを多用すればするほど、それらの問題が顕著になってきます。しかし、Swift1.2から、これらの問題は大きく解消されています。これらを踏まえると、Swift1.2からはまともに使えるようになった、と言ってもよいでしょう。

Swiftのエコシステム

 Swiftのエコシステムには、既に素晴らしいライブラリが存在します。例えば、awesome-swiftのリストが参考になります。

 当該プロジェクトでも、すでに下記のSwift製ライブラリを使用しています。

 このようにSwiftには既に実用的なライブラリが多く存在し、効率よく開発を進めることができます。

 ちなみに、SwiftではStatic Libaryを生成することができず、また、Dynamic FrameworkはiOS8以降でのみのサポートとなっています。当該プロジェクトではiOS7もサポートする必要があったので、Swift製のライブラリはGitのサブモジュールとして管理し、プロジェクトファイル内にソースを直接追加してコンパイルしています。

Swiftに移行するべきか

 Swiftに移行することで、以前より、表現力が高く、堅牢なコードが書けるようになりました。アプリケーションが堅牢であることは、他のプラットフォームに比べてリリースサイクルが長くなってしまいがちなiOSアプリケーションにおいて、非常に重要です。

 実行時のクラッシュは!を使用して、強制的にアンラップした箇所や、unownedでキャプチャした箇所などに限局されていき、それ以外のクラッシュを招くようなコードは実行前に検出し易くなりました。

 このようなメリットを踏まえると、「新規のプロジェクト」や「まだ規模が小さく、長期的に運用されそうなプロジェクト」に関してはSwiftを積極的に選択するべきです。

 一方で、「大規模なプロジェクト」をSwift化するのは難しいと感じました。段階的移行といっても、前述のように、モデルやAPIクライアントのレイヤーを書き換えるとなると非現実的な作業量に成り兼ねません。また、新規のコードだけがSwift、というのも悪いとまではいきませんが、Swiftの利点がフルに活用できない点、言語切り替えのスイッチングコストが発生する点などを考慮すると、それほどメリットが無いのではないでしょうか。

おわりに

 Swiftはまだまだ発展途上な言語ではあるものの、既に十分な機能を備え、また、そのエコシステムも充実してきています。Swiftの機能を十分に活用すれば、Objective-Cに比べて、保守性が高く、安全なアプリケーションを実装することができるでしょう。この記事が、皆様の新規プロジェクトへのSwiftの採用、またObjective-CからSwiftへの移行の後押しに少しでもなれば幸いです。

クックパッドにおけるサーバ監視と運用の工夫

$
0
0

こんにちは。インフラストラクチャー部の加藤(@EugeneK)です。 今回はWebサービスを運用する上で欠かせない、モニタリングをクックパッドでどうしているかという話をします。

死活監視と性能監視

Webサービスを運用している以上、そのサービスを稼働しているサーバがあり、サーバには故障やトラブルが発生します。 また、どれくらいのパフォーマンスが出ているか、リソースをどのくらい消費しているかなどのトレンドを把握することは、成長するサービスを支えていく上で欠かせません。

故障やトラブルにいち早く気づくための仕組みを死活監視と言います。

また、サーバリソースの時系列での推移を知るために、グラフとしてトレンドを可視化する仕組みを性能監視と言います。

ポーリング監視の限界とZabbixのアクティブ監視

クックパッドでは死活監視にNagios、性能監視にMuninを使用してきましたが、サーバ台数の増加に伴い限界が見え始めました。 具体的には、監視を行うサーバが監視対象のサーバへ定期的に通信をして死活や性能の監視を行っていましたが、監視対象のサーバ台数が500台を越えるあたりから監視が追い付かず、指定した間隔で行われないという問題が発生しました。

f:id:EugeneKato:20150427120737p:plain

その問題を解決するためにZabbixを導入しました。Zabbixとは統合監視ツールであり、監視とグラフ作成が両方行えるツールですので、NagiosとMuninを両方使う必要がなくなります。

また、Zabbixの特徴としてアクティブ監視という機能があります。アクティブ監視とは監視対象のサーバにインストールされたエージェントが各種リソースや性能の状況を取得し、メトリックスとして監視サーバに報告する機能です。

f:id:EugeneKato:20150427120731p:plain

この機能を用いると、監視サーバのポーリングがボトルネックとなって台数の増加に対応できないという問題が解決し、スケーラビリティを確保することができます。 実際にクックパッドでは1秒間に3000項目を越える監視を実現しています。 もちろん、Pingによる監視とエージェント自体が稼働しているかどうかの監視は引き続き監視サーバ側から行う必要がありますが、それ以外の監視は全てアクティブ監視で行うことが出来ます。

自動登録

クックパッドではAWS上でサーバを運用しているため、オートスケールを始めとしてサーバの増減が日常的に発生しています。 そこで、監視の仕組みもサーバの増減に追従する必要があります。

Zabbixには自動登録の仕組みがあり、エージェントが起動したときに監視サーバと通信し、監視サーバに未登録であれば新規登録して監視に追加する機能があります。 この仕組みを使ってサーバの増加に対応しています。

一方で、減少に対しては独自の仕組みを使っています。 オートスケールで減少した分については、AWSのAPIで取得したサーバリストとZabbixのAPIで取得したサーバリストを突合させて、減少分を削除するスクリプトを定期的に実行しています。 また、オートスケール以外で減少した分についてはサーバ停止後もグラフを振り返りたい場合があるため、手動で削除するようにしています。

chatopsによる作業負担の軽減

Zabbixでの監視の流れは以下の通りとなっています。

  • エージェントによるメトリクスの取得と監視サーバへの報告
  • 監視サーバ側で報告された値をもとに、トリガーとして設定された閾値と比較
  • トリガーに紐付けられたアクションによってメールによるアラートの通知など任意のタスクを実行

クックパッドでは監視で異常が発生した場合はメールによる通知と同時に、業務で使用しているチャットにも通知するようにしています。 f:id:EugeneKato:20150427163735p:plain

これは、そのアラートに対して誰が対応しているかなどの状況を素早く共有するのがねらいです。 また、手動によるオペレーションを実施する際に一時的にアラートを止めたい場合も、チャットルームに常駐しているロボットに向けて発言することで、ZabbixのAPIを通じて自動的にメンテナンス設定を行うようにしています。 f:id:EugeneKato:20150427164352p:plain

こういった「chatops」の仕組みを用いることで、最小限の手順で情報の共有や操作が行えるようになります。

多リージョン展開とプロキシ監視

クックパッドは今世界展開をしている最中であり、AWSでの多リージョン展開をしています。 リージョン毎に監視システムが存在してしまうと管理・運用の手間が増えてしまうため、Zabbixのプロキシ監視の仕組みで一元化するようにしています。 f:id:EugeneKato:20150427120743p:plain

図にあるとおり、zabbix-serverとzabbix-proxyはVPNによって接続し、各リージョンにあるzabbix-proxyはそのリージョン内の監視対象のサーバから取得したメトリクスを集約してzabbix-serverに報告するようになっています。 これによって、VPNを経由する通信はzabbixプロトコルが使用するポートのみを許可すればよいことになり、ネットワークの設定も最小限で済ませることができます。

おわりに

クックパッドでの監視の仕組みについて紹介しました。 冒頭でも触れましたように、サーバのモニタリングはサービスの品質を向上させる上で欠かせません。 モニタリングの向上はサービスの向上ととらえ、日々改善を行っています。

ブラウザ拡張を用いた業務改善手法

$
0
0

買物情報事業部の根岸(@negipo)です。今回はブラウザ拡張を日常業務でどう使っているかについて紹介します。

ブラウザ拡張とは

ブラウザ拡張は、ブラウザによるウェブとのインターフェースをJavaScriptやCSSを用いて自分好みにカスタマイズする機能です。Google Chromeを利用していればChromeウェブストアなどで公開されている拡張をインストールできるでしょう。一方で、開発したブラウザ拡張を自分で使うために、Chromeウェブストアによる公開と言うプロセスを踏むのは面倒です。日常的にウェブのインターフェースを改変する道具としてブラウザ拡張を使うためにはいくつかの手法がありますが、僕はGithubのdefunktさんが作ったdotjsを使っています。詳細は省きますが、今開いているページでalertを出すぐらいの機能であれば10秒で開発作業を終えることができると思います。

また、dotjsで作成したスクリプトを配布して拡張をチームの人などに使ってもらうのはやや敷居が高いため、そういったことをやりたいのであれば素のChrome拡張として実装したり、あるいは全く別種のオートメーションを導入したりするといったことが必要でしょう。

ブラウザ拡張を用いた業務改善手法

変更しづらいアプリケーションの挙動を変更する

さて、日常業務で扱っているアプリケーションに問題があって書き換えたいのであれば、本来はそのアプリケーションを改修をするのが正しい道です。実際にクックパッドで内製されている社内情報共有システムなどは常に改善が進んでいます。一方で世の中にはGitHub Enterpriseや勤怠管理システムなどの容易に改変できないアプリケーションが存在します。そこを好き勝手に便利にできたら便利そうですね。それでは具体的に何をどう変更するのか見てみましょう。

コミュニケーションを自由に改変する

ブラウザ拡張によるインターフェースの改変といえば、一般的にはウェブにある情報を自分のブラウザで表示するときに何らかの改変を入れて便利にすることができるだけに見えますが、実際にはフォームのテキストフィールドの挙動を変更したりするなどして自分からウェブにアウトプットする際にも変更することができます。これを利用してウェブのさらに先にいるチームメンバーとのコミュニケーションを改変することができます。

僕の ~/.js/githubenterprise.cookpad.com.jsを今見たら下記のような機能が含まれていました。

  • 1) outdated diffを自動的に開く
  • 2) console.log, debugger, XXXなどの注意すべきdiffが含まれていたらチカチカする
  • 3) Pull RequestやIssueの新規作成時に空のフィールドに下記のようなテンプレートを流し込む
  • タイトルにレビューの締切として2時間後の時刻を自動挿入
  • チームメンバーから2人選んでメンション
  • ユーザーストーリーやKPI、簡易チェックリストなど、デスクリプションに必要な要素のテンプレート

(1)のoutdated diffを開く開かないというような設定はまさにパーソナルな好みの問題であり、シンプルにブラウザ拡張の便利さが現れていると思います。

(2)は自分の実装他人の実装問わずやばそうな実装が検出できるので便利です。こういった実装を検出するオートメーションの方法はいろいろあると思いますがレビューのインターフェース上でチカチカしているという点が好みです。

(3)は自分のアウトプットを他人に届ける際に、その内容をわかりやすく伝えるために便利な部分です。特に締切の明示とかはサツバツとすることがあるので賛否両論だと思いますが、僕が所属しているチームでは緊急性がわかりやすくなるので付加しているメンバーが多いです。緊急性が低ければ適当に明日の夕方とかを締め切りにしています。

確信がないインターフェース改修のプロトタイピングに用いる

まったく別の軸の話として、ある日僕はレスポンスタイムの状況が音で聞こえたら便利かもしれないなと思ったのですが、唐突に社内のレスポンスタイムのモニタリングシステムから音がし始めたらきっとモニタリングどころではなくなってしまうでしょう。事前にブラウザ拡張でプロトタイピングすることで、チームに影響を与えることなく確信のない改修がもたらす体験を改善することができます。

今現在クックパッドで使っているモニタリングを行っているウェブアプリからは上記のような音がしていて、何か問題があると高音のサイン波を発してくれるため、サービスの状況がわかりとても便利なのですが、この機能が実際に取り込まれるまでには下記のプロセスを踏みました。

  • Web Audio API + dotjsで簡単に実装する
  • 長期間使ってこれはいいのではという気持ちになったので社内情報共有システムにスクリプトを公開する
  • 思ったより反応がよくて受け入れられそうだったのでモニタリングシステムにPull Requestする
  • 取り込まれてみんなが使いはじめる

ほとんどのアプリケーションで共通していることだと思いますが、体験の改善は現実のデータに基づいて現実のユースケース下で行うことが望ましいと思います。ブラウザ拡張を用いて、効果の最大化をリリース前に検証できることには価値があると考えています。

おわりに

世界とのつながりかたを変える簡単な方法として、ブラウザ拡張は便利な道具です。道具を磨いて楽しく暮らしたいですね。


A/B テストで施策の効果を検証!エンジニアのための R 入門

$
0
0

こんにちは、買物情報事業部でサーバサイドの開発を担当している荒引 (@a_bicky) です。

今回のエントリでは R で A/B テストの結果検証を行う方法の一例について紹介します。 エンジニアでも自分の関わった施策の効果検証のために簡単な分析をすることがあるかと思いますが、そんな時にこのエントリが役立てば幸いです。

なお、次のような方は対象外です。

  • A/B テストや KPI の設計に興味のある方
    • この辺には全く触れません
  • プログラミング初心者
    • わからない単語が大量に出てくるでしょう
  • R で統計学や機械学習の手法をバリバリ使いたい方
    • 世の中の "分析"の多くは集計処理がメインです
  • Python, Julia など既に分析する上で使い慣れた言語・ツールがある方
    • 今回のエントリ程度の内容であればわざわざ乗り換える必要もないでしょう

OS は Mac を前提として説明するので、Windows や Linux では一部動かないものもあるかもしれませんがご了承ください。 また、R のバージョンは現時点で最新バージョンの 3.2.0 であることを前提とします。

何故 R か?

それは私の一番使える言語だからです!というのが一番の理由ですが、他にも次のような理由が挙げられます。

  • 無料で使える
  • R 関連の書籍なども大量に存在していて情報が豊富
  • RStudio や ESS のような素晴らしい IDE が存在する
  • パッケージ(Ruby でいう gem)が豊富
  • ggplot2 パッケージを使うことで複雑なグラフが手軽に描ける
  • data.table, dplyr, stringi パッケージなどのおかげで数百万オーダーのデータでもストレスなく高速に処理できるようになった

ちなみに、R のコーディングスタイルのカオスっぷりに辟易する方もいるでしょうが、そこは耐えるしかないです。*1

アジェンダ

  • R の環境構築
    • 最低限の設定
    • IDE の導入
  • R の使い方についてさらっと
    • コンソールの使い方
    • デバッグ方法
    • data.frame について
  • A/B テストの結果検証
    • コンバージョン率 (CVR) を出す
    • CVR の差の検定をする
    • ユーザの定着率(定着数)を出す
  • 番外編
    • R でコホート分析
  • 最後に

R の環境構築

Mac の場合は Homebrew 経由でインストールするのが手軽です。Linux の場合もパッケージマネージャ経由で手軽にインストールできます。

% brew tap homebrew/science
% brew install r

これでターミナルから R を起動できるようになったはずです。

% R

R version 3.2.0 (2015-04-16)--"Full of Ingredients"
Copyright (C)2015 The R Foundation for Statistical Computing
Platform: x86_64-apple-darwin14.3.0 (64-bit)

R は、自由なソフトウェアであり、「完全に無保証」です。
 一定の条件に従えば、自由にこれを再配布することができます。
 配布条件の詳細に関しては、'license()'あるいは 'licence()'と入力してください。

R は多くの貢献者による共同プロジェクトです。
 詳しくは 'contributors()'と入力してください。
 また、R や R のパッケージを出版物で引用する際の形式については
 'citation()'と入力してください。

 'demo()'と入力すればデモをみることができます。
 'help()'とすればオンラインヘルプが出ます。
 'help.start()'で HTML ブラウザによるヘルプがみられます。
 'q()'と入力すれば R を終了します。

>

R を使う際には基本的にこのコンソール上で作業することになります。コマンドラインからスクリプトを実行するのはバッチなどを運用するケースぐらいです。

R を終了するには q関数を実行するか Ctrl-D を入力します。

> q()
Save workspace image? [y/n/c]: n

"Save workspace image?"で y を入力すると現在のセッションの内容がワーキングディレクトリに .RData という名前で保存され、次回起動時に定義したオブジェクトなどが復元されます。

最低限の設定

個人的に次の 2 つの設定だけは欠かせないと思っています。

  • 言語の英語化
    • エラーメッセージでググった時に参考になる情報が多いので、英語アレルギーでない限りは英語化した方が良いでしょう
  • リポジトリの設定
    • パッケージをインストールする際にいちいち指定する必要がなくなります

次のコマンドを実行することで上記の設定が可能です。

% # 言語の英語化
% echo'LANGUAGE=en'>> ~/.Renviron
% # パッケージをインストールする際のリポジトリを設定
% echo'options(repos = "http://cran.md.tsukuba.ac.jp")'>> ~/.Rprofile

.Renviron は R を実行する上での環境変数を定義するファイルです。

.Rprofile は起動時の処理を記述するファイルです。options関数の repos引数に筑波大学のリポジトリを指定しています。 ただ、筑波大学は休日に停電を行うことがあるので、その際は別のリポジトリを指定しないとパッケージをインストールできません。

IDE の導入

前述したように、R を使う際には基本的にコンソール上で作業することになりますが、それなりの処理を行う場合は IDE を利用すると便利です。 IDE を利用することで、スクリプト上のコードをコンソールに送って挙動を確認するようなことも可能になります。 メジャーな IDE は次の 2 つです。

RStudio を使うには、ダウンロードリンクから自分の環境にあったファイルをダウンロードしてインストールするだけです。 Option+Shift+K(Windows は Alt+Shift+K らしい)を押せばショートカットキーが表示されるので、その内容を読むことでどのようなことができるかある程度把握できるでしょう。

ESS は MELPA からインストールできます。例えば init.el に次のような内容を記述して M-x package-list-packagesを実行して ess を選択します。melpa-stable だと今は ess-15.3 がインストールされます。

(when(require'packagenilt)(add-to-list 'package-archives'("melpa-stable" . "http://melpa-stable.milkbox.net/packages/")t)(package-initialize))

ESS の初期設定には思うところがあるので私はこのようにカスタマイズしています。

R の使い方についてさらっと

コンソールの使い方

まず、コンソールの使い方についてさらっとご紹介します。再度 R を起動してください。以降の説明は全てコンソール上で行うことを前提とします。

% R -q  # -q オプションを指定するとスタートメッセージを表示しない>

コンソール上では文字を入力した状態で TAB を入力すると補完候補が表示されます。

> read
read.csv          read.delim        read.fortran      read.socket       readChar          readLines
read.csv2         read.delim2       read.ftable       read.table        readCitationFile  readRDS
read.dcf          read.DIF          read.fwf          readBin           readline          readRenviron

関数名の手前に ?を付けるか、help関数に関数名等を指定することでドキュメントを参照することができます。

># print 関数のドキュメントを参照する> ?print
> help("print")

この後 data.table::freadのような書き方や &演算子が出てきますが、これらのドキュメントを見ることもできます。

># 特殊なシンボルはバッククォートで括る> ?`::`
> ?`&`

また、コンソールにオブジェクト名を入力して評価するとそのオブジェクトの内容が表示されます。

> R.version
               _                           
platform       x86_64-apple-darwin13.4.0   
arch           x86_64                      
os             darwin13.4.0                
system         x86_64, darwin13.4.0        
status                                     
major          3                           
minor          2.0                         
year           2015                        
month          04                          
day            16                          
svn rev        68180                       
language       R                           
version.string R version 3.2.0 (2015-04-16)
nickname       Full of Ingredients         

関数オブジェクトを評価することで関数の実装内容を確認することもできます。

># print 関数の内容を表示> print
function(x, ...) 
UseMethod("print")<bytecode:0x7f9782441a38><environment: namespace:base>

これで、R の基本的な構文を説明しなくてもドキュメントと組み込み関数の実装を頼りにコードを読み解くことができますね!

デバッグ方法

GDB や Ruby の pry-byebug, pry-stack_explorerのように R にも便利なデバッガが存在します。 使い方の対応表は次のとおりです。

内容 R GDB pry-byebug/pry-stack_explorer
コールスタックを表示 where bt show-stack
ステップオーバー n n next
ステップイン s s step
ステップアウト f fin step
次のブレークポイントまで実行 c c continue
ヘルプを表示 help h help
処理を中断して終了 Q q quit
関数 f にブレークポイントを設定 debug(f) break f break ClassName#f
関数 f のブレークポイントを削除 undebug(f) clear f break --delete <breakpoint number&rt;
現在の位置にブレークポイントを設定 browser() binding.pry

実際に debug関数を使って関数にブレークポイントを設定してみます。 まず、エラーに直面したら traceback関数を実行することでバックトレースを確認できるので、その内容から原因となっている関数を特定します。

> f <-function(x){ y <- as.character(x); g(y)}> g <-function(x) sum(x)> f(1:5)
Error in sum(x): invalid 'type'(character) of argument
> traceback()2: g(y) at #11: f(1:5)

バックトレースの内容から g関数の中でエラーが起きていることがわかるので、debug(g)g関数にブレークポイントを設定してデバッグすると良いでしょう。

> debug(g)> f(1:5)
debugging in: g(y)
debug: sum(x)
Browse[2]>

R のデバッガでは、コールスタックを辿って親(呼び出し元)のフレームに移動するようなことはできませんが、parent.frame関数で親のフレームの環境を取得することができます。 evalq関数を使えば指定した環境でコードを評価することができるので、親のフレームの状態を確認することも可能です。

Browse[2]># 親フレーム(関数 f)のオブジェクト一覧
Browse[2]> evalq(ls(), parent.frame())[1]"x""y"
Browse[2]># 親フレーム(関数 f)の x の値
Browse[2]> evalq(x, parent.frame())[1]12345

これで不具合に直面してもデバッガを駆使して原因を特定できますね!

edit関数や trace関数を使うことで、関数の任意の場所にブレークポイントを設定することもできますが割愛します。

data.frame について

R の特徴的なデータ構造に data.frameがあります。行列の各列に異なるクラスのデータを保持できるデータ構造で、データベースのテーブルのようなデータ構造をイメージしていただくと良いと思います。 ログの分析では data.frameを使うことがほとんどでしょう。

># R のデータの例でよく使われる iris データ> head(iris)
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
15.13.51.40.2  setosa
24.93.01.40.2  setosa
34.73.21.30.2  setosa
44.63.11.50.2  setosa
55.03.61.40.2  setosa
65.43.91.70.4  setosa
># 各列のクラス> sapply(iris, class)
Sepal.Length  Sepal.Width Petal.Length  Petal.Width      Species 
   "numeric""numeric""numeric""numeric""factor"># 1 行目のデータを取得> iris[1,]
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
15.13.51.40.2  setosa
># Species の列を取得> head(iris$Species)[1] setosa setosa setosa setosa setosa setosa
Levels: setosa versicolor virginica

A/B テストの結果検証

さくっと R の導入から使い方まで説明したところで、擬似的なログを使って簡単な分析をしてみます。 次のような背景から分析することになったとします。

あるサービスのユーザ数を増やすために、チーム内でランディングページ (以降 LP) の改修を検討しています。既存の LP と新しい LP で 4/1 〜 4/7 の 1 週間 A/B テストを行ったので、新しい LP の効果を検証したいです。

準備

まず、今回の分析で使うパッケージをインストールします。各パッケージの概要については使う際に説明します。

> pkgs <- c("data.table","dplyr","tidyr","ggplot2")> install.packages(pkgs)># 各パッケージのバージョン> sapply(pkgs, packageVersion, simplify =FALSE)$data.table
[1]1.9.4’

$dplyr
[1]0.4.1’

$tidyr
[1]0.2.0’

$ggplot2
[1]1.0.1’

次に、今回使用する擬似的なログを作成します。ここに擬似データを作成する create_sample_data関数を定義したスクリプトを配置したので、読み込んだ後に関数を実行してください。 スクリプトの読み込みには source関数を使います。source関数は主にローカルのファイルのパスを指定しますが、このように URL を指定することもできます。

># R 3.2.0 でないと https には対応していないので注意>source("https://gist.githubusercontent.com/abicky/3a4789c3fd163a71606c/raw/5f1aeb86b8f0eb50caf386aa3fce9bc5354df9b5/create_sample_data.R")># 擬似データ(40 MB 程度)の作成> file_paths <- create_sample_data()
Creating logs of 'a' pattern .......... done
Creating logs of 'b' pattern .......... done
Writing files done

create_sample_data関数を実行することで一時ディレクトリに event_logs.tsv と access_logs.tsv が作成されます。返り値は list で、event_log_fileaccess_log_fileにそれぞれの絶対パスが格納されています。

それでは、eventlogs.tsv と access_logs.tsv を読み込みましょう。 小規模なデータの読み込みには read.table関数などを使うのが一般的ですが、read.table系の関数は非常に遅いので data.tableパッケージの fread関数を使います*2。data.table パッケージは、data.frameを高速化したような data.tableクラスを提供するパッケージですが、今回はデータの読み込みのためだけに使用します。なお、fread関数で読み込んだデータは data.tableクラスのオブジェクトになります。

># file_paths$event_log_file には event_logs.tsv の絶対パスが格納されている> event_logs <- data.table::fread(file_paths$event_log_file)> event_logs
              time   user_id event pattern
     1:1428373621201681931   imp       a
     2:1428299552898389685   imp       a
     3:1427862703944675268   imp       a
     4:1428109708660797792   imp       a
     5:1428236105629114044   imp       a
    ---259289:1427877582671321141    cv       b
259290:1428203926733054604    cv       b
259291:1427948288590425796    cv       b
259292:142814062529272541    cv       b
259293:1428052793466384837    cv       b
># file_paths$access_log_file には access_logs.tsv の絶対パスが格納されている> access_logs <- data.table::fread(file_paths$access_log_file)> access_logs
               time   user_id  page
      1:1427862710944675268 page1
      2:1427862739944675268 page3
      3:1427862750944675268 page1
      4:1428236117629114044 page2
      5:1428236120629114044 page1
     ---1251843:1430046543466384837 page1
1251844:1430220035466384837 page2
1251845:1430220043466384837 page1
1251846:1430220055466384837 page3
1251847:1430220071466384837 page2

event_logs の time はそのイベントが発生した unix time、event は "imp"か "cv"でそれぞれ LP の impression と conversion(ここではユーザ登録)を意味しています。pattern は a が新しい LP で b が既存の LP です。

access_logs の time はユーザがアクセスした unix time、page はアクセスしたページを意味しています。今回 page は使いませんがアクセスログっぽさを出すために苦労して付与してみました。

コンバージョン率 (CVR) を出す

CVR の算出など一連のデータ操作には dplyrパッケージを使います。dplyr パッケージは組み込みのデータ操作用の関数に比べて高速な上、SQL に近い感覚で処理を記述できるのでエンジニアにとっては馴染みやすいでしょう。*3

まず、読み込んだデータを tbl_df関数で dplyr 用の data.frame 表現に変換します。data.tableオブジェクトのままでも処理できますが、次の理由から変換することにします。

  1. data.tableオブジェクトは POSIXltオブジェクトを扱えないなどの制約がある
  2. 私の手元の環境では tbl_dfで変換した方が若干高速だった
  3. data.tableオブジェクトと data.frameオブジェクト(read.table等で読み込んだ場合のオブジェクト)で dplyr の処理結果が異なることがあるのでどちらかに統一しておきたい
># パッケージのロード(C++ の using namespace dplyr、Python の from dplyr import * に相当)>library(dplyr)

Attaching package:'dplyr'

The following object is masked from 'package:stats':

    filter

The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
> event_log_df <- tbl_df(event_logs)

さて、CVR を出すには imp と cv の UU を出す必要があります。SQL だと次のようなクエリですね。

SELECT
  pattern,
  event,
  COUNT(DISTINCT(user_id)) uu
FROM
  event_logs
GROUPBY
  pattern,
  event
;

上記の SQL では次のような手順を踏んでいます。

  1. pattern, event でグループ化する
  2. それぞれのグループに関してユニークな user_id をカウントする

これに対応する dplyr の処理は次のようになります。

> event_counts <- event_log_df %>%
+# 1. pattern, event でグループ化する+   group_by(pattern, event) %>%
+# 2. それぞれのグループに関してユニークな user_id をカウントする+   summarize(uu = n_distinct(user_id))> event_counts
Source: local data frame [4 x 3]
Groups: pattern

  pattern event     uu
1       a    cv  300972       a   imp 1003993       b    cv  289014       b   imp  99896

%>%は左辺のデータを右辺の関数の第一引数に流す演算子で、dplyr パッケージが提供している演算子です*4。dplyr による処理ではこの演算子を使った記法が好まれます。左側の処理の出力が右側の処理の入力になるので pipe (|) をイメージするとわかりやすいでしょう。

ちなみに、%>%を使わないと次のような書き方になります。

event_counts <- dplyr::summarize(
  dplyr::group_by(event_log_df, pattern, event),
  uu = n_distinct(user_id))

さて、現在次のような縦長のデータになっていて、cv / imp を出すのが少し面倒です。

pattern event uu
a cv 30097
a imp 100399
b cv 28901
b imp 99896

これを次のような横長のデータに変換できれば cv の列を imp の列で割るだけで CVR が算出できます。

pattern cv imp
a 30097 100399
b 28901 99896

横長のデータに変換するには tidyrパッケージの spread関数を使います。tidyr パッケージは縦長のデータを横長にしたり、列を分割したりデータの整形に便利な関数を提供しているパッケージです。 spread関数の第 1 引数には列に並べたいフィールドを指定します。今回の場合は imp, cv が列に並んでほしいので event を指定することになります。第 2 引数には、第 1 引数に指定したフィールドによって作成される列の各セルに表示する値となるフィールドを指定します。今回の場合は event ごとの uu の値を表示したいので uu を指定します。

> event_counts %>%
+   tidyr::spread(event, uu)
Source: local data frame [2 x 3]

  pattern    cv    imp
1       a 300971003992       b 2890199896

event ごとに列が割り当てられましたね。 あとは cv を imp で割るだけです。

> event_counts_with_cvr <- event_counts %>%
+   tidyr::spread(event, uu) %>%
+# cvr という列を追加+   dplyr::mutate(cvr = cv / imp)> event_counts_with_cvr
Source: local data frame [2 x 4]

  pattern    cv    imp       cvr
1       a 300971003990.29977392       b 28901998960.2893109

結果を見る限り、pattern a(新しい LP)の CVR が 30.0 % で pattern b の 28.9 % より高いですね!

CVR の差の検定をする

先ほどの結果より、新しい LP によって CVR が 28.9 % から 30.0 % に向上したことがわかりました。これは有意な差なのでしょうか?

比率の差が有意かどうかを確認するには prop.test関数を使用します。prop.test関数ではカイ二乗検定による母比率の差の検定を行います。第 1 引数には成功回数、第 2 引数には試行回数を指定します。今回のケースだと第 1 引数に cv の数、第 2 引数に imp の数を指定することになります。alternativeには片側検定をする場合に "greater"か "less"を指定します。指定しない場合は両側検定になります。

># event_counts_with_cvr の cv 列の内容を取得> cv <- event_counts_with_cvr$cv
># event_counts_with_cvr の imp 列の内容を取得> imp <- event_counts_with_cvr$imp
> prop.test(cv, imp, alternative ="greater")2-sample test for equality of proportions with continuity
    correction

data:  cv out of imp
X-squared =26.331, df =1, p-value =1.438e-07
alternative hypothesis: greater
95 percent confidence interval:0.0071026181.000000000
sample estimates:
   prop 1    prop 20.29977390.2893109

出力内容についてざっくり説明します。

"2-sample test for equality of proportions with continuity correction"は 2 郡の比率が等しいかどうかを検定したことを意味しています。その際、連続性の補正 (continuity correction) を行った旨が表示されていますが、連続性の補正の有無で経営判断が変わるほど結果に影響を及ぼすことはまずないと思われるので気にしなくて大丈夫です*5

"X-squared = 26.331, df = 1, p-value = 1.438e-07"はカイ二乗検定のカイ二乗値などを表しています。この中で重要なのが p-value です。この値は、pattern a の CVR と pattern b の CVR が等しい場合に今回のような結果が得られる確率を表しています。CVR が同じである確率と解釈しても A/B テストの検証においては差し支えないでしょう*6。CVR が同じである確率と捉えた場合、同じである確率は 1.44 × 10-7と極めて低いと言えます。

あとの出力内容は片側検定であることや、95 % 信頼区間の情報やそれぞれの CVR を表しています。

以上の結果より、今回の結果は有意な差と言えそうです。

ちなみに、p-value の目安として 0.05 を切ったら有意な差(5 % 有意水準)とすることが多いですが、経営判断する上では CVR 以外の様々な要因(e.g. メンテナンスコスト)もあるので、あくまで参考程度にするのが良いと思います。

ユーザの定着率(定着数)を出す

新しい LP によって CVR が上がったことを確認できましたが、ユーザの定着率が下がってしまっては意味がありません。 ということで、次は定着率を出してみます。 定着率の定義として、ここではユーザ登録したユーザが n 日後にアクセスした割合を採用します*7

少し複雑な処理になるので、SQL の書き方にも様々な選択肢がありますが、例えば次のようなクエリで各パターンの経過日数ごとの UU と定着率を算出できます(PostgreSQL)。

SELECT
  pattern,
  elapsed_days,
  uu,
  uu::float / first_value(uu) OVER (PARTITION BY pattern ORDERBY elapsed_days)  retention_ratio
FROM (
  SELECT
    pattern,
    elapsed_days,
    COUNT(DISTINCT a.user_id) uu
  FROM (
    SELECT
      user_id,
      to_timestamp(time)::date - to_timestamp(min(time) OVER (PARTITION BY user_id))::date elapsed_days
    FROM
      access_logs
  ) a
  INNER JOIN
    event_logs e
  ON
    a.user_id = e.user_id
  WHERE
    event = 'cv'AND elapsed_days < 14GROUPBY
    pattern,
    elapsed_days
) t
ORDERBY
  pattern,
  elapsed_days
;

上記の SQL では次のような手順を踏んでいます。

  1. access_logs を user_id でグループ化する
  2. グループごとに最小の unix time を算出する
  3. 2 で算出した時間と time を日に変換してから差分を算出して elapsed_days とする
  4. event_logs と user_id で inner join する
  5. event が 'cv'で elapsed_days が 14 より小さいレコードに限定する
  6. pattern, elapsed_days でグループ化する
  7. グループごとにユニークな user_id をカウントする
  8. pattern でグループ化する
  9. グループごとに elapsed_days が最小のレコードの uu の値で uu を割る
  10. pattern, elapsed_days でソートする

これに対応する dplyr の処理は次のようになります。

># dplyr 用の data.frame に変換> access_log_df <- dplyr::tbl_df(access_logs)># unix time を date に変換する関数> time_to_date <-function(time){+   as.Date(as.POSIXct(time, origin ="1970-01-01", tz ="Asia/Tokyo"))+}> uu_transitions <- access_log_df %>%
+# 1. access_logs を user_id でグループ化する+   group_by(user_id) %>%
+# 2. グループごとに最小の unix time を算出する+   mutate(min_time = min(time)) %>%
+# グループ化を解除(集約処理が終わったら ungroup するのが無難)+   ungroup() %>%
+# 3. 2 で算出した時間と time を日に変換してから差分を算出して elapsed_days とする+   mutate(elapsed_days = time_to_date(time)- time_to_date(min_time)) %>%
+# 4. event_logs と user_id で inner join する+   inner_join(event_log_df, by ="user_id") %>%
+# 5. event が 'cv'で elapsed_days が 14 より小さいレコードに限定する+   filter(event =="cv"& elapsed_days <14) %>%
+# 6. pattern, elapsed_days でグループ化する+   group_by(pattern, elapsed_days) %>%
+# 7. グループごとにユニークな user_id をカウントする+   summarize(uu = n_distinct(user_id)) %>%
+# 8. pattern でグループ化する+   group_by(pattern) %>%
+# 9. グループごとに elapsed_days が最小のレコードの uu の値で uu を割る+   mutate(retention_ratio = uu / first(uu, order_by ="elapsed_days")) %>%
+# グループ化を解除(集約処理が終わったら ungroup するのが無難)+   ungroup() %>%
+# 10. pattern, elapsed_days でソートする+   arrange(pattern, elapsed_days)

CVR を出した時の処理に比べると格段に複雑になりました。わかりにくそうな関数について少し補足説明します。

関数 概要
filter条件にマッチするデータのみ抽出する(SQL の WHERE 句に相当)
arrange指定したフィールドでソート(SQL の ORDER BY 句に相当)
mutate指定したフィールドを追加(SQL の SELECT *, new_field に相当)

group_by関数を適用した後に mutate関数を適用すると SQL の PARTION BY 相当の処理が行われます。

filter関数の引数に &演算子を使っていますが、これはベクトルの論理積を意味しています。ベクトルの要素ごとに AND 条件で評価し、その結果をベクトルで返す演算子です。なお、ベクトルの論理和には |演算子を使います。

># Ruby の (1..5).to_a に相当> x <-1:5># 2 より大きい要素は TRUE になる> x >2[1]FALSEFALSETRUETRUETRUE># 4 より小さい要素は TRUE になる> x <4[1]TRUETRUETRUEFALSEFALSE># ベクトルの論理積。2 より大きく 4 より小さい要素は TRUE になる> x >2& x <4[1]FALSEFALSETRUEFALSEFALSE

filter関数は指定したベクトルの要素が TRUE の場合に対応する行のデータを抽出します。上記の例では event の値が "cv"で elapsed_days が 14 より小さいデータを抽出しています。

ちなみに、上記の処理をもう少し最適化すると次のようになります。不要なデータを早い段階で削除しています。

uu_transitions <- access_log_df %>%
  group_by(user_id) %>%
  mutate(min_time = min(time)) %>%
  ungroup() %>%
  mutate(elapsed_days = time_to_date(time)- time_to_date(min_time)) %>%
  filter(elapsed_days <14) %>%
  select(user_id, elapsed_days) %>%
  distinct(user_id, elapsed_days) %>%
  inner_join(
    event_log_df %>%
      filter(event =="cv") %>%
      select(user_id, pattern),
    by ="user_id") %>%
  group_by(pattern, elapsed_days) %>%
  summarize(uu = n_distinct(user_id)) %>%
  group_by(pattern) %>%
  mutate(retention_ratio = uu / first(uu, order_by ="elapsed_days")) %>%
  ungroup() %>%
  arrange(pattern, elapsed_days)

さて、本題に戻って結果を見てみましょう。

># print 関数の n 引数に表示する行数を指定することで全データを表示> print(uu_transitions, n = nrow(uu_transitions))
Source: local data frame [28 x 4]

   pattern elapsed_days    uu retention_ratio
1        a       0 days 300971.00000002        a       1 days 112700.37445593        a       2 days  97470.32385294        a       3 days  97410.32365355        a       4 days  88300.29338476        a       5 days  81750.27162187        a       6 days  77160.25637118        a       7 days  72230.23999079        a       8 days  69460.230787110       a       9 days  66020.219357411       a      10 days  64370.213875112       a      11 days  60530.201116413       a      12 days  57520.191115414       a      13 days  55550.184569915       b       0 days 289011.000000016       b       1 days 127190.440088617       b       2 days 113790.393723418       b       3 days 111620.386215019       b       4 days 100540.347877220       b       5 days  93770.324452421       b       6 days  87660.303311322       b       7 days  82830.286599123       b       8 days  79440.274869424       b       9 days  75520.261305825       b      10 days  72140.249610726       b      11 days  69290.239749527       b      12 days  65410.226324328       b      13 days  62610.2166361

初日の uu は pattern a の方が大きいですが、全体的に pattern b の定着率は pattern a に比べて低く、その結果 14 日目 (elapsed_days が 13) の uu は pattern b の方が小さくなっています。

値の推移については数字を眺めるよりも可視化した方がわかりやすいでしょう。可視化には ggplot2パッケージを使うことにします。 ggplot2 は折れ線グラフ、面グラフ、ヒストグラム等を統一的なインタフェースで描画できるパッケージで、複数のグラフの重ね合わせや軸の分割など複雑なグラフも手軽に描画できることが特徴です。 plot関数等を使ったグラフの描画に慣れている方にとって ggplot2 は使いにくいかもしれませんが、R を初めて使うエンジニアの方には ggplot2 を使った描画方法を習得することをお勧めします。

>library(ggplot2)
Loading required package: methods
># グラフにデータ (uu_transitions) をセットし、横軸を elapsed_days、縦軸を uu にする> g <- ggplot(uu_transitions, aes(x = as.integer(elapsed_days), y = uu))># 折れ線グラフ (geom_line) を描画する。その際 pattern によって色を変える> g + geom_line(aes(color = pattern))

f:id:a_bicky:20150508114152p:plain

上記の例では pattern によって色を変えて描画していますが、例えばデモグラ情報(年代、性別等の情報)を持ったデータであれば、年代ごとに異なる色を使い、性別ごとに異なる線種を使うようなことも可能です。 ggplot2 の使い方についてはドキュメメントに豊富な例が載っているのでそちらを参照してください。次の資料も ggplot2 を使う上で非常に参考になります。

一粒で3回おいしいggplot2

さて、グラフを見ると pattern b は 2 日目以降 pattern a よりも UU が低いことがひと目でわかります。これは LP の内容からユーザが期待する内容とサービスの内容に差異があったと考えられます。

以上を踏まえると、現状のまま新しい LP を導入するのは避けた方が良いという判断になるでしょう。

番外編

R でコホート分析

最近 Google Analytics にコホート分析の機能が追加されましたね。R であのような表を作成してみましょう。縦軸にユーザ登録日、横軸に経過日数、値に定着率を取ることにします。

データは先程も利用した access_log_dfを使います。今回はユーザ登録日を付与していることと、レコード数を絞るためにログの期間を 4/7 までに限定していること以外は A/B テストのユーザ定着率を出した時とほとんど変わりません。

> cohort <- access_log_df %>%
+   group_by(user_id) %>%
+   mutate(min_time = min(time)) %>%
+   ungroup() %>%
+   mutate(+     registraion_date = time_to_date(min_time),+     elapsed_days = time_to_date(time)- time_to_date(min_time)+) %>%
+   filter(time < as.POSIXct("2015-04-08", tz ="Asia/Tokyo")) %>%
+   group_by(registraion_date, elapsed_days) %>%
+   summarize(uu = n_distinct(user_id)) %>%
+   group_by(registraion_date) %>%
+   mutate(retention_ratio = round(uu / first(uu, order_by = elapsed_days),3))> cohort
Source: local data frame [36 x 4]
Groups: registraion_date

   registraion_date elapsed_days   uu retention_ratio
12015-03-310 days  5321.00022015-03-311 days  1870.35232015-03-312 days  1950.36742015-03-313 days  1880.35352015-03-314 days  1910.35962015-03-315 days  1760.33172015-03-316 days  1450.27382015-03-317 days   310.05892015-04-010 days 83511.000102015-04-011 days 33260.398
..              ...          ...  ...             ...

これをおなじみ tidyr::spread関数で横長のデータに変換します。

> cohort %>%
+   select(registraion_date, elapsed_days, retention_ratio) %>%
+   tidyr::spread(elapsed_days, retention_ratio) %>%
+   arrange(registraion_date)
Source: local data frame [8 x 9]

  registraion_date 0123456712015-03-3110.3520.3670.3530.3590.3310.2730.05822015-04-0110.3980.3580.3550.3220.2970.250NA32015-04-0210.4110.3560.3500.3120.264NANA42015-04-0310.3960.3480.3610.287NANANA52015-04-0410.4150.3590.322NANANANA62015-04-0510.4170.336NANANANANA72015-04-0610.368NANANANANANA82015-04-071NANANANANANANA

簡単ですね!

最後に

以上、単純な例ではありましたが、A/B テストの結果を検証をする際にどのような検証を行うか、R を使う場合にどうすれば良いかがなんとなく理解いただけたのではないかと思います。 もちろん、より単純な検証を求められるケース、より詳細な検証を求められるケース、全く別の検証を求められるケースなど状況によって要求は様々なので、状況に応じて検証方法を検討してください。

今回は単純な集計処理しか行いませんでしたが、これから R で本格的に分析をしようと思っている方は以下の資料にも目を通すことをお勧めします。 Tokyo.R のような R コミュニティにも一度顔を出してみると良いと思います。

あとは拙作のマニアックなスライドですがいくつか参考になるかもしれません。

ではでは快適な R ライフを!!

※このエントリーは knitrパッケージを使って作成しました

*1:コーディングスタイルに迷ったら R 界で影響力の大きい Hadley Wickham 氏のコーディングスタイルに従うのが良いと思います

*2:手元の環境だと、アクセスログを読み込むのに read.table は data.table::fread の 100 倍近く時間がかかります

*3:まだ枯れていないのでバグを踏むことはありますが・・・

*4:厳密には magrittr パッケージが提供しているものを dplyr パッケージが取り込んでいます

*5:気になるようであれば corret 引数に FALSE を指定することで補正を行わなくすることも可能です

*6:A であれば B が得られる確率が p-value であって、B が得られたから A である確率ではないことを強調しておきます

*7:個人的に、普段の分析では n 週間後にアクセスした割合を採用しています。ソーシャルゲームのように毎日アクセスすることを前提としたサービスでは n 日後にアクセスした割合で良いと思います

クックパッドのサーバプロビジョニング事情

$
0
0

インフラ部の荒井(@ryot_a_rai)です。この記事ではクックパッドで利用しているプロビジョニングツール "Itamae"の紹介と細々した Tips を紹介します。

式年遷宮とプロビジョニングツール

現在、弊社ではインフラの式年遷宮*1を進めています。式年遷宮以前、弊社では Puppet を利用してサーバをセットアップしていましたが、式年遷宮に際して既存のプロビジョニングに関するコードは捨てることになるため、プロビジョニングツールの再検討を行うことになりました。

Puppet, Chef, Ansible, SaltStack を検討した結果、

  • 言語特性の観点では、Ruby DSL な Chef が良い
  • アーキテクチャ・エコシステムの観点では、シンプルな Ansible が良い

といった点から、どれも決め手に欠ける状況で、Ruby DSL で記述できるシンプルなプロビジョニングツールが必要とされていました。 そこで、以前から筆者が細々と開発していたItamae(当時は Lightchef と呼んでいました)が採用され、開発が進められました。

Itamae とは

Itamaeは一言で言うとかなりシンプルな Chef で、以下のような特徴があります。

  • Chefにおける cookbook, role, environment などの概念はなく、レシピのみを管理します
    • 低い学習コストで使うことができます
  • プロビジョニング対象のサーバに Itamae が入っている必要がない
    • SSH 経由で他サーバをプロビジョンすることが可能です
  • Gem でプラグインを作ることができる
    • Bundlerのみで依存関係を記述することができる
    • Chef における Berkshelf の代替

入門 Itamae

Itamae の使い方について軽く触れてみます。

まず、Ruby と Bundlerをインストールしておいてください。作業用ディレクトリを作ります。

$ mkdir itamae-getting-started
$ cd itamae-getting-started

Itamae をインストールするために Gemfile を置きます。直接gem installでインストールすることも可能ですが、レシピが意図せず動かなくならないように Bundler で Itamae のバージョンを固定することをおすすめします。プラグインを使う場合には、この Gemfile にプラグインを追加するだけで利用できます。

# Gemfile
source 'https://rubygems.org'
gem 'itamae'

Itamae をインストールします。

$ bundle install

レシピを書いてサーバの状態を記述することができます。ここでは sl コマンドをインストールしてみます。

# sl.rb
package "sl"do
  action :install# デフォルト値なので省略可能end
$ bundle exec itamae local sl.rb
 INFO : Starting Itamae...
 INFO : Recipe: /home/ryotarai/itamae-getting-started/sl.rb
 INFO :   package[sl] installed will change from 'false' to 'true'

なお、SSH 越しに実行する場合は以下のように実行します

$ bundle exec itamae ssh --host host001.example.jp sl.rb

上記のレシピに書かれているpackageはリソースと呼ばれ、サーバ上の何かしらのリソース(パッケージやファイルなど)の状態を記述します。Itamae には他にも様々なリソースが用意されていますが、代表的なものをいくつか紹介します。

# package リソース
package "nginx"do
  action :install
  version "..."end# remote_file リソース# ファイルを特定のパスに置くことができます
remote_file "/etc/nginx/nginx.conf"do
  source "nginx.conf"end# template リソース# remote_file リソースと同様ですが、eRuby (ERB)として評価した結果を書き出します
template "/etc/nginx/conf.d/itamae"do
  source "itamae.erb"end# execute リソース# 任意のコマンドを実行することができます
execute "echo Hello >> /etc/something"do
  not_if "grep Hello /etc/something"# このコマンドが失敗した場合のみ実行されます# only_if "grep -v Hello /etc/something" # このコマンドが成功した場合のみ実行されますend

ほかにもリソースが用意されているので一度 https://github.com/itamae-kitchen/itamae/wiki/Resourcesに目を通してみることをおすすめします。

もう一つよく使われる機能として、レシピから他のレシピを読み込む機能があります。

include_recipe "sl.rb"

上記のように、include_recipeに読み込みたいレシピのパスを渡すと他のレシピを読み込むことができます。パスはinclude_recipeを書いたレシピがあるディレクトリからの相対パスになります。 ちなみに、同じレシピを複数回include_recipeしても1度しか読み込まれないようになっているので、ご注意ください。

基本的な使い方は以上です。これだけ覚えれば使い始められるので、ぜひ導入してみてください。

さらに詳しい使い方などはドキュメントを参照してください。

Itamae Tips

クックパッドでの Itamae の使い方で特徴的な点をいくつか紹介します。

role、cookbook

前述の通り、Itamae には role や cookbook を管理する仕組みはありませんが、include_recipeで他のレシピを読み込むことで同様の機能を実現しています。

例えば、

├── bootstrap.rb
├── cookbooks
│   ├── nginx
│   │   ├── default.rb
│   │   └── templates
│   │       └── etc
│   │           └── nginx
│   │               └── nginx.conf.erb
│   └── ruby
│       └── default.rb
└── roles
    └── web
        └── default.rb

このように、cookbook と role のディレクトリを用意し特定の命名規則にしたがってファイルを置いています。

# bootstrap.rb# 2015/05/12 10:03 修正moduleRecipeHelperdefinclude_cookbook(name)
    include_recipe File.join(__dir__, "cookbooks", name, "default.rb")
  endendItamae::Recipe::EvalContext.include(RecipeHelper)

include_recipe File.join("roles", node[:role], "default.rb")
# roles/web/default.rb
include_cookbook "nginx"
include_cookbook "ruby"

role はサーバによって異なるので、node[:role]を参照して実行するようにしています。クックパッドでは EC2 インスタンスのタグで role を指定しているので specinfra-ec2_metadata-tagsを利用して、実行するレシピを決定しています。

このように準備しておくと、以下のように実行することができます。

$ echo '{"role": "web"}' > node.json
$ bundle exec itamae local --node-json=node.json bootstrap.rb

remote_file, templateのsource :auto

remote_file, template リソースにはsourceアトリビュートがあり、これでファイルやテンプレートを指定します。

$ ls
recipe.rb   nginx.conf.erb
# recipe.rb
template "/etc/nginx/nginx.conf"do
  source "nginx.conf.erb"end

通常、sourceにはファイルパスを指定しますが、特別な値として:autoが用意されています。:autoが指定されると、Itamaeは配置先のファイルパスから自動的にファイルを探します(詳細)。ちなみに、sourceのデフォルト値は:autoなので、これは省略することができます。

├── recipe.rb
└── templates
    └── etc
        └── nginx
            └── nginx.conf.erb
# recipe.rb
template "/etc/nginx/nginx.conf"doend

この方法を利用すると、ディレクトリ構成がわかりやすくなりファイルが増えた場合にも管理しやすくなると感じています。

Node#reverse_merge!

ノードアトリビュートにデフォルト値を設定する場合は、Node#reverse_merge!が便利です。Node#reverse_merge!はすでに値がセットされている場合は上書きせず、ディープマージを行います。

例えば、以下のように使います。

# recipe.rb
node.reverse_merge!({
  nginx: {
    worker_processes: 4
  }
})

template "/etc/nginx/nginx.conf"
$ bundle exec itamae local recipe.rb
# この場合、worker_processesは4になります

$ echo '{"nginx": {"worker_processes": 8}}' > node.json
$ bundle exec itamae local --node-json=node.json recipe.rb
# この場合、worker_processesは8になります

SSHを使わない

Itamae はコマンドを実行して、その結果を受け取ってから、次のコマンドを実行するため、レイテンシが高いサーバに対して SSH 実行を行うと、遅く感じることがあります。そのような場合は、対象サーバに Itamae をインストールすることで解消します。クックパッドでも最初は SSH 実行を使っていましたが、国外のサーバが増えるにつれ、レイテンシが気になるようになりサーバ上でのローカル実行に切り替えました。

システムに入っている Ruby を使って Itamae をインストールすることも可能ですが、Ruby のバージョンアップなどによって Itamae が動かなくなってしまうことを防ぐため、Ruby などの依存関係ごとインストールするパッケージを用意しています。現在、Ubuntu 14.04 用の Debian Package のみビルドしていますが、chef/omnibusを使っているので、他ディストリビューション用のパッケージもビルドできると思います。

オペレーションフロー

実際に Itamae を実行する際には複数台にまとめて実行するため、Capistrano でレシピを転送したあと Itamae を実行しています。

ただ、この方法だと

  • 台数が増えた時に遅い
  • 台数が増えてくるとオペレーションのコストがかかる
  • 手動で実行していると、レポジトリ上のコードとサーバの状態が食い違う
    • コミットされたからといって Itamae が実行されるわけではない

といった問題があり、現在自動実行の仕組みを開発中です。

f:id:ryotarai:20150511221427p:plain

GitHubへのプルリクエストの作成やプッシュを契機として、dry-run や実際の実行を行います。上図の通りクックパッドでは Consul を利用しようとしていますが、Itamae の SSH 実行など他のバックエンドも用意しようと考えています。

今後の方向性

今後も低い学習コストで使い始められ、軽量・シンプルに使えるという特徴を維持していきます。 それと同時に、大きな規模になっても使える機能を備えていきたいと考えています。

欲しい機能がある場合やバグを見つけた場合は、遠慮なく Issue や Pull Request を作成していただければと思います。

Itamae Meetup is coming soon

ついに実用段階に入った、と言っても過言ではない Itamae ですが、利用事例も少しずつ聞くようになってきたのでミートアップを開催する予定です。日時などはまだ未定ですが、ぜひお越しください&発表してください。

開発者テストの失敗を追跡しやすくすることで大人数での Web サービス開発を加速する

$
0
0

会員事業部サービス開発グループ長の村田です。

私は2015年1月から会員事業部でサービス開発エンジニアをやっていますが、2014年4月までは技術部開発基盤グループで Web サービス開発を加速させる様々な取り組みを実施していました。本稿では、開発基盤グループ時代に私が取り組んだ開発者テストの失敗を追跡しやすくする取り組みについて説明します。

クックパッドの Web サービス開発と CI

クックパッドのサービス開発は、大きくても5名くらいの小さなチームが一つの機能を担当します。しかし、多数のチームが1つの大きな Rails アプリケーションを同時に変更するのが特徴です *1

Web サービス開発を加速する工夫には様々な方向性が考えられますが、ここでは、クックパッドのようなスタイルでの Web サービス開発を加速するために開発者テストを何如に円滑にするかを考えます。

f:id:mrkn:20150519094528p:plain:w120

図: オムキンス

クックパッドではオムキンスと呼ばれる CI システムがあり、CI でのテストをパスしたリビジョンだけがデプロイを許されます。

サービス開発はデプロイしてからが本番です。開発中のサービスをユーザに出して、使われ方を分析して改善していくサイクルを何度も回すには、何度もデプロイする必要があります。 CI でのテストが円滑に成功し続けることが高速なサービス開発の肝です。

コミットと CI の監視

クックパッドでは、開発者は基本的に自分のコミットを自分で master ブランチにマージし、デプロイも自分でやります。そのため、開発者は自分のコミットがマージされた後に CI で走るテストの結果には十分気を付けています。

CI でのテスト結果はチャットに通知されます。そのような環境では、開発者は、自分のコミットが CI でテストされているときは、いつもよりチャットに注目して失敗にすぐ反応できるよう準備しています。その最中はいつもより開発に集中できません。開発に集中してしまうと、テストの失敗にすぐ反応できないからです。

そのような状況を解消し開発者が開発に集中できるようにするため、2012年に jenkins-hipchat-publisher プラグインをベースに、CI でテストが失敗したときにコミットした人をチャット通知で自動メンションするプラグインが、当時開発基盤グループに所属していた id:sora_hによって開発されました*2。そのプラグインによる通知の様子を以下に示します。

f:id:mrkn:20150519103844p:plain:w600

図: テスト失敗通知でのメンション

このようなメンション通知があるおかげで、チャットに張り付いていない開発者でもテストの失敗に気付きやすくなります。

チャットでの失敗通知をリッチにする

テストの失敗通知がチャットに流れたときの開発者の動きを見てみましょう。通知でメンションされた開発者は CI のテスト実行ログを確認します。どのテストが失敗したかを把握して次の行動に移るためです。このとき、下図で示す4つの場合に分かれます。

f:id:mrkn:20150519110911p:plain:w500

図: テスト失敗時の行動4パターン

失敗が自分の変更に関係ある場合は、チャットで修正中である旨を報告し、テストがきちんと通るように修正します (図の左上 Case 1) 。

失敗が自分の変更と無関係である場合は2つに分かれます。図の右上 Case 2 は、自分のコミットが原因で他人のテストを失敗させてしまった場合です。この場合は、原因を調査するために、失敗したテストの関係者を git blame で調べてチャットで質問したり、修正を移譲したりします。この作業は、CI の実行ログと手元のターミナルとを行き来する必要があり地味で面倒な作業です。

他人のコミットによって自分が書いたテストが失敗する場合もあります (図の左下 Case 3)。 この場合は、自分が書いたテストの内容が間違っていたり不完全だったりするので、テストを自分で修正する必要があります。しかし、自分ではすぐに失敗に気付けません。

このように、CI でテストが失敗した後に起きる行動には、CI の失敗に注目していない他人を巻き込む必要がある場合の方が多く、たいていその対象者は git blame で調べる必要があります。この工程は、サービス開発を遅延させる大きな要因です。

これを改善するため、以下に示す新しい通知を導入しました。

f:id:mrkn:20150519111046p:plain

図: テスト失敗の通知完全版

1行目が新しい通知です。下の2行は先ほどお見せした jenkins-hipchat-publisher プラグインによる通知です。

この通知の内容は、rspec がログの最後に出力してくれる、失敗した examples を再実行するコマンドラインとほとんど同じです。違いは、以下の要素が加わっていることです。

  • 「ファイル名:行番号」の部分が GitHub Enterprise へのリンクになっている (もちろん、該当行への直リンク)
  • その行の最終更新リビジョン (git blame の結果で、もちろん GHE へのリンクになってる)
  • その行を最後に変更した人と時期 (これも git blame の結果)

これらの情報がチャットに流れてくるだけで、失敗した example をすぐに調べられます。失敗が自分の変更と直接関係なさそうなときでも、git blame をしないで関係者をすぐ呼べます。開発者は、自分が必要なときだけテストの失敗ログを見に行けば良いし、手元で再実行したい場合もチャットで通知された rspec のコマンドラインを端末にコピペするだけです。

まとめ

本稿では、CI で失敗したテストについの情報をチャットに通知することで、開発者テストの失敗を追跡しやすくする方法について説明しました。

最後に、この通知内容を生成するスクリプトを紹介します。このスクリプトは、標準入力に rspec がログの最後に出力する rspec コマンドのリストが与えられる事を前提に書かれています。コマンドライン引数で、欲しいフォーマット (html, json, plain-text) と git のブランチ名を与えます。

#! /usr/bin/env rubyrequire'pathname'require'time'require'rubygems'require'bundler/setup'require'action_view'includeActionView::Helpers::DateHelperGHE_REPOSITORY_ROOT = ENV["GHE_REPOSITORY_ROOT"]

defshort_ref(ref)
  `git show --oneline #{ref}`.each_line.first.split(//)[0]
end

format = ARGV[0]
branch = ARGV[1]

root_dir = Pathname.pwd
app = root_dir.basename

entries = $stdin.read.lines.map { |line|
  rspec, filename, lineno, description = line.chomp.sub(/\s*#\s*?(.*)$/, "\\1").split(/[ :]/, 4)
  nextnilunless rspec && filename && lineno
  description ||= ''

  spec_real_path = Dir.chdir(File.dirname filename) { Pathname.pwd.join(File.basename filename).relative_path_from(root_dir) }

  [ filename, lineno ].tap do |ary|
    blame = `git blame -w -l #{spec_real_path}#{additional_argument}`
    hash, author, timestamp = blame.match(/^([0-9a-fA-F]+)\s+(?:\S+\s+)?\(([-+=^:;<>_@\.0-9A-Za-z ]+?)\s+(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+[-+\d]+)\s+#{lineno}\)/m)[1,3]

    relative_timestamp = time_ago_in_words(Time.parse(timestamp))
    ary << hash << "#{author}, #{relative_timestamp} ago"<< spec_real_path << description
  end
}.compact

case format
when'html'if branch
    entries.each do |filename, lineno, hash, info, spec_real_path, description|
      message = "rspec "

      path = root_dir.join(spec_real_path).relative_path_from(root_dir.parent)
      message << %Q[<a href="#{GHE_REPOSITORY_ROOT}/blob/#{branch}/#{path}\#L#{lineno}">#{filename}:#{lineno}</a>]
      message << %Q[ \# (<a href="#{GHE_REPOSITORY_ROOT}/commit/#{hash}">#{short_ref(hash)}</a>) #{info}<br />]

      puts message
    endendwhen'json'require'json'

  failures = entries.map do |filename, lineno, hash, info, spec_real_path, description|
    path = root_dir.join(spec_real_path).relative_path_from(root_dir.parent)
    { :file => filename, :line => lineno,
      :commit => hash,
      :description => description,
      :real_path => path,
    }
  end

  payload = {:failures => failures}
  payload.merge!(:build_url => ENV["BUILD_URL"]) ifENV["BUILD_URL"]
  payload.merge!(:build => ENV["BUILD_NUMBER"]) ifENV["BUILD_NUMBER"]
  puts payload.to_json
else
  entries.each do |filename, lineno, hash, info, spec_real_path, description|
    puts "rspec #{filename}:#{lineno} \# (#{hash}) #{info}"endend

*1:Akira Matsuda. The recipe for the worlds largest rails monolith. Ruby on Ales 2015

*2:このプラグインは社内サービスから情報を取得する必要があるためオープンソースにしてません

クックパッドの新卒研修2015

$
0
0

技術部の牧本 (@makimoto) です。 新卒社員研修の企画・運用を担当しています。

このエントリをご覧になっている方の中には、この春から新社会人として働いている方も多いのではないでしょうか。

クックパッドでもこの春に新卒社員が入社し、現在新卒社員研修の真っ最中です。

本稿では、クックパッドにおける新卒社員向けの技術・サービス開発領域の研修についてご紹介をします。

みんなが技術を理解すること

今年の新卒研修では、エンジニア職ではない総合職の新卒社員にも技術研修を実施しました。

職種や役割にかかわらず、すべてのメンバーが技術を正しく理解し、活用することが、組織の成長に役立つと考えているからです。

営業やサービス開発のディレクターなどエンジニア以外のメンバーが、技術的な知識にもとづいた意思決定をできるようになることで、もっと事業の精度や速度の改善に貢献できるのではという思いがあります。

とは言え、エンジニア職と総合職とで同じ研修プログラムを受けてもらうのは困難なので、セキュリティや品質などの共通で理解してほしい研修を経たあと、職種ごとに組まれた技術研修プログラムに参加してもらいました。

総合職向け技術研修

今年度の総合職向けの技術研修は、以下のようなメニューにしました。

  • 社内のエンジニアによるケーススタディ (レシピ検索、分析システムなど)
  • 情報技術の活用 (検索技術と暗号技術の仕組み)
  • ハードウェア
  • ソフトウェア
  • ネットワークとインターネット
  • ウェブアプリケーションの構造
  • プログラミングの基礎

直接は業務には関わらない (ので配属後に学ぶのが難しい) ハードウェアやソフトウェアの仕組みから、インターネットがどのように繋がっているか、われわれが提供しているウェブアプリケーションがどのような構造になっているかを学ぶプログラムです。 プログラミングについては、概念的な話だけではなく実際触ってみて身につく面も多いので、基礎的なプログラミングの研修も加えました。

Brian Kernighan 『ディジタル作法*1などからカリキュラムを選定し、独自の教材を開発しました。

総合職新卒社員は、理工系出身から社会学系出身まで多様なバックグラウンドを持っていますが、実際に手を動かすワーク (例えば、RSA 暗号を手元で計算して体験する、など) を取り入れたり、先に理解できた人がまだの人を教える仕組みを作ったりして取り組んでいます。

エンジニア職向け技術研修

エンジニア職の技術研修では、個人開発では体験できない内容にフォーカスしました。

  • インフラ研修
  • チーム開発研修
  • クックパッドのウェブアプリケーションの例外調査・修正

インフラ研修では、インフラストラクチャー部の協力のもと、実際に本番環境で使える sudo 権限や AWS のキーを新卒社員に与えて、サービスのサーバ構成の調査やサーバのセットアップと本番への導入などを体験します。

チーム開発研修はワイクル株式会社の角征典様に協力いただき、『Team Geek』やアジャイル・スクラムのプラクティスを取り入れたチームで成果を出すための技法を学ぶ研修でした。

f:id:makimoto:20150508173822j:plain

チーム開発研修の一幕
アイディアを組織に広めるゲーム

サービス開発研修

サービス開発における PDCA サイクルを高速に回す考え方は、どのような仕事にも応用が効ききます。 ユーザーの課題発見、施策の効果を検証するまでの、ひと通りの流れを体験しながら学ぶプログラムとして設計しました。

  • サービス企画フレームワーク
  • ユーザー調査
  • プロトタイピング
  • 施策検証

サービス開発に携わっているエンジニア・デザイナーを講師に、座学や過去の施策から学ぶケーススタディ、具体的なテーマに沿った課題などを行ないました。

リースタートアップなどの理論と実際の過去の施策における成功事例・失敗事例などを知るとともに、特定のテーマに沿って EOGS *2やリーンキャンバスを書いてもらったり、ユーザーインタビューの設計・実施をしたりなど、実際にサービス開発で行なうプロセスを実践します。

f:id:makimoto:20150522085026p:plain

サービス開発研修の一幕
ユーザーインタビューを行ない、そこで得られた知見をもとにペルソナを構築する

おわりに

本稿では、クックパッドにおける新卒社員向けの技術・サービス開発領域の研修についてご紹介をしました。

この規模の研修プログラムを組むのはクックパッドとしても初めての取り組みでしたが、われわれの技術や知識、アイディアの棚おろしをして、見直す良い機会になりました。*3

新卒研修のゴールは、新卒社員が実務で活躍できるようになっていることです。 この研修を通して、新しい仲間たちが現場で活躍できるように引き続きサポートしていきます。

最後に宣伝ですが、クックパッドでは今年も学生の皆さんを対象に夏のインターンシップの開催が決定しました。 詳しいことが決まり次第、本ブログなどで紹介しますので、今しばらくお待ちください。

*1:この本は新卒ソフトウェアエンジニアのための技術書100冊 - クックパッド開発者ブログのリストにも含まれています

*2:Emotion Oriented Goal Setting. クックパッド内製のサービス企画フレームワーク

*3:特に、研修全般を取りまとめている私にとっては、研修教材をまとめて見ることができるので非常にお得な機会でした。

サービス開発におけるエンジニアの役割

$
0
0

会員事業部*1の小川(@conceal_rs)です。

会員事業部ではプレミアムサービスの価値を向上させるために、日々機能改善や新しい機能やサービスの開発をしています。今回はサービス開発をするときにエンジニアがどういう役割を果たすといいかについて、私なりの経験からの話をしたいと思います。

サービス開発とは

サービス開発とはユーザのみなさんに、アプリやウェブを通じて何らかの価値を提供することだと考えています。価値と言ってもいろいろなものや形態があり、Webサービスというとだいたいがツールぽいものを想像しますし、最近だとゲーミフィケーションを使った脳トレサービスなどもあります。また既存のサービスに新しい機能や体験を追加して価値を届けるということもサービス開発です。私が所属している会員事業部はプレミアムサービスを利用して頂いているユーザのみなさんに新しい価値を提供すべく、日々業務に勤しんでいます。

もちろんサービス開発とは、何も新しいものだけではありません。例えばバグ修正や既存機能の改善、また他社と連携した企画など、さまざまなものを含んでいます。開発規模も数日で終わるものから数ヶ月かかるものまで、果ては数人で数ヶ月以上かかるものまで多種多様な開発があります。

クックパッドでのサービス開発

ではクックパッドにおけるサービス開発はどのように行っているのか。このブログでもサービス開発についての幾つか記事が書かれていますが、ここでは私がいままで行ってきたサービス開発についてお話したいと思います。

チームで開発する

クックパッドではサービス開発をするとき、ディレクターとエンジニアがチームを組むことがほとんどで、その多くがディレクター1名とエンジニア1名の2名体制という少人数です。少人数での開発になる理由としては、クックパッドでのサービス開発ではそこまで大規模なものはあまりないということと、少人数だとスピード感があるためです。

大規模なものがないというと先ほどの開発規模の話と齟齬があるかもしれませんが、実際にサービス開発に関わるときに関わる範囲というのはそれほど大きくはありません。例えば小さな機能であればRailsで言うところのコントローラー単位になることが多く、また大きなものになったとしてもchankoのような限定公開する仕組みを使っているため、影響範囲を閉じ込めることが可能です。

別の観点では、ディレクターとエンジニアが一対一で話をするほうが、より充実した議論ができる可能性が高いということも理由の一つです。改善や機能追加であっても、どのようなものをユーザのみなさん提供するかについてしっかり議論しなければならないと考えています。議論するときにあまり人数が多くなると、論点が発散してしまって収集がつかなくなることもあります。そのようなときに二〜三人であれば、議論もそこまで発散しないですし、落とし所を探りやすいという印象があります。

ユーザへ提供する価値を考える

サービスを開発するときには、まずユーザのみなさんに提供する価値がどんなものであるのかをしっかり考えるようにしています。当たり前のように思えて意外に難しいのがこの価値を考えるプロセスです。EOGSやAARRRなど色々なツールを使うことが多いですが、まずは使ってもらうユーザさんのことをしっかり考えるように心がけています。例えばクックパッドの主なユーザは主婦層ですが、じゃあいつ使うのか、昼時ならどういう気持ち・状況で使っているのか、重要度・切迫感はどの程度かなど、いろいろなことを考えるように心がけています。

とは言え私は男性なのですべてわかるとは思っていません。そういうときは身近にいる人のことを考えたり、社内でターゲットユーザに近い人を探して話を聞いたりします。それで全てがわかるわけではないですが、よりユーザさんの目線に近い人とたくさん話をすることで、いろいろなことに気づけるので積極的にやるべきだと考えています。

またデータもわかる範囲で調べるようにしています。例えば官公庁が出している白書や調査会社が公表しているデータからわかることも多いですし、クックパッドへのアクセス情報からわかることもあります。

これらの気づきやデータも参考にして、提供する価値についてしっかりと考えることを心がけています。

効果測定する

さて価値を考えてチームで実装すればあとは公開するだけですが、ただ公開すればいいというわけではありません。事前に考えた価値に本当に効果があったかを検証しなくてはなりません。この辺りをしっかり考えておかないと、漠然と「なんとなく使われている」ということしかわからず、どう改善していいものか判断できなくなってしまう可能性があります。よりよいものを作り続けるためには、この効果測定をしっかり設計しなければなりません。逆に言うと、測定できる指標をしっかり設計しないと、何が良いものであるかを判断できないということでもあります。

エンジニアのサービス開発への関わり方

サービス開発の概要は説明してきましたが、ではエンジニアはどのように関わるべきなのでしょうか。これも私の体験からくる持論なのですが、簡単に紹介したいと思います。

企画段階では実現可能性について考えない

エンジニアであれば企画を考えたり相談したりしているときに、どれ位の期間で実装できるか、実装可能かどうかについて考えることが多いと思います。もちろん実現不可能なものを考えても仕方ないのですが、私自身は企画段階では実現可能性についてあえて考えないようにしています。「そんなことしたら無理ゲーやらされることにならない?」と不安に思うかもしれませんが、企画が固まりきるまでに実現できるかどうかを考えてしまうと、逆に発想が制限されてしまう可能性もあります。この制限によって、本当に作りたかったものではないものとは微妙に違うものができあがってしまっては元も子もありません。作りたい価値を明確にするまではできるかぎり自由な発想で議論できるようにしたいので、実現可能性についてはあえて考えないようにしています。

サービスの価値を一歩引いて考える

じゃあサービス開発するときにエンジニアが気をつけるべきことってなんでしょうか。例えばディレクターから企画が出されたときに漠然と良さそうだと感じたとしたら、そのまま実装に入ってしまうことって多いかもしれません。ただこの漠然とした感想というのが危険です。本当に良いと思っていないのに作り始めると、たいてい中途半端なものになってしまいます。そのようなときは、できれば一歩引いた状態で考えることを心がけています。ただ一歩引いて考えると言ってもなかなか難しいものです。漠然と良いとか悪いと感じることってなかなか言語化できないので説明もしづらいし、そうすると何も言えなくなって「黙っておこうかな」と思うこともあるかもしれません。

あえて「なぜ」と問いかける

そういうときの一つの方法として、あえて「なぜ何ですか?」と問いかけるようにしています。例えば「料理が楽しくなる機能」についての企画であれば、「料理が楽しくなる必要があるの?」と問いかけます。個人的には楽しくなったほうがいいと思っていますが、なぜ「楽しいほうがいい」のかについて詳しく議論されていないことも多く、また企画者がその理由を言語化しきれていない場合、チームが漠然とした価値に向かって進んでいってしまう危険性があります。そのような状態では精度が低いものが生まれる可能性が高く、公開しても価値を感じ取ってもらえずに終わることになってしまいます。これでは意味がありません。そのためにもできるかぎり問いかけを続けるようにしています。

そもそも論、極論を提示する

もう一つの方法として、「そもそも論」や極論を言うときもあります。例えば先ほどの「料理が楽しくなる機能」であれば、「そもそも料理って自分でする必要があるの?」という問いかけになります。そこで「例えばある業者が1食100円でバランス的にも食材の安全性についても完璧な料理を作ってくれるとしたら、自分で料理する意味はあるの?」という極論を出します。もちろん現実的には実現不可能だとは思うのですが、価値を考える上ではこのような問いかけをすることも重要です。

このような方法を使う理由としては、一般的に何らかの議題について話をしたり考えたりするときには、人は何かしら前提条件をおいている可能性が高いと考えているからです。考えるべき価値というのは議題の奥深くに存在していることがほとんどなのに、その表層だけをたどってしまって価値に行き着かないことがわりと多いと感じています。そのためにも「そもそも論」や極論を提示してみるのも一つの方法です。

よりよいサービス開発に向けて

このようにエンジニアが行うサービス開発といっても、企画を考えて実装するだけではなく、いろいろな役割があることをつらつらと書いてみました。もちろんこれが正解というわけではなく、他にもっといい方法があるかもしれません。日々試行錯誤しながらよりより価値を提供できるようにサービス開発を続けています。クックパッドでは一緒にサービス開発をするエンジニアを募集しています。我こそはと思われる方はぜひご応募ください。

*1:技術部長でもあります

モバイルファースト時代のネットワークレイヤデバッグ手法

$
0
0

こんにちは。インフラストラクチャー部 セキュリティグループの星 (@kani_b) です。
クックパッドでは主に "セキュリティ"か "AWS"というタグのつきそうな業務全般を担当しています。

ここ数年、クックパッドではいわゆるネイティブアプリの開発が非常に盛んです。
私達インフラストラクチャー部はネイティブアプリの直接の開発者ではありませんが、開発が円滑に進むように色々なレイヤでそのお手伝いをしています。

PC 向けサービス開発と比較して、スマートフォン向け、特にネイティブアプリにおいては、何かトラブルがあった際に どこで何が起きているか、そのデバッグを行うことが若干難しいと感じています。 今回はいわゆる jailbreak や root 化をせず、ネットワークのレイヤからデバッグを行う方法についていくつかご紹介します。

HTTP プロキシによるキャプチャ

まずは HTTP プロキシを利用してリクエスト/レスポンスのキャプチャを行う方法です。 アプリが行っている通信が HTTP/HTTPS だけで、対象のスマートフォンに自由にアクセスでき、 HTTP レイヤで様子を伺うことができれば良い場合はこちらを使います。

以下のようなソフトウェアが使えます。

  • Charles Proxy
    • シェアウェア。GUI で操作できる
  • Burp Proxy
    • Free 版と Professional 版がある。GUI で操作可能。脆弱性検査などにも多く利用されている (主に Professional 版)
  • Fiddler
    • Burp と同じく、脆弱性検査などにも利用されているプロキシ。無料ソフトウェアとして公開されている
  • mitmproxy
    • OSS。Python で実装されている。ターミナルから使う

これらのツールの使い方は基本的には共通しています。起動すると PC に HTTP Proxy を作成してくれるので、 開発用のスマートフォンのネットワーク設定を変更し、起動した Proxy を経由するようにするだけです。 PC が接続されているネットワークと同じネットワークに接続して設定するのが楽で良いでしょう。 他のソフトウェアにも様々な機能がありますが、デバッグ用途であれば大半のケースで mitmproxy が使えると思います。

以前のバージョンの mitmproxy で HTTPS 通信をキャプチャするためには、初回起動時に生成された証明書一式を 何らかの手段でスマートフォンに転送して…という設定を行う必要があったのですが、 現在はプロキシの設定をした状態で mitm.itにアクセスすると証明書をダウンロードできます。 (上記リンクを踏むとわかりますが、プロキシを通さないと正しく設定ができていないことがわかります)

これで、HTTP/HTTPS 通信をキャプチャする用意ができたので、スマートフォン側でプロキシの設定をするだけです。 試しにクックパッドアプリ開発環境の通信をキャプチャしてみると以下の画面のように見えます。 f:id:kani_b:20150528164909p:plain

任意のリクエスト/レスポンスについて詳細 (header, body, etc) を確認することもできます。 また、リクエスト/レスポンスのリプレイや書き換えも可能なので、 例えばクライアント、サーバ共に細工したリクエストやネットワーク環境悪化による再送などが悪影響を及ぼさないか調査することも可能です。

パケットキャプチャ

HTTP プロキシを使うとだいたいのケースでデバッグを行うことが可能ですが、 以下のような場合は HTTP レイヤではなく IP レイヤでキャプチャを行う必要があります。

  • キャリア回線を使っている時の通信をそのまま解析したい
  • (DNS|SSL/TLS|その他 HTTP 以外のプロトコル)で行われている通信を確認したい
    • 例えば TLS handshake で決まった CipherSuite の確認など

iOS の場合

iOS と Mac を利用している場合、Remote Virtual Interface (RVI) が使えるようになっています。 RVI は Apple のドキュメントにも記載されていますが、USB 接続した iOS デバイスが行う通信を Mac 側にミラーしてくれるインタフェースです。 通信を Mac 経由で行うのではなく、あくまで iOS デバイスのメインネットワークインタフェース (4G や Wi-Fi) が行っている通信のコピーが Mac に流れてきているだけですので、自然な形でキャプチャを行うことができます。 RVI のセットアップも、Mac と iOS デバイスを USB で接続し、対象の iOS デバイスの UUID を調べたら以下のように RVI デバイスを作成します。

$ rvictl -s xxxxxxxxxxxxxxxxxxxxxx (デバイスの UUID)

この状態で ifconfig を見ると rvi0 というインタフェースが作成されています。

$ ifconfig rvi0
rvi0: flags=3005<UP,DEBUG,LINK0,LINK1> mtu 0

このインタフェースは他のネットワークインタフェースと同様に扱えますので、tcpdump や Wireshark など お好みのネットワークアナライザでキャプチャや解析を行うことが可能です。 一例として、Wireshark を使えば GUI 上で簡単に解析を行うことができます。 f:id:kani_b:20150528164934p:plain

Android の場合

Android は root 化なしに RVI のようなものを使う手段がないため、 tPacketCaptureなどキャプチャ用 Android アプリを利用するか、 Android が利用するネットワークの経路上 (例えば PC を Wi-Fi AP にしてそこに接続する) でキャプチャを行う必要があります。
他に良いキャプチャ方法をご存知の方がいたら是非教えてください!

デバッグできるレイヤを増やす

スマートフォンアプリに問題が起こった際、上記のようにネットワークレイヤからも調査できるようにしておくと 調査の幅が広がります。
例えば、筆者は以下のようなことをこれまで紹介したような方法で調査していました。

  • アプリの不具合により、HTTP ヘッダに予期していないバイト列が含まれリクエストに失敗する現象
  • アプリが利用する特定のホストに対して名前解決が行われなくなる現象
  • 特定のアプリ/端末が HTTPS 接続で利用する CipherSuite
  • API エンドポイントに不正な文字列やリプレイされたリクエストを送出した時の挙動

まとめ

この記事では、主にネットワークのレイヤから、スマートフォンアプリのデバッグを行う手法について解説しました。 ここで紹介した手法はアプリ開発者に限らず、サーバ側の開発・運用に携わる方も覚えておいて損はないと思います。
クックパッドではエンジニアの役割によらず、それぞれの得意とするレイヤで協力しながら調査を行うことがよくあります。
色々な視点から、楽しいデバッグライフを送りましょう。

モバイルファースト時代のWebアプリケーションデバッグ手法

$
0
0

買物情報事業部の前田 (@TakatoshiMaeda) です。

Webアプリケーションを開発していると、思ったようなスタイルが適用されなかったりJavaScriptの挙動が意図しないものとなっているケースがままあります。そのような時に、Chrome Developer ToolsSafari Web Inspector等を用いてスマートフォン実機に接続をしてデバッグしますが、

  • Android標準ブラウザ
  • Android/iOSアプリケーション内部のWebView*1

ではインスペクタとの連携に対応しておらず、上記ブラウザでのみ再現がされる不具合に対してはHTML/CSSの確認やJavaScriptを実行しながらのデバッグは基本的に出来ません。 weinreというデバッグツールを用いて、上記ブラウザ環境下でWebアプリケーションのデバッグを行う方法について紹介します。

weinre

weinreはNode.jsで開発されているWebアプリケーションのデバッグツールです。開発PC上でweinre serverを立ち上げ、webページに埋め込んだweinreクライアントとやりとりを行うことで、ブラウザがインスペクタに対応していない場合でもインタラクティブなデバッグが可能です。

f:id:takatoshi-maeda:20150528172447p:plain

JSConsole等類似のデバッグツールもありますが、weinreはブラウザ標準のインスペクタと似たようなインターフェースと機能を備えており違和感なく利用できることからweinreを利用しています。

開発PCと同一LANにスマートフォンが接続されている環境で、実際に実機でデバッグを行う手順を解説します。

インストールから起動まで

Node.jsがインストールされていればnpmコマンドでインストール可能です。

$ npm install -g weinre
$ weinre --boundHost $YOUR_LOCAL_IP

上記コマンドでweinreが立ち上がります。boundHostオプションの指定ですが、スマートフォンから開発サーバーにアクセスをする場合、localhostを指定してしまうとデバッグに必要なJavaScriptの取得が出来ないため、開発機のローカルIPを指定して下さい。

weinreの起動確認と、対象となるJavaScriptの埋め込み

開発機のローカルIPの8080ポートに対してブラウザでアクセスをすると、以下の画面が表示されます。

f:id:takatoshi-maeda:20150528172457p:plain

この画面が表示されれば起動しています。

ここからデバッグ対象のWebアプリケーションにデバッグコードを挿入します。

  • 対象となるスクリプトをscriptタグで事前に挿入する方法(Target Script)
  • ブックマークレットでscriptタグを動的に挿入する方法(Target Bookmarklet)

の2つの方法がありますが、今回はブックマークレットを用います。 Target Bookmarkletセクションに

Javascript:(function(e){e.setAttribute("src","http://10.0.1.9:8080/target/target-script-min.js#anonymous");document.getElementsByTagName("body")[0].appendChild(e);})(document.createElement("script"));void(0);

上記タグが生成されています。スマートフォンにコピーして、デバッグを行いたいページでアドレスバーに貼り付けて下さい。貼り付けた後、開発PC上でhttp://$YOUR_LOCAL_IP:8080/client/#anonymousにアクセスすると、インスペクタが立ち上がっています。

f:id:takatoshi-maeda:20150528172511p:plain

Targetsに対象となるサイトが表示されていれば接続に成功しています。 通常のインスペクタと同様、選択しているDOMエレメントの配置確認や動的なスタイル変更、JavaScriptの実行が可能です。

f:id:takatoshi-maeda:20150528172518p:plain

ネイティブで提供されているインスペクタと比べると手間がかかりもたつきを感じることは事実ですが、不具合の手がかりが全くない状況でバグを潰すよりも遥かに効率が良いと感じるはずです。

最後に

AndroidやiOSでは新しいバージョンのOSであれば標準でデバッグが可能となってきており取り巻く状況は改善してきています。しかし、まだまだ古い環境で利用されているユーザーさんが多いことも事実です。 この記事が問題に直面した方の開発効率化の一助となれば幸いです。

*1:AndroidはKitKat以降ではコードでデバッグオプションを有効にするとWebViewもインスペクタと接続でき、iOSは6.0以降であればXcodeでビルドし端末に転送したアプリケーションはインスペクタと接続できます


サービスを通じて日々の嬉しい体験を増幅する

$
0
0

こんにちは。ユーザファースト推進室エンジニア兼デザイナーの長野です。

クックパッドでは、今年に入ってからレシピやつくれぽ*1を外部のSNSに共有できる機能を拡充しており、そのデザインおよび開発を担当しています。

本エントリでは、特につくれぽ共有の施策について、サービスの背景やねらいをご紹介したいと思います。進行中のプロジェクトのため数値的な成果などをご紹介することができないのですが、本エントリを通じてクックパッドのサービス開発の考え方をお伝えできればいいなと思います。

つくれぽ共有施策の概要

クックパッドのサービス上でつくれぽ写真をタップすると、写真を大きく見ることができ、そこからつくれぽをFacebookやTwitterなどの外部SNSへ共有することができます。

f:id:yoshiko-nagano:20150601110955p:plain

つくれぽ拡大表示画面(左:PC、右:スマートフォン)

このつくれぽをSNSで見た人は、リンクを押すとつくれぽ固有のパーマリンクへ遷移することができます(これまでつくれぽにはパーマリンクがありませんでした)。 さらに、パーマリンクや拡大表示の画面には「いいね」ボタンがあり、つくれぽの作者さんに向けてライトなフィードバックを送ることができます。

f:id:yoshiko-nagano:20150601110947p:plain

つくれぽパーマリンクページ(左:PC、右:スマートフォン)

施策の背景

これまで、つくれぽはレシピの作者さんへ感謝の気持ちを伝えるためのものとして機能してきました。送られたつくれぽはレシピ作者さんの元へ届き、レシピの作者さんはそこに返信コメントを返すことができます。 あくまでつくれぽはレシピに従属するものとして設計されており、それ故に各つくれぽのパーマリンクも用意されていませんでした。

しかし、実際つくれぽを送る工程を想像してみると、他人のレシピを見ながらではあるものの、自分なりのアレンジや工夫を加えたり、盛り付けにこだわったり、写真を綺麗に残すために創意工夫をしたりと、とてもクリエイティブな作業があふれています。 そこで今回の施策では、つくれぽも一人のユーザーさんによって作られた貴重なコンテンツ(作品)であると捉え、サービスの設計を進めました。

施策のねらい

自分がみつけた美味しいレシピを共有できれば、それが誰かの役に立つかもしれない。美味しく上手につくれた料理を誰かに共有すれば、家族やレシピ作者さん以外にも、注目されたり褒めてもらえるチャンスがあるかもしれない。そんな風に、つくれぽを送る人の料理や生活がより楽しくなるチャンスを広げることが本施策のねらいです。

また、レシピ作者さんにとっても、つくれぽが外部に拡散されることによって、よりレシピを見てもらえるチャンスが増えます。 SNS上でつくれぽに出会うユーザは、美味しそうな料理写真をきっかけに、その作り方まで詳細に知ることができるようになります。 もちろんクックパッドは拡散されたつくれぽからクックパッドを訪れてくれるユーザが増えればバンザイです。

このように、各登場人物がwin-winの関係になるよう設計を行いました。

f:id:yoshiko-nagano:20150601101847p:plain

本施策のEOGS(Emotional Oriented Goal Setting*2

リリースと改善

本施策のリリースは以下のステップで段階的に進めました。

  1. つくれぽのパーマリンクをリリース
  2. 各種デバイス・画面にSNSシェアのリンクを順次追加していく
  3. ライトフィードバックをリリース

2の最初でスマートフォンWebにだけ導線を追加し、ある程度流入の傾向が見えてきたところで、一度パーマリンクのデザインを見直しました。 具体的には、FacebookからスマートフォンWebに流入した場合の直帰率が他に比べて高くなっていることが分かったので、そこを重点的に改善対象としました。

直帰率が上がっている原因として以下の仮説を立て、デザインを修正しました。

  • Facebookの場合、友人の投稿した料理写真に興味をもってクリックしている
  • レシピよりも投稿者に関心があるが、初期デザインのパーマリンクでは投稿者の他の投稿などがファーストビュー(および1スクロールくらいで見える範囲)で目に入らない
  • 結果、自分に関心のある情報に出会えず離脱する
f:id:yoshiko-nagano:20150601111004p:plain

パーマリンクデザイン改善(左:Before、右:After)

改善のポイントは以下です。

  • ファーストビューで投稿者の人感を感じられるようにし、その人の他の投稿に遷移しやすくする
  • 同時にレシピの位置は下げずに、興味を持った料理写真のレシピにはすぐアクセスできる状態を確保する

この変更の結果、問題となっていた直帰率はぐっと下がり、PCへの流入の場合や他のSNSからの流入とほぼ同等の数値にまで改善することができました。

このように細かく改善を行いながら、各デバイスでリンクの設置場所を増やし、現在は3のフェーズのライトフィードバック導入へと進んでいます。 拡散したものが見てもらえて、嬉しいフィードバックとして届くところまでがこの施策なので、今後も機能追加と改善を続けていきます。

日常の嬉しい体験を増幅するという考え方

本施策の本質的なゴールは、「レシピを見て美味しい料理が作れた!」という体験で得られる楽しさや喜びを、サービスを通じて増幅させることです。 日常生活で日々行われている作業をテクノロジーの力でより喜びの多いものにする。クックパッドでのサービス開発の根底には、常にこのような考え方があります。

そもそも、クックパッドというサービスの始まりも「レシピを検索する」よりも先に「レシピを載せる」ためのサービスでした。 毎日料理をしていても、褒めてくれるのは家族ぐらい。ともすればとても孤独な作業になりかねない料理を、レシピという形でインターネットに発信することで、遠いどこかの家庭で自分の料理が役に立つ可能性が生まれる。日々の生活に埋もれてしまっているけれど、実はとてもクリエイティブで誰かの役に立てるはずの作業を、ツールとプラットフォームを提供することで世の中にオープンにし、誰かの役に立つ喜びや人に褒められたり注目される喜びを作り出すのが、私たちのサービスの役割だと思っています。

もしこのエントリを通じて、クックパッドのサービス開発にご興味を持っていただけた方がいらっしゃいましたら、ぜひエンジニア・デザイナー採用へエントリーください。ご応募おまちしております!

*1:クックパッド上でレシピを見て料理を作ったユーザから、料理写真にコメントを添えてフォトレポートを送れる仕組み

*2:クックパッド内製のサービス企画フレームワーク。サービスの各登場人物の疑いようのない欲求を整理し、それらを満たす解を導く。企画時にこれをきちんと設定すると、サービスの登場人物と欲求が明確になり、開発の目的がぶれにくく、成功のイメージを共有できる。

Cookpad Apple Watch App 誕生の舞台裏

$
0
0

f:id:yskm39:20150603171838j:plain

買物情報事業部の三浦です。

クックパッドではApple Watchの発売に合わせて、iOSアプリをApple Watch対応にアップデートしました。 クックパッドのWatchアプリは、レシピを閲覧するという機能だけをもったとてもシンプルなアプリです。 実はそんな王道なアプリでも、いくつかの紆余曲折を経て完成させました。今回はその開発の経緯を通して、改めて実感したシンプルさやユーザー視点の大切さをお伝えできればと思います。

アプリの機能紹介

クックパッドのWatchアプリでできることは以下の2点のみです。 とてもシンプル。

  • 一番最後に見たレシピの閲覧(材料と手順のみ)
  • 調理時間から起動できるタイマー機能

f:id:yskm39:20150603115507p:plain

レシピ閲覧機能にした経緯

クックパッドでWatchアプリをつくるとなれば、「レシピを見る以外他にはないでしょう」と思われるかもしれません。 でも開発当初は「Watchでレシピを読まなくない?」、「レシピ見るならiPhoneでよくない?」という意見があり、私自身も最初はそう思っていました。

クックパッドではレシピ閲覧以外のサービス機能を提供しているので、それらに関連したものをつくろうと考えました。 いくつか上がったアイデアの中に、買物リストアプリというものがありました。クックパッドには買物リスト機能があり、レシピにある材料を買物リストにいれて管理することができます。 買物時は片手でかごを持ちながらもう片方で食材を選ぶ。その場面で、iPhoneをかばんから取り出して買物リストをチェックするよりも、手首にあるWatchでチェックする方ができ、ユーザー体験としてもWatchだからより便利になるアイデアだと感じ当初はこの企画を進めていました。

ユーザーのことを考える

しかし、WatchアプリはiPhoneアプリと一対の関係なので、一つしかつくることができません。企画を進めていく中で、クックパッドのユーザーが本当に期待しているもの、多くのユーザーが対象になるものは何かという部分にフォーカスして、今一度企画を考えなおしました。そうすると、やはり「クックパッド=レシピ」というところに戻ってきました。 レシピ x Watch で何かできないかを考え直しました。

f:id:yskm39:20150603115510p:plain

サブとして考える

ただ、iPhoneアプリのメイン機能であるレシピの検索やレシピを読むという体験は、Watchである優位性を出すことができません。 そんな中で、料理中というシーンだけにフォーカスをした時に可能性を感じました。

料理中のアプリの役割としては、材料や分量、次の手順など、ちょっとした確認作業で十分です。手のふさがる料理中であれば、Watchの優位性が出てきます。 料理中のちょっとしたことを手助けするツールとして、この料理中の機能だけ切り出してWatchアプリに担わせてはどうかと考えました。 それぞれデバイスの特性があるので、あくまでもiPhoneアプリをリプレイスするものではなく、サブとして役割分担をしてあげるとより効果的になりそうだと感じました。

f:id:yskm39:20150603115512p:plain

ユーザーストーリーを組み立てる

Watchアプリの開発にあたってユーザーストーリーをたてました。

ペルソナ

  • 名前: 石原 聡美
  • 年齢: 29歳
  • 性別: 女性
  • 職業: 専業主婦
  • 家族: 夫と2人のこども (6歳と3歳)

ストーリー

  • iPhoneで夕飯の献立を決めて、材料や作り方はざっと覚えて調理開始 → 台所は狭くiPhoneは少し離れたキッチンボードにおいている
  • 合わせ調味料の分量はどれくらいだったか?手元でさっと確かめた → Cookpad Watch Appを立ち上げて、材料を確認
  • 材料も切り揃って、まずは何から炒めるのか再度確認をした → 作り方を見て確認
  • 最後は具材に味がしみるまで5分計って煮た → 作り方のところにあるタイマーをタップして5分計る

これは一緒に開発したデザイナーの元山が作成しました。 このユーザーストーリーは後の開発で機能の取捨を迫られたり意見が食い違う場面でも、常にこの人が本当に欲しいものは何かという軸で議論することができたので焦点がぶれず、プロダクトをつくるときに非常に重要な役割になりました。

最近見たレシピ機能をもたせる

f:id:yskm39:20150603115515p:plain

iPhoneアプリには"最近見たレシピ"というレシピ履歴機能があります。 多くの場合、この履歴の中にレシピ決定されたものが入っていると想定されるので、この機能をWatchアプリにのせることに決め開発を進めました。

レシピの情報を最低限に

f:id:yskm39:20150603115516p:plain iPhoneアプリでのレシピ詳細は、レシピの検索、決定、料理中、料理後と様々なシーンで使われる情報を網羅しています。 これを料理中に必要な情報と限定すると、タイトル、写真、作者名、材料、作り方の5つの要素に絞られます。情報としてもとてもシンプルになります。 あくまでもその他の要素はiPhoneアプリで見てもらうという分担です。

f:id:yskm39:20150603115518p:plain

こちらがWatchアプリの詳細画面になります。作り方も、スクロールで一覧性を上げるよりも、料理中に自分がどこの工程にいるかがわかるように、ページングで1工程ずつ確認できるようにしました。

レシピ選択画面をどうするか

f:id:yskm39:20150603115521p:plain

履歴機能ということなので、レシピ選択をできる画面も用意しました。 上記が当初の画面遷移図の一部です。レシピ選択画面もページングで選択可能にしています。

この遷移だとタイマー画面を表示するには、レシピ選択→レシピ詳細→タイマー と二回モーダルで画面を表示させる必要があります。これを実装してみたところ、モーダルで表示された画面の左上に表示されるはずのOS提供の閉じるボタンが出ず、Watchではモーダルにモーダルを重ねることが想定されていないようでした。

これは困ったぞとなり、選択画面をリストビューにすることも考えましたが、リストビュー(hierarchical interface) x ページング(page-based interface)という遷移になり、これはApple Watch Human Interface Guidelines上でも推奨されていないスタイルになります。 タイマーのモーダルをやめることも選択肢にありましたが、開始/停止/リセットをレシピ詳細に入れてしまうのにもムリがありました。

そこで、いっそのことレシピ選択画面をなくそうという意見が挙がりました。

正直、最初はアプリとしてそんなミニマルな機能を提供するだけでいいのか、見た目上も素っ気なさすぎるのではないかと考えました。 でもチームで議論する中で、この考えは自分の作り手側の勝手な視点なのだと気付かされました。見た目やアプリとしてどうではなく、ユーザーストーリーに立ち返り、ユーザーが料理をするときに本当に必要な機能は何なのかを考え直しました。

シンプルであることを恐れない

チームで議論を重ねた上で、レシピ選択はiPhoneアプリ側に任せ、レシピ選択画面をなくすことに決定し実装しました。

結果的にとてもシンプルなアプリになりました。Watchは料理中のレシピの確認の役割のみを担う。Watchアプリとしては必要十分なものになりました。

その後の開発でタイマー機能の実装上、タイマーをどの画面でどう制御するかという課題にもぶち当たりましたが、その時もベースがシンプルであるがゆえに、シンプルな方法で解決することができました。このタイマー実装の部分は割愛しますが、Watchの制約の中でレシピ選択画面があるだけで、より複雑でユーザーを迷わせる部分が間違いなく出ていたと思います。

「シンプルであることを恐れない」

これは開発中にある方に言われた言葉ですが、今でも心に響いている言葉です。

まとめ

開発を通して学んだことは、以下3点です。

  • デバイスの役割を明確にする
  • 極力シンプルにする
  • 困ったときはユーザー視点にたつ

Watchの開発を通して、デバイスの特性を見極めながら、何をやらせて何をやらせないかを明確にすることが、とても重要でした。「あったら便利そう」は使われない可能性が高い機能。逆にその機能がユーザービリティを落とすことになりかねませんでした。特にWatchの場合は、デバイスや開発上の制約が多いので、極力シンプルにして、コア機能にしぼっていく必要があります。

そして、そんな制約の中、開発をしていくと、迷ったりチーム内で議論する場面が多く発生します。そんな時はいつも、企画当初に設定したユーザー視点に立って、その人が本当にやりたいことは何か、本当にその人が求めている機能なのかという軸で議論をすると、自ずと1つの方向へ集約してより良いプロダクトになっていくことを実感しました。

とても当たり前でシンプルなことですが、開発中はデザインや実装の都合、あるいは機能やサービスとしての事情など、どうしても作り手側の視点が混じり、軸がぶれてしまいます。

"誰のためのプロダクトなのか"

いつもその本質を問いながら開発を進めていくことで、よりよいプロダクトを開発していきましょう。

Dokumi (日本語)

$
0
0

(English version here)

技術部モバイル基盤グループのヴァンサン(@vincentisambart)です。今日は最近作ったツール「Dokumi」の話をしようと思います。

紹介

他部署のエンジニアの仕事をもっと楽にすることが、技術部の重要な目的の1つです。その中で、Dokumiはモバイル開発者のコードレビューの負荷を減らすためのツールです。

なぜ「毒味」という名前にしたかと言うと、人間がレビューする前に、コードに毒(バグ、不自然なコードなど)が入っているかどうか毒味するツールだからです。別の言葉で言うと、少し進化したCI用のlintツールですね。pull requestが出る度に、Jenkinsがそのpull requestにDokumiをかけます。現在はDokumiはiOSアプリだけに対応してしていますが、今後はAndroidアプリへの対応も考えています。

現時点でDokumiは以下のことをやっています。

  • 静的解析(Xcodeの「Analyze」機能)をかけます。 f:id:vincentisambart:20150603121414p:plain
  • 自動テストを走らせます。 f:id:vincentisambart:20150603121409p:plain
  • コミットされたけどXcodeのバージョンデータしか変わっていないXIB/Storyboardファイルを指摘します。 f:id:vincentisambart:20150603121400p:plain
  • pull requestにmilestoneが指定されていないとそれを指摘します。 f:id:vincentisambart:20150603121419p:plain
  • 静的解析や自動テストを走らせるためにアプリをビルドするので、ビルド中に出ている警告やエラーも同時に拾います。

以上のキャプチャーで見られるように、Dokumiは、GitHub Enterpriseと連動して、指摘をpull requestのコメントとして投稿します。指摘の対象がこのpull requestで変わる行であれば、行にコメントを付けます。対象が変わっていない行であれば、pull requestと関係ありそうなものは普通のコメントにまとめて投稿します。pull requestと関係なさそうなもの(例えばこのpull requestで変わっていないファイルに出る警告)は無視されます。

メリット

XcodeのTestやAnalyzeは、開発者が自分自身でかけることができますが、時間かかるし、かけるのを忘れる時もあります。また、 gitに変更を入れるのを忘れることもあります。なのでDokumiはすべてのpull requestに強制的にかけます。因みにXcodeの静的解析のお陰で人間がレビューしたコードでも不具合や変なコードを見つけることができたのでやってみることをおすすめします。

また、直接GitHub Enterpriseのpull requestにコメントを付けるメリットも大きいと思います。pull requestの度に自動テストを走らせるのは、以前から行われていました。しかし、失敗したテストを知るには、Jenkinsのログのxcodebuildの出力を見るしかなく、簡単ではありませんでした。

実は元々Dokumiの結果はウェブページとして整えようと思っていたけれど、良いデザインが思いつかなかったし、わざわざデザイナーに頼んでもな…と思っていたら、単に、普通の人間のレビューと同じようにすればいいだけだと気づきました。

困ったこと

GitHubのAPIを使って実装しようとした時に、特定の行にコメントを付けるには現時点(2015年6月頭)のAPIでは不足しているところがあると気づきました。

特定の行にコメントを付けるエンドポイントの説明を見ると、指定する必要あるのは「The line index in the diff to comment on.」です。ファイルの行番号ではなくdiffの中の行番号ですね。diffから計算するしかない…そしてこれだけだったらまだしも、そもそもAPIでdiffを取得する方法がない…

GitHubはpull requestのURLに.diffや.patchを付けるとdiffを取得できるのですが、そのdiffはpull requestのコメントに使われているdiffと相違があります:ファイル名変更の対応が違います。.diff/.patchで取得できるdiffはファイル名変更が:

  • 旧ファイル名の削除
  • 新ファイル名の追加

になります。pull requestのコメントで使われているdiffはファイル名変更が:

  • ファイル名変更自体
  • 変わっている行だけの追加・削除

になります。gitをコマンドラインで使うと、git diff--find-renames (略して-M) を付けるかどうかの違いと同じです。

GitHubのAPIで取得できるdiffに find-renamesを指定できないならどうすればいいのでしょうか。いまDokumiはレポジトリを手元でcloneして、Ruggedというライブラリを使ってdiffを計算します。もし計算のやり方がGitHubのと違っていれば、ズレが出てくる可能性がありますが、それでも大きな問題にならないでしょう。

pull requestでnew-featuremasterにマージしたいと仮定すると、僕の理解が合っていれば、pull requestのページに出ているdiffをコマンドラインで見たいときは:

$ git diff --find-renames `git merge-base new-feature master`..new-feature

質問コーナー

どの言語で実装されていますか? Rubyですね。僕自身も昔からRubyを使っていますし、社内にはRubyエンジニアが多いからです。

どうやって警告やエラーを拾っているのですか?主に正規表現を使ってxcodebuildの出力から拾っているだけです。静的解析の指摘はxcodebuild analyzeがビルドディレクトリに生成したplistファイルを見ています。

オープンソース化の予定はありますか?まだ未定です。どの環境でも動くようにするにはコストが高いと思いますし、他人が使えないものをオープンソースする意味もあまり感じないですね。でも実装に関する質問あればお答えします。

Dokumi (English)

$
0
0

(日本語版はこちらへ)

Let's talk about Dokumi, a tool I have been recently working on.

Introduction

I am part of Cookpad's "Technical Department". One of the department's goals is to make life easier for other engineers. I am in charge of iOS, so I wrote Dokumi to decrease the time that mobile engineers spend on code reviews.

In Japanese, Dokumi means "food tasting", or more literally "poison tasting" (for example, tasting food before a king to make sure the food is not poisoned). I named this tool "Dokumi" because it "tastes" the code to check if any "poison" (bug, strange code) is in it. In other words, it's an advanced lint tool intended to be run by your CI server. Every time a pull request is created or updated, Jenkins runs Dokumi on it. Currently, Dokumi only supports iOS apps, but we are thinking about adding Android support.

Dokumi currently does the following:

  • Runs a static analysis of the code (available in Xcode as "Analyze"). f:id:vincentisambart:20150603121414p:plain
  • Runs your automatic tests. f:id:vincentisambart:20150603121409p:plain
  • Points out the XIB/Storyboard that are changed by the pull request but for which only the Xcode version information changed. f:id:vincentisambart:20150603121400p:plain
  • Points out if the milestone of the pull request has not been set yet. f:id:vincentisambart:20150603121419p:plain
  • To run the static analysis or automatic tests, Dokumi has to build the application, so it also picks up the warnings and errors that occurred during the compilation.

As you can see in the screenshots above, Dokumi posts the issues found as comments to the GitHub Enterprise request. Issues on lines modified by the pull request are added as comments to those specific lines. Issues that are not on a line modified by the pull request but seem to be related are regrouped in one normal comment. Issues that do not seem to be related to the pull request (for example warnings found on a file not modified by the pull request) are ignored.

Merits

Any developer can run Test and Analyze in Xcode by himself. However, it takes time and it is easy to forget to run them. Also it's easy to forget to add some change to git. That is why Dokumi is run on all pull requests. By the way, thanks to static analysis, we did find problems in code that had already be reviewed by humans. So if you never used it, it might be worth a try.

I also think adding remarks directly to GitHub Enterprise's pull requests saves a non-negligible amount of time. Running automatic tests on each pull request was something we had been doing for a while. However, when tests were failing, you had to spend time finding the failure reason in the xcodebuild output inside Jenkins's log.

In fact, at first, I was thinking about putting all the errors in a nice web page, but I could not think of a design I liked, and I did not really want to bother a designer for that. That is where I realized that I could simply do it the same way humans review code.

Obstacles

When I tried implementing that idea using GitHub's API, I realized the current (as of June 2015) version of API makes it difficult to comment on a specific line.

Looking at the documentation for the endpoint to comment on a specific line, you have to pass it "The line index in the diff to comment on.". You first need to map file lines to diff lines. And to do that, first you have to solve an other problem: there is no way to get the diff you need via the GitHub API.

If you add .diff or .patch to a GitHub pull request URL, you can download its diff. However, that diff, and the one used for the pull request's comments, handle file renames differently. In the diff you get by adding .diff/.patch to the pull request URL, a rename becomes:

  • old file name deletion
  • new file name addition

In diffs used for pull request comments, a rename is:

  • file rename itself
  • addition/deletion of only the lines that changed

When using git from the command line, it is the same difference as passing --find-renames (shortened to -M) or not to git diff.

The GitHub API does not let you get the diff it uses for pull request comments, so what can you do? Dokumi currently clones the repository locally, and using the Rugged library, it generates its own version of the diff for the pull request. Of course if the way the diff is generated is different from the way GitHub does it, the line the comment ends up on might be incorrect, but even it this happens it should not be a big problem.

If a pull request is to merge new-feature into master, the way to display its diff from the command line is the following:

$ git diff --find-renames `git merge-base new-feature master`..new-feature

Q&A

In what language was Dokumi implemented in? Ruby. I have been programing in Ruby for a while, and at Cookpad we have many Ruby engineers, making it a good fit.

How do you detect warnings and errors? Mainly using regular expressions on the output of xcodebuild. For static analysis, the plist files generated by Xcode Analyze are also used.

Do you have any plan to open-source it? Not decided yet. It would be costly to make it work on most environments, and I do not feel it is really useful to open-source something that most people could not use easily. But I would be happy to answer any question about its inner workings.

チーム開発の進め方

$
0
0

f:id:Yoshiori:20150604175344p:plain

こんにちは!クックパッド編集室メディア開発グループ長の @yoshioriです。

今回はウチのチームの開発の進め方や見積もりの仕方を説明しようと思います。

実はコレ系の話は 5 年前にもデブサミで発表したのですがこの時はリリースまで 1 年とかのレベルのプロジェクトの進め方の話でした。今回は 1,2 ヶ月でリリースまで持っていく開発の進め方を説明します。

動画サービス部分を microservices 化するときに実際に行った事を元に説明します。開発者は 3 人で 1.5 ヶ月位の開発です。

何故このようなことを行うのか

誰だって楽しく仕事がしたいし、なるべく不安などは無い方が良いはずです。 例えば自分がやっている作業がどうなったら終わりなのかわかっていなければ不安でしょうし、いつまでに作ればいいのかわかっていなければ不安でしょう。

そういった不安をなるべく無くすためにうちのチームでは 見える化コミュニケーションを重視し、メンバー全員で楽しく開発することを目標にしています。

まずは作らなきゃいけないものの洗い出し

いわゆるタスク出しってやつですね。正直この作業はあんまり面白いものではないです。特に開発初期などはエンジニアはまっさらな状態からコードをドンドン書いていけて凄く楽しいのでいきなり手を付けたくなります。ですがチーム開発でそれをやってしまうと比較的早く破綻しますし、その時にはメンバーそれぞれのモチベーションにも差が出てきたりして軌道修正をするのが難しくなります。なのでメンバーの意識も高い初期になるべく楽しく行うのが大事だと思います。

まずは全員で時間を作りやらなきゃいけないタスクの洗い出しをします。 1,2 ヶ月で完成させるようなものはアジャイル開発でよく使われているユーザーストーリーで分けると粒度が少し大きくなってしまうため機能単位でタスク出しをしています。

どの程度の粒度で出すかというと

  • DB 設計
  • Video テーブル移行スクリプト
  • Video 取得内部 API

のような感じで出しています。 実際に全員で「もう出すもの無いよね」というところまで出し切ったらそれを一個づつ付箋に書いていきます。

出したタスクの重さを決める

f:id:Yoshiori:20150604175437p:plain

次に出したタスクの重さを決めていきます。ユーザーストーリーで分けているときはストーリーポイントとか言われているものですね。コレはなるべく抽象化したポイントであらわし、かかる時間などの具体的な数値で出さないことが大事です。以下説明のためにこの数値を SP (ストーリーポイント) と書きます。

実際の数値の出し方ですがプランニングポーカーと呼ばれている手法を使って行います。簡単にうちのチームで行っているプランニングポーカーの説明をします。

  1. 「1,2,3,5,8,13,∞,?」 どこかで見た数列 + 無限と ? のカードを人数分用意します。
  2. すべてのタスクの中からちょうど真ん中くらいの難易度だろうと思うのもをみんなで選びその SP を 5 にします。
  3. まだ SP を決めていないタスクを選びます。
  4. 簡単にそのタスクの機能の説明をします。
  5. それぞれがカードの中から SP を選び同時に出します。
    • 最初に出したタスクが 5 だというのを参考に考えます。
    • 専門分野など情報が不足しすぎていて自分には見積もりできない時は ? を出します
      • これはその後説明を求めます。
    • タスクが大きすぎて 13 を大幅に超えると思ったら ∞ カードを出します
      • タスクをさらに分割します
  6. 数値が合わなかったら一番大きい数字と小さい数字を出した人間がそれぞれの根拠を説明します。
  7. 6 で出た意見を参考にもう一度全員で SP を出します。
  8. 全員の数字が揃うまで 6,7 を繰り返します。
  9. すべてのタスクに SP が振られるまで 3 〜 8 を繰り返します。

これですべてのタスクに SP が振り分けられます。 プランニングポーカーは面倒臭い見積もりをゲーム感覚で楽しく行えるのでオススメです。

イテレーション期間

次にイテレーション期間を決めますが、もうコレは色々経験してきた結果

水曜日始まりの 1 週間を 1 イテレーションとする

が一番しっくり来るので僕の独断でそうしています。簡単に理由を説明すると

  • 1 週間より短いと細かすぎて朝会などと区別があやふやになる
  • 1,2ヶ月の開発に 2 週間だと 2 〜 4 イテレーションしか回せずうまくリズムにならない
  • 月曜日で振り返りを行うと先週の問題点があんまり気にならなくなってる
  • 金曜日に振り返りを行うと月曜日には改善しようと思ってたことを忘れる

という理由からそうしています。

スケジュールぎめ

1 週間というタイムボックスが決まったので、それぞれのイテレーション期間でどのタスクを行うかを決めていきます。 最初はだいたいザックリ「今週はこのくらい出来るんじゃね?」という量を割り当てていき、次の週などもそれを元に割り振っていきます。この時に大事なのはタスクの順番を意識することです。例えば最初に例に上げた「DB 設計」を終わらせないと「Video テーブル移行スクリプト」は実装できません。こういった順番を考慮しつつ各イテレーションに作業を割り振って行きます。(後で説明しますがココで上げたスケジュールは大体破綻しますw)

実際に開発を回していく

f:id:Yoshiori:20150604175516p:plain

スケジュールも決まったので実際に開発にかかります。うちのチームでは基本的に個々の作業管理はツールとしてのかんばんで行っていますが少しアレンジしています。

  1. タスク自体はイテレーションを表した大きめの紙に貼られている
  2. 自分が実際に作業しているものには自分の名前の書かれたマグネットを置く
  3. 終わったものには猫のシールを貼る
  4. 予定になかったタスクが発生したら赤い付箋に書き、そこにも SP をふる
    • 突然降ってきた仕事
    • 仕様考慮漏れ
    • バグ修正

タスクが終わったものはシールを貼って表していますがコレはその作業が完全に完了するまで行わないことを徹底しています。(アジャイルで「完全 Done 」と呼ばれているものですね)

振り返りを行う

水曜日には振り返りを行います。まず計画で上げたタスクで完全に完了したものの SP をすべて合計して算出します。 コレがチームの 1 週間で行える作業量という指針になります。ついつい「この 8SP の作業、あとちょっとで終わるから」と言って合計数に含めたくなりますがやめましょう。

さて、だいたい最初のイテレーションが終わるとすでに最初の計画が破綻していると思います。 今回うちのチームも 1 イテレーションで 30SP 位の作業は出来るだろうと見込んでいたのですが、 20SP しか完了出来ませんでした。つまり 10SP の作業はあぶれ、今後のイテレーションに割り振られている 30SP をすべて 20SP に修正すると一気に 1.5 倍ほどのスケジュールになってしまいます。

ここで大事なのが振り返りです。

まずは我々は1イテレーションに 20SP しか消化できないという認識をします。 そこで何故 20SP しか消化出来なかったのかなどを話し合います。今回は 3 人なので KPT のような比較的きっちりした振り返りではなくスタンディングでお互いの意見を言い合う形で行いました。 最初のイテレーションはデータ移行などのタスクに引きづられ、大きめなタスク( 13SP )が実際にデータを入れてみたら修正する箇所が出てきたりして完全 Done にならなかった事がわかりました。 幸いにもその修正はすぐに終わるだろうという事で今回はスケジュールの見直しはしないで行けそうだということになりました。

ここで厳密に今後のスケジュールを見直すことも可能ですが、いきなりここで厳密にやってもテンションが下がるだけなので行いませんでした。実際、本当に持ち直せなかったら次のイテレーションでスケジュールの切り直しをするつもりでしたが、うまく持ち直したのでそのまま進みました。

イテレーションを回していく

上記のようにイテレーションを回しながら

  • 抜けているタスクはないか
  • タスクに割り振られている SP は妥当か
  • イテレーションに割り振られているタスク量は適切か
  • スケジュールを切り直す必要はないか

は話し合っていき、スケジュールをドンドン正確にしていきます。

チームのパフォーマンスチューニング

f:id:Yoshiori:20150604175552p:plain

このようにイテレーションを回していくとベロシティ(チームの平均消化 SP)がわかってきます。あくまで指針としてですがこのような数値を毎週出しておくとチームの健康状態を図れます。ベロシティが落ちていればなにか問題があるのでしょう、逆に上がっていれば何か改善する事があったのでしょう。

振り返りの時に「なんか今週仕事があんまり進まなかった気がするよね!なんでだろ?」と感覚的にいうよりも「何故今回はベロシティ下がったんだろう?」というほうが数字にも出てるのでチームメンバーも共感でき、一緒に改善案を考えれます。

パフォーマンスチューニングの基本「推測するな、計測せよ」です。

まとめ

チーム開発の進め方として主にタスク出しやスケジュールの切り方を説明してみました。僕はもともと「スケジュール立てるのとか苦手だし、誰か得意な人がやればいいんじゃね?」とか思っていましたが今では

タスク出しやスケジュール立てるのは才能ではなく技能なのでやり方覚えれば誰でも出来る

と感じています。実際にやっていることはプログラムを書くときと同じで大きい問題を小さく切り分けて順番に対処していっているだけです。そして計測してそこから逆算しているだけです。

やり方を覚えて身に付ければ誰にでも出来る事なので、まだ習得していない人や昔の僕の考えと同じようなことを考えている人は早めに習得しちゃうことをオススメします。

アジャイルな見積りと計画づくり ~価値あるソフトウェアを育てる概念と技法~

アジャイルな見積りと計画づくり ~価値あるソフトウェアを育てる概念と技法~

  • 作者: Mike Cohn,マイクコーン,安井力,角谷信太郎
  • 出版社/メーカー:毎日コミュニケーションズ
  • 発売日: 2009/01/29
  • メディア:単行本(ソフトカバー)
  • 購入: 74人 クリック: 764回
  • この商品を含むブログ (225件) を見る
Viewing all 726 articles
Browse latest View live