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

月間1000万PVを支える「UIの言葉選び」のためのチェックリスト

$
0
0

クックパッド ダイエット事業室の田中です。昨年5月からスタートした「クックパッド ダイエット」にリリース当初から携わり、デザインやダイエットニュースの編集を担当しています。 現在クックパッドダイエットのサイトは月間1000万ページビューを超え、「ダイエットといえば『クックパッド ダイエット』」と言われるような世界を目指して、日々、運用・改善に取り組んでいます。

今回紹介するのは、クックパッドダイエットのUIをデザインする時の「UIの言葉選び」の具体的なチェックリストです。

最高のレイアウトでも言葉がイマイチだと台無しに

みなさんは、UIの中の「文言(言葉)」をどのようなプロセスで決定していますか?

UIのレイアウトや遷移の方法について熟考する姿勢を持っている方は多いと思うのですが、文言の検証方法については確固たる視点を持っていない方もいらっしゃるのではないかと思います。

UIで王道のレイアウトや、利用頻度の高いUIパーツを選択したとしても、そのUIにのせる「文言(言葉)」が、ユーザーの期待に沿ったものでないと「使いづらい」と思われてしまうことがあります。ユーザーは一瞬の判断で次にどのボタンを押すべきか判断します。その一瞬の短い間に、そのUIを「理解してもらう」または「期待をもって次を見たいと思ってもらえる」必要があります。

チェックリストで「UIの言葉選び」の問題を発見する

ユーザーテストをする前に、簡単にチーム内で文言の是非をチェックできるように、基本的な事柄ですが気をつけると良い項目をチェックリストとしてまとめました。

1. 文字量は多すぎないか

内容をアピールしたいあまりに、説明文章のように文字が多いメニューやボタンになってしまうことがあります。 冗長になってないか?という疑いを常に持って、例えば、2秒以内に見つけられるかというテストをします。

2. 文字量は少なすぎないか

すっきりしたサイトを目指すあまりに、逆に説明不足な言葉になってしまうことがあります。見た目は美しくても、 次のページに何があるのかという想像がわかなければ、押してもらえません。ユーザーインタビューなどで、「ここを押したら何がでると思いますか」等の質問をはさむようにしています。

3.読みやすい漢字か(難しければひらがなにするか、漢字にする必要があればルビをふる)

何かに特化したサイトを制作していると、ターゲットとしているユーザーとデザイナー自身の知識が乖離してしまうことがあります。例えばダイエットサイトの場合だと、私が「華奢見せコーデ」という文言を選んで表示した時に、同僚から「読みづらい」という指摘をうけました。こういった問題は、隣のチームの人に見てもらうだけでだけでもかなり違うと思います。「華奢」については、ルビを振ることで対応しました。

4.重要なキーワードを英語で表現していないか?

日本人向けのデザインでは、英字を雰囲気を作るための「ビジュアルデザイン」として採用することがあり、ユーザーは英字を読み飛ばす傾向があります。どうしても英字を使用したい場合は、 雑誌などでよくあるような、英字と日本語両方を並べて配置することを検討すると良いと思います。

5. 他のページで名称が異なっていないか

サイトの改善を繰り返していると、同じことを指しているのに、名称がばらばらになることがあります。漢字とひらがなの違いにも注意しましょう。例えばダイエットのサイトだと、「痩せる」と「やせる」についてどちらを採用するか検討したことがあります。バナーなどでコンテンツを魅力的に見せるために、またはSEO対策として、あえて別の言葉を使うことはあると思いますが、UIについては基本的には同じ機能は同じ文言でリンクしても良いと思います。 また、日付の表記・時間の表記も、統一できているかどうかを見落としがちな項目です。

6.別の機能を同じ名称にしていないか

逆に、サイトが大きくなってくると、違う機能やページなのに、似た名称になり、同じ文言でリンクをしてしまうことがあります。特にウェブサービスの場合は、どのページからユーザーの回遊がスタートするかわかりません。どのページからでも目的の1ページにたどり着けるように注意しましょう。   

7.ユーザーの期待に沿っているか

これは意外と見落とされがちな項目で、「分かりやすい」だけではクリックされないことがある、という話です。例えばダイエットサイトでは、「ニュース」というコンテンツがありましたが、「やせる情報と知識」に変更したところ、クリック率が上がりました。ダイエットに興味を持っているユーザーの期待するキーワードがUIやボタンに入れられると良いようです。期待するキーワードの候補を出すために、例えばgoogle Adwordsのキーワードプランナー等で検索ボリュームや組み合わせ候補について調べることができます。

8.ページ全体のバランスを考慮しているか

一部だけ難しい言葉になっていたり、トーンが変わっていないか確認します

9.初見の人が理解できない言葉を使っていないか(無意味な造語になっていないか)

サービスのオリジナリティを模索するあまり、機能やページ名が、分かりづらい造語になってしまうことがあります。例えばクックパッドでも、「つくれぽ」という言葉がはじめてクックパッドを見た人には分かりづらいという話があり、現在は「つくれぽ(つくったよフォトレポート)」とできるたけ併記するようにしています。どうしても言葉選びに悩む場合は、類似サービスをいくつか見て一般的にはどう言い換えるかを参考にします。

10.SEOに強い文言か

画像でUIを作成する場合でも、リスト要素やaltタグとして、文言を入力することができます。ユーザの期待に沿うことと近いですが、同じように検索されやすいキーワードを考慮するという視点を持つことで、より良い言葉選びができると思います。

11.ユーザー名を活用できているか

重要でどうしてもユーザーにぜひ目に留めてもらいたいところでは、「○○さん」というようにその人のユーザー名を表記して呼びかけるようにすると効果的です。人混みの中でも、自分の名字を呼ばれるとはっとするように、UIの中でも自分のユーザー名が表示されていると「自分に関係があるコンテンツかな」と目に止まりやすくなります。マイページなどにユーザ名を置くことは一般的ですが、緊急性の高いものや、パーソナライズした重要な情報などに利用できると思います。

おまけ:実際にテストしてみる

文言を変更した時は、簡単にでも良いので、結果を確認できるようにしておきます。

例えばトップページの離脱率を減少するために、グローバルナビゲーションの変更を行ったことがありました。 ユーザインタビューから、文字が多いという意見があったため、冗長すぎるのではということで思い切って文言を削除しました。

結果、ナビゲーションのクリック率は全体的に減少しましたが、直帰率が6パーセント減少しました。ページ全体のバランスを考慮するという点から、ナビゲーションの文字量を減らすことで、下のコンテンツに目を向けてもらえるようになりました。

UIの中の文言は、小さな要素に見えますが、ほとんどのユーザーが目を通し、次の行動を決める重要なものです。特に項目7~11は「環境・文脈」という視点からのチェックポイントで、その影響力は強く、ケースバイケースで最適な文言は変わってくると思いますが、気に留めておきたい項目だと思います。 クックパッドも、クックパッド ダイエットも、まだまだ良くしていきたい・良くしていくべき箇所があります。ユーザの幸せが増すように、一緒に考えてつくってくださる方を募集中です!クックパッドの仕事に興味がある方はご一報ください。


新コンテンツを作る際のSEO施策の効果評価とクックパッドのSEO

$
0
0

クックパッド検索・編成部の五十嵐啓人です。今年から、クックパッドの検索エンジンの価値を最大化するミッションを担当しています。今回のエントリでは、昨年まで担当したエディトリアル部門での新コンテンツ立ち上げのSEO事例についてお話します。

離乳食や夏休みの自由研究コンテンツを通じたコンテンツ集客事例

近年クックパッドはレシピだけでなく、料理周辺の課題を解決するコンテンツを集めることに注力しています。昨年その中の小さな施策のひとつとして、夏休みの自由研究に料理を取り入れることを提案する「自由研究」、離乳食期の食の課題解決を狙った「クックパッド ベビー&ママ」という二つのミニチャンネルを公開しました。

いずれも、クックパッドの数千万人の利用ユーザー中、「小中学生を持ち自由研究に課題をもつユーザー」「乳児食を探したいユーザー」は相当に限定されます。そのため、情報が必要な人にコンテンツを届けるために、SEOが効率のよい集客方法として予想されました。

※クックパッド自由研究画面イメージ f:id:flar:20150310101734p:plain

※クックパッド ベビー&ママ画面イメージ f:id:flar:20150310101741p:plain

新コンテンツ進出時のSEO事例 〜コンテンツやワーディングの選択と効果分析手法〜

利用者がコンテンツにたどり着く際の情報の探し方を熟考

SEOを単にランキング上昇のテクニックと考えている方もいますが、SEOで最も重要なのはコンテンツを探す人と、提供者のギャップを埋めることです。そのため、情報を探す人のコンテンツの探し方を調べ、それにあったコンテンツを用意する必要があります。また、情報をどのくらいの人が探しているのか(または潜在的な需要があるのか)ということを見誤ると、徒労に終わるためこちらの見極めも重要です。

ターゲットが明確であれば、これは非常に簡単です。自由研究を例に取ると、コンテンツ名称と課題が同じため、多くの人が情報を探す際に利用する最重要キーワードは「自由研究」となります。

「自由研究」の課題を解決をしたい人のボリューム推定は、自分が規模を把握できているキーワードと対比させることでGoogle Trendから推測できます。

今回は当社で一番流入が増加するバレンタインと比較しました。下記グラフのとおりバレンタインを下回るものの、それなりの需要が期待できることが推測できます。 f:id:flar:20150310101906p:plain

また、自由研究に興味がある人が、どのような課題を抱えているかも把握が必要です。 一番簡単に調べる方法としては、検索エンジンの関連キーワード候補を見る方法があります。 f:id:flar:20150310101916p:plain

これを見ると自由研究の「ネタ」を探したいこと、また様々な「ネタ」情報がある中で「小学1年生」「6年生」「中学生」などに情報をフィルタしたいことがわかります。これらの情報をもとに、計画していたコンテンツの内容や、「自由研究」で検索した場合の競合のコンテンツ特性などを含め方向性を決定しました。その結果「主に小学生で夏休みに、自由研究のネタ出しに困っている人に向け、料理観点から自由研究の題材を提供する」というコンテンツで他社と差別化し、サイト内の各種ワーディングやコンテンツ構成でもこれらのキーワードに表記を統一しました。

Ginzametricsを利用した注目キーワードの追跡

SEOと集客上の関係を見える化するため、当社では、Ginzametricsというツールを利用しています。Ginzametricsは非常にマニアックなツールですが、指定したキーワードの順位監視や競合の情報などをわかりやすく算出してくれる強力なサービスです。

今回も、事前に当社で定めたターゲットキーワードの他、「自由研究」関連でボリュームの多いキーワードを事前にGinzametricsのキーワード監視リストに追加しています。

監視に追加したキーワードとそのねらい

# =>ビッグキーワード
自由研究  

# => 2語かけあわせでボリュームが大きく、特にフォーカスしたもの
自由研究 ネタ 
自由研究 小学生
自由研究 夏休み
自由研究 実験

# => 2語かけあわせでボリュームが大きいが、フォーカスはしていないもの
自由研究 中学生 
自由研究 まとめ方
自由研究 テーマ

# => コンテンツ制作に当たり著名人を起用したため
パックン
パトリックハーラン

# => クックパッドが自由研究コーナーを作ったということを知っている人のためのナビゲーショナルクエリ
自由研究 クックパッド

# => ボリュームは少数だが最低限どこの競合にも負けたくないキーワード 
自由研究 料理 

f:id:flar:20150310101921p:plain

SEOでの流入が獲得できるまでどのくらい時間がかかるか?

一般的に新コンテンツを立ち上げて検索エンジンに評価されるまでどのくらいの時間がかかるのでしょうか?残念ながら具体的な流入数までは公開できませんが、「自由研究」を立ち上げた際の、検索順位、集客数、またキーワード検索数のボリューム推移をGoogleTrendから取得した数字をプロットしたのが下記のグラフです。

f:id:flar:20150310101925p:plain

※青棒はGoogle Trendから取得した「自由研究」の同期間中のキーワードボリューム比率(一番下を0、一番上を最大の100)。緑線は、今回ターゲットに設定したそれぞれのキーワードの平均検索順位です(一番下は50位または圏外、一番上は1位)。 赤線及び、オレンジ線はコンテンツにランディングした流入数の推移です(自然検索以外の流入も一部含みます)。

コンテンツが公開されたのは7/23ですが、検索順位が最も高くなったのは8月下旬となりました。コンテンツ追加やワーディング・タイトル調整、リンク構造などの施策を続ける中で、流入が極大化されるまで1カ月近くを要したことになります(集客施策としては、あと1月早く施策を準備をしていればもっと成果を挙げられたということがわかります)。なお、離乳食向けの情報を提供する「ベビー&ママ」でも、最終的な順位上昇やトラフィックに大きな変化が見られるまでに1カ月近くを要しました。その他、こちらのグラフからは検索ボリューム数と、順位、そして流入の3つの関係など非常に面白い情報が読み取れると思います。

また、以下はUSの調査会社Caphyonが公開している、Googleの検索結果ランキングごとのCTRです。これをみると上位10位以内では、ランキング変動があると、流入数も変化がありそうですが、#10以下では多少順位変動があっても流入数の変化を観測するのは困難であることが伺えます(なお類似の調査は他にもあり、この結果はその中でもかなり楽観的な数字です)。

f:id:flar:20150310101927p:plain

この施策ではGinzametrisを利用して、1ヶ月の間、総合的な検索ボリューム、検索順位、流入数の3つの指標をトータルで把握することで、冷静にSEO施策の効果分析を進めていくことができました。いずれの情報が欠けても、Googleという非公開情報を相手に、粘り強く施策を打つのは心が折れてしまったことでしょう。

f:id:flar:20150310101929p:plain

クックパッドのSEO体制

今回当社でのSEO事例を紹介しましたが、今回のように集客でSEOを強く意識するサービス運用事例は当社ではあまり事例がありません。普段クックパッド内でSEOがどのような体制で実施されているか、最後に簡単にご紹介しましょう。

当社は検索エンジンから大きな集客を得ており、SEOが比較的成功しているサービスのひとつとされていますが、社内で専属のSEO担当者はおりません。Google事情に比較的詳しく、サービス利用者数増加に責任を持つ私がサポートをすることもありますが、基本的には各現場でできる「テクニックに依存しない本質的なSEO」を志向しています。そのため、当社が重視するのが下記の2点です。

  1. 何れかの分野でNo.1になるコンテンツを集める、作る
  2. ユーザーファーストなサービスであることを自問し続ける

当社の強みは「レシピ数が多いこと」と言われますが、これを生み出すのは「楽しみながらコンテンツを寄せるユーザーのコミュニティ形成」です。各サービス担当者にはコミュニティ形成が競争力の源泉であるという考えが企業文化として共有されています。ユーザーが楽しめるサービスの開発を目指した結果として、競合に負けない量や質、更新頻度の高いコンテンツが生み出され、結果的にクローラーにも高く評価されます。

また、ユーザーが使いやすいサービスを作ることは結果的に、GoogleのSEO評価指標を高めることとも合致します。リンク構造の最適化やページロード時間の短縮、コンテンツ以外の広告などの要素でページを埋め尽くさない、わかりやすいタイトルをつける、どんな環境でも同じ内容が表示できるようにするといったことはその一例でしょう。

当社も細かいSEOテクニックを使っていないわけではありません。また、コード変更の際のGithub上でのPull Requestに目を光らせ修正をする場合もあります。しかし、SEOテクニックによるページの価値向上は、上記のような本質的なサービス作りを実施した上で、なお必要なときに限って行えば良いと考えています。専任の担当者がいればもっと良くなるところはたくさんありますが、過剰にSEOを意識した業務せずに、結果的にクローラーに評価されるサービスを作れる文化形成に努めています。

まとめ

いかがでしたでしょうか? 昨年の実例を通して、SEO集客を前提としたコンテンツ作成時の、効果分析について手法や、必要な期間など、施策の進め方のイメージをお持ちいただけたと思います。いずれも基本的な内容だと思いますが、実際のワークフローと施策評価も含め公開されている事例は少ないため、読者の方の業務の何かヒントになれば幸いです。

クックパッドでは、さらにユーザーさんの料理の課題を解決できるサービスになれるよう、様々なコンテンツを作り・集め、ユーザーさんが探しやすい形にアレンジし、提供していきたいと考えています。

Elasticsearch を使った位置情報検索

$
0
0

ホリデー事業室の内藤です。

ホリデー事業室は昨年の4月に発足した部署で、Holiday(https://haveagood.holiday)という新規サービスの開発を行っています。

Holiday とは、クックパッドが長年取り組んでいる「毎日の料理を楽しみにする」分野からは少しだけ離れ、「いつもの休日を楽しくすることで人生を豊かにする」ことを目指したサービスです。

例えばこちらのおでかけプランのように、「〇〇に行くならここも行ったほうがいいよ」や「〇〇を散策するならこのコースだよね」など、おでかけのレシピを投稿したり探すことができるようになっています。

今回は、全文検索エンジン Elasticsearch を使って、全文検索と位置情報を絡めた検索についてお話したいと思います。

本稿で説明する内容は、実際に Holiday の中でも応用を加えた形で使われています。

Holiday では、複数のおでかけスポットを組み合わせて一つのおでかけプランを作ることができます。 例えばそのおでかけプランの作成画面では、登録するおでかけスポットを選ぶ時に、京都のおでかけプランなら京都のスポットを優先的に掲示することで、スムーズなプラン作成を可能にしています。 f:id:qtoon:20150310133959p:plain

また、おでかけプランの詳細画面においては、緯度経度情報を加味したおでかけプラン同士の関連性を計算し、関連度の高いものを合わせて表示することで、週末のおでかけ先を探している人がより行き先を決めやすくなるような仕組みを作っています。

このような位置情報を応用した様々な機能が、Elasticsearch を使うことによって簡単に実現できます。

なお本稿に含まれるサンプルコードは、GitHub 上で公開しています。
https://github.com/9toon/es-geo-sample

前準備

まず始めに、必要なデータを用意します。 今回は、名称・住所・緯度経度の情報を持つおでかけスポットを用意します。

テーブル定義は以下のようになります。

# import.rb

create_table(:spots) {|t|
  t.string :name
  t.string :address
  t.decimal :lat, precision: 9, scale: 6
  t.decimal :lon, precision: 9, scale: 6
}

このテーブルに、いくつかのスポット情報を登録します。

# import.rb

spots = [
  { name: '清水寺',     address: '京都府京都市東山区清水1-294',     lat: 34.994401, lon: 135.783283 },
  { name: '京都御所',   address: '京都府京都市上京区京都御苑3',     lat: 35.025414, lon: 135.762125 },
  { name: '八坂神社',   address: '京都府京都市東山区祇園町北側625', lat: 35.003634, lon: 135.778525 },
  { name: '金閣寺',     address: '京都府京都市北区金閣寺町1',       lat: 35.039381, lon: 135.729230 },
  { name: '北野天満宮', address: '京都府京都市上京区北野馬喰町',    lat: 35.030428, lon: 135.735327 },
  { name: '清水寺',     address: '神奈川県海老名市国分北2丁目',     lat: 35.460435, lon: 139.398696 },
  { name: '清水寺',     address: '群馬県高崎市石原町2401',          lat: 36.309917, lon: 138.989039 },
  { name: '清水寺',     address: '岐阜県加茂郡富加町加治田985',     lat: 35.498399, lon: 136.997405 },
  { name: '清水寺',     address: '愛知県東海市荒尾町西川60',        lat: 35.028889, lon: 136.911644 },
]

spots.each do |spot|
  Spot.find_or_create_by!(spot)
end

次に、Elasticsearch のインデックス・スキーマを定義します。

nameおよび addressは、日本語での全文検索を行いたいので analyzerkuromojiを指定します。 また、位置情報と絡めた検索機能を利用するために、locationというフィールドを作り geo point typeを指定します。

ただし、データベース内では緯度経度情報を lat, lonという形式で保持しているので、Elasticsearchにデータを流し込む際には geo_pointタイプの書式に合うように加工する必要があります。 elasticsearch-modelでは as_indexed_jsonメソッドを使うことで、Elasticsearch に渡すデータをカスタマイズできます。

# spot.rbclassSpot< ActiveRecord::BaseincludeElasticsearch::ModelincludeElasticsearch::Model::Callbacks

  mapping do
    indexes :id, type: 'string', index: 'not_analyzed'
    indexes :spot_name, type: 'string', analyzer: 'kuromoji'
    indexes :address, type: 'string', analyzer: 'kuromoji'
    indexes :location, type: 'geo_point'enddefas_indexed_json(options = {})
    { 'id'        => id,
      'spot_name' => name,
      'address'   => address,
      'location'  => "#{lat},#{lon}",
    }
  endend

Elasticsearch にデータを流し込む準備ができたので、実際にドキュメントを登録します。

# import.rbSpot.import(force: true)

ここまでで、前準備は終了です。

なおサンプルでは、以下のコマンドを打つことで、ここまでの処理を行うことができます。

$ bundle exec ruby import.rb

距離順での並び替え

登録しているおでかけスポットを、ある地点からの距離順にソートしてみます。 今回は、阪急京都線河原町駅(lat: 35.003765, lon: 135.769463)からの距離が近い順に並び替えてみます。

以下のように設定することで、距離順に並び替えることができます。

# spot.rbclassSpot< ActiveRecord::Baseclass<< selfdefsort_by_distance(lat, lon)
      body = {
        sort: {
          _geo_distance: {
            location: {
              lat: lat,
              lon: lon,
            },
            order: 'asc',
            unit: 'meters',
          }
        }
      }

      Spot.__elasticsearch__.search(body)
    endendend

_geo_distanceの中では、中心点を定める location、ソート方向を定める order、そして単位を定める unitを指定しています。

なお今回のサンプルでは、ソート処理や絞り込み処理が適切に行われたかどうかを確かめやすくする目的で、各スポットと中心点との距離を算出する scriptを含めることにします。 この scriptを含めたメソッドの全体像は以下のようになります。

# spot.rbclassSpot< ActiveRecord::Baseclass<< selfdefsort_by_distance(lat, lon)
      body = {
        sort: {
          _geo_distance: {
            location: {
              lat: lat,
              lon: lon,
            },
            order: 'asc',
            unit: 'meters',
          }
        },
        script_fields: calc_distance_script(lat, lon),
      }

      Spot.__elasticsearch__.search(body)
    endprivatedefcalc_distance_script(lat, lon)
      { distance: {
          params: {
            lat: lat,
            lon: lon,
          },
          script: "doc['location'].distance(lat,lon)", # 点[lat, lon] からの距離をメートル単位で算出
        }
      }
    endendend

scriptに関する詳しい内容は、公式ドキュメントの scriptingを読むと詳しく記載されています。

では実際に先ほど登録したデータで試してみます。

$ bundle exec pry

> require'./spot.rb'
=> true> spots = []

> Spot.sort_by_distance(35.003765,135.769463).records.each_with_hit { |record, hit| spots << { name: record.name, address: record.address, distance: "#{hit.fields.distance.first.to_i}m" } }

> spots
=> [
  {:name=>"八坂神社",   :address=>"京都府京都市東山区祇園町北側625", :distance=>"1008m"},
  {:name=>"清水寺",     :address=>"京都府京都市東山区清水1-294",     :distance=>"1858m"},
  {:name=>"京都御所",   :address=>"京都府京都市上京区京都御苑3",     :distance=>"2544m"},
  {:name=>"北野天満宮", :address=>"京都府京都市上京区北野馬喰町",    :distance=>"4821m"},
  {:name=>"金閣寺",     :address=>"京都府京都市北区金閣寺町1",       :distance=>"5981m"},
  {:name=>"清水寺",     :address=>"愛知県東海市荒尾町西川60",        :distance=>"127177m"},
  {:name=>"清水寺",     :address=>"岐阜県加茂郡富加町加治田985",     :distance=>"147367m"},
  {:name=>"清水寺",     :address=>"群馬県高崎市石原町2401", :        :distance=>"386772m"},
  {:name=>"清水寺",     :address=>"神奈川県海老名市国分北2丁目",     :distance=>"407190m"}
]

このように、中心点の河原町駅からの距離順に並んでいることが分かります。

中心点からの距離で絞込む

では次に、中心点から一定の範囲内にあるおでかけスポットのみを取り出してみます。 今回は、河原町駅から10km(10000m)以内にあるスポットのみを取り出します。

中心点から指定の半径内に収まるデータを絞り込むには、geo_distanceフィルターを使います。 elasticsearch-modelを使った場合、以下のように書くことで、このフィルターを使うことができます。

# spot.rbclassSpot< ActiveRecord::Baseclass<< selfdefspots_in_range(lat, lon, radius = 10000)
      body = {
        query: {
          filtered: {
            filter: {
              geo_distance: {
                location: {
                  lat: lat,
                  lon: lon,
                },
                distance: "#{radius}meters",
              }
            }
          }
        },
        script_fields: calc_distance_script(lat, lon),
      }

      Spot.__elasticsearch__.search(body)
    endendend

geo_distanceの中では、中心点を定める locationと、そこからの半径の長さを表す distanceを指定します。

これを実行すると、以下のような結果が得られます。

$ bundle exec pry

> require'./spot.rb'
=> true> spots = []

> Spot.spots_in_range(35.003765,135.769463, 10000).records.each_with_hit { |record, hit| spots << { name: record.name, address: record.address, distance: "#{hit.fields.distance.first.to_i}m" } }

> spots
=> [
  {:name=>"金閣寺",     :address=>"京都府京都市北区金閣寺町1",       :distance=>"5981m"},
  {:name=>"北野天満宮", :address=>"京都府京都市上京区北野馬喰町",    :distance=>"4821m"},
  {:name=>"清水寺",     :address=>"京都府京都市東山区清水1-294",     :distance=>"1858m"},
  {:name=>"京都御所",   :address=>"京都府京都市上京区京都御苑3",     :distance=>"2544m"},
  {:name=>"八坂神社",   :address=>"京都府京都市東山区祇園町北側625", :distance=>"1008m"}
]

このように、全て河原町駅から10km以内に位置するスポットのみを抽出することができました。

より柔軟な位置情報検索

では最後に、少しだけ応用的な使い方として、スポット名および住所での全文検索と位置情報を組み合わせた検索を行います。

いくつかやり方はありますが、今回は function_score_queryを使ってこれを実現しようと思います。

実際にサービスを運営していると、検索結果の並び順をどうするか決めるためには様々な要因を考慮する必要があることに気が付きます。

例えば、現在地周辺のおでかけスポットを検索する機能を作るとすれば、

  • 現地までの距離
  • 検索クエリとの関連度
  • 人気度

等々を考慮する必要があるでしょう。

そのため、距離順など一つの基準で並び替えてしまった場合、人気がないスポットや検索クエリとの関連が低いスポットが上位に並んでしまうということになりかねません。 この問題を解決するためには複数の条件で並び替えの順序を決める必要がありますが、その際に function_score_queryが活躍します。

function score queryは、functionsセクションに複数の条件(function)を記載でき、それぞれの条件でのスコアを合算した値を使ってソートを行います。

今回はサンプルとして、スポット名および住所で全文検索を行い、その結果を 検索クエリとの関連度距離を考慮して並び替えたものを返すメソッドを作りました。

# spot.rbclassSpot< ActiveRecord::Baseclass<< selfdefsearch_by_keyword(keyword, lat, lon)
      body = {
        query: {
          function_score: {
            score_mode: 'multiply',
            query: {
              simple_query_string: {
                query: keyword,
                fields: ['spot_name', 'address'],
                default_operator: :and,
              }
            },
            functions: [
              {
                filter: {
                  query: {
                    simple_query_string: {
                      query: keyword,
                      fields: ['spot_name'],
                      default_operator: :and,
                    }
                  }
                },
                weight: 5
              },
              {
                filter: {
                  query: {
                    simple_query_string: {
                      query: keyword,
                      fields: ['address'],
                      default_operator: :and,
                    }
                  }
                },
                weight: 2
              },
              {
                gauss: {
                  location: {
                    origin: {
                      lat: lat,
                      lon: lon,
                    },
                    offset: '1500m',
                    scale: '2000m',
                  }
                }
              }
            ]
          }
        },
        script_fields: calc_distance_script(lat, lon),
      }
    endendend

観光地に出向いて周辺のおでかけスポットを検索する際、徒歩で行けるくらい近いスポットについては、数メートルの違いはそれほど重要ではなかったりします。 ただ、これがもう少し遠くなり、少し歩いてでも行きたいかどうか悩むような距離にあるスポットについては、距離の違いが重要な判断材料になってきます。 そして、これがさらに遠くなって、歩くにはちょっと厳しいなという距離になってくると、今度はまた距離の重要性が低くなります。

本サンプルでは、上記の感覚を検索結果に反映するために gauss function を使っています。

この function に関する詳細な説明は公式ドキュメント the closer, the betterに譲るとして、サンプル内での設定を簡単に説明すると、

  • 1500mまでは徒歩圏内とみなし、この範囲に存在するスポットについては距離差がソート結果に反映されないようにする(検索クエリとの関連度のみを考慮する)
  • 1500m ~ 5500mに位置するスポットについては、ちょっと歩いてでも行く価値があるかを判断する重要な材料となりうるため、距離差をソート結果に反映する
  • 5500m以上離れた場所に位置するスポットについては、再び距離差が重要ではなくなるため、距離差をソート結果に反映しないようにする

というような設定になっています。

実際のサービスでは、この閾値をどこに設定するか、他の条件とどうバランスをとるかを調整することで、検索結果の品質を高めていくことになります。

では、このメソッドを実行してます。

$ bundle exec pry

> require'./spot.rb'
=> true> spots = []

> Spot.search_by_keyword('京都', 35.003765,135.769463).records.each_with_hit { |record, hit| spots << { name: record.name, address: record.address, distance: "#{hit.fields.distance.first.to_i}m" } }

> spots
=> [
  {:name=>"京都御所",   :address=>"京都府京都市上京区京都御苑3",     :distance=>"2544m"}, # 「八坂神社」よりも遠い!
  {:name=>"八坂神社",   :address=>"京都府京都市東山区祇園町北側625", :distance=>"1008m"},
  {:name=>"清水寺",     :address=>"京都府京都市東山区清水1-294",     :distance=>"1858m"},
  {:name=>"金閣寺",     :address=>"京都府京都市北区金閣寺町1",       :distance=>"5981m"},
  {:name=>"北野天満宮", :address=>"京都府京都市上京区北野馬喰町",    :distance=>"4821m"}
]

このように、スポット名・住所の両方に「京都」が含まれていて、より検索クエリとの関連度が高い「京都御所」が一番上に位置しています。 一方で、それ以外のスポットについては、上で示した設定に沿うような並び順になっていることが分かります。 実際のケースでは、PV数のような人気を表す指標や価格帯など様々な要素を含めることで、より使いやすい検索結果を提供することができそうです。

まとめ

このように、Elasticsearch を使うことで簡単に全文検索と位置情報を連携した検索機能を作ることができました。

特に最後の function_score_queryを使った検索は、条件を色々組み替えることでどんどんと洗練させることができそうです。 また、メソッド内のクエリの設定を書き換えるだけで結果が即座に変わるため、プロトタイピングもしやすく簡単にランキングの調整ができるので便利です。

今回の記事が、位置情報を使った検索がしたいという方にとって、少しでもお役に立てていれば幸いです。

なお Holiday では、日本の休日を楽しくしたい (iOS|Android|Rails) エンジニアを募集しています。

日本全国には、まだまだたくさんの知る人ぞ知るおでかけスポットや、休日の過ごし方があります。
そんな「日本中の魅力を発掘し様々な切り口で紹介することで、『次の休日どうしよう...』という悩みをなくしたい」という思いに共感いただける方がいらっしゃいましたら、ぜひ我々と一緒にこの問題の解決に挑戦しましょう!

ご応募はこちらから ↓

Holidayで日本の休日を楽しくしたいiOSエンジニアを募集! by クックパッド株式会社

Holidayで日本の休日を楽しくしたいiOSエンジニアを募集! - クックパッド株式会社の求人 - Wantedly

タイアップページでのLiquidの利用について

$
0
0

こんにちは。広告事業部の鈴木です。

皆さんはLiquidと呼ばれるテンプレートエンジンをご存知でしょうか? LiquidShopifyのメンバーを中心として開発されているテンプレートエンジンの一種で、 最近ではJekyllに使われていることでも知られています。 クックパッドの広告事業部では、広告商品の一つであるタイアップページ*1でこのLiquidを活用しています。

例. マルちゃん焼そば弁当コンテスト! [クックパッド] 簡単おいしいみんなのレシピが199万品

このタイアップページでは、そこからリンクされているレシピ応募コンテストページにユーザさんが投稿してくれたレシピ数を表示するのにLiquidが使われています。

f:id:szkmp:20150313165147p:plain

タイアップページにはデータベースに保存されたデザイン済みのHTMLに以下のようなプレイスホルダーになっているタグが含まれていて、ページがレンダリングされる際にコンテストへのレシピ投稿数と置換されます。

{{ contest_recipe_counts.537 }}

537はコンテストページのID *2

どうしてLiquidが便利なのか?

Liquidの他によく知られたテンプレートエンジンの例として、ERB, Haml, Slimが挙げられますが、これらはあらかじめ用意されたテンプレートファイルからHTMLを生成するのに向いています。これに対し、Liquidはデータベースに保存されたデザイナが用意してくれたHTML snippetの中にデータリソースを表示したいといった用途に向いています。

こんな業務を持っている人に便利
  • 独自のコンテンツ管理システムがある
  • CMSで入稿するコンテンツでデータベース内の情報を動的に取り扱いたいことがある
  • 一般的なCMSで入稿できるデータはテキストかHTMLに制限されているのが普通ですが、そこにデータソースを表示したいなどの理由で既に自前でカスタムタグを作って解析させている場合 *3
タイアップページの例の場合

クックパッド内のほとんどのページは共通のUIの上にコンテンツを配置する方法でデザインされています。 しかし、タイアップページはクライアントの商品イメージに合わせるためにそのような共通UIを使わず、 基本レイアウトを除くほぼすべてのUIをキャンペーンごとにデザインしなおします。

当然ながらそのデザインはクライアント側にチェックしてもらう必要がありますから、 デザインを決定するまで何度も何度も本番へデザイン変更を反映しなければいけません。

しかし、細かくデザインを修正するたびにデプロイするということになると、 デザイン修正のたびにエンジニアの作業が必要となり運用が面倒です。 そこでタイアップページは、入稿してもらったHTMLスニペットをデータベースに保存しておき、 「土台」となるHTMLページの枠に流し込むだけという構造にしています。 このような構造にすることで、デザイナがエンジニアを介さずHTMLを直接入稿できるようになるわけです。

そのように、静的HTMLが保存されている場合でも上記のタイアップページの様に”コンテストへの投稿数を動的に表示してほしい”といったような要件があるものです。 そういった事例を解決するのがLiquidです。 上述したようなLiquid tagをデザインに埋め込むことでデザインからデータソースにアクセスして表示することができます。

コード上のフロー

ここでコード上のフローを確認しましょう。

ControllerがModelのデータソースをロードしViewに渡す

Viewのレンダリングを開始する

Viewがデータソースをテンプレート上に展開する

Liquidでは展開されたデータソースのコンテンツ中のLiquid Tagをテンプレートエンジンがさらに展開するイメージです。

その他の用途例

Drop

Dropはあるモデルから、引数をとってsetterの役割をするmethodを排除してHashのような振る舞いの構造体を作ります。あるモデルから得られるデータリソースをデザイナがセキュアに表示できるように開放したいときに便利です。

例えば以下の例ではTieupRecipeというモデルをTieupRecipeDropに渡して、Liquidのテンプレートエンジンに{{ tieup_recipe.title }}を含んだ文字列をパースさせると、TieupRecipe#titleが取得できるというものです。

tieup_recipe = TieupRecipe.find(id)
tieup_recipe_drop = Pr::Liquid::TieupRecipeDrop.new(tieup_recipe)

template = Liquid::Template.parse("タイトル:{{ tieup_recipe.title }}")
template.render('tieup_recipe' => tieup_recipe_drop)
=> "タイトル:簡単!夏野菜の三色ロール"

TieupRecipeDropはあらかじめ自前で定義しておく必要があります。

class TieupRecipeDrop < Liquid::Drop
  def initialize(tieup_recipe)
    @tieup_recipe = tieup_recipe
  end

  def title
    @tieup_recipe.recipe.title
  end
end
Liquid Block

Liquid BlockはHTML tagを他のタグで囲みたい場合に使います。下記の例ではmobileというlabelの付いたTieupMailというモデルのレコードをデータベースから探して、そのメールを送るフォームを開くボタンを生成します。

  • tieup_mails tableのレコード

f:id:szkmp:20150313172414p:plain

  • 入稿されたLiquid tag
{% tieup_mail_colorbox mobile %}
  <img src="http://img5.cookpad.com/tieup/672/week_bt_sp.gif" width="74" height="74">
{% endtieup_mail_colorbox %}
  • 生成されたHTML
<a class="colorbox_link app_open_browser" data-iframe="true" data-dialog_width="700" data-dialog_height="400" href="http://cookpad.com/ct/88594" style="background: tranparent;">
  <img src="http://img5.cookpad.com/tieup/672/week_bt_sp.gif" width="74" height="74">
</a>
  • その見た目

f:id:szkmp:20150313172433p:plain

  • そのボタンをクリックすると表示されるメール送信フォーム

f:id:szkmp:20150313172448p:plain

※. colorbox_linkというCSS classがあるanchor tagをクリックするとJavascript Libraryがこのようなダイアログを表示するようになっています。

送信メールのレコードの中身が反映されています。送信メールの情報は管理画面で入稿されます。デザイナはメールフォームのlabel名さえ伝えられて知っていればメールフォームへのリンクを自由なデザインでコーディングでき便利です。

まとめ

以下のような場合にLiquidを採用すると便利です。

  • 頻繁にデータソースを含むページを修正する必要がある

  • 誰にでもデータソースを表示できるようになって欲しいが、誤ってデータソースが変更されたり、誤って公開されてはいけないデータソースが公開されるのを防ぎたい

用途例で示したように広告事業部のタイアップ案件では、Liquidを採用したことによってデザインフリーに(デザインに依存しないように)機能を切り出して積み立てることができるようになりました。デザインを絡めた業務フローがあるようなあなたの現場でもLiquidが思わぬライフセイビングになるかもしれませんよ。

*1:クライアントの製品の訴求を目的とした、タイアップ企画商品のページのこと

*2:詳しいタグの使い方はこちらを参考にしてください

*3:Liquidはカスタムタグの解釈と作成の機能を備えています

日経電子版xクックパッド データハッカソンの開催報告

$
0
0

検索・編成部の兼山です。

3/7に日経電子版xクックパッド データハッカソンが開催されました。

今回はデータハッカソンの報告をさせていただこうと思います。

開催概要: 日経電子版×クックパッド データハッカソン for students

f:id:code46:20150307094043j:plain

公開されたデータ

日経電子版

  • 記事テキストデータ
  • 紙面画像データ
  • 株式数値データ
  • 他にも多くのデータにアクセスできました

クックパッド

  • レシピデータ
  • 献立データ
  • 検索ログデータ(1ヶ月分)

参加してくれた学生

  • 18人
  • 人気だったツール: R, Python, word2vec

成果

7チームに分かれて取り組んで頂きました。

3つの賞を設けた都合、3つプロジェクトを紹介させていただきます。

ハッカソン実施後に両社の主催者で話しましたが、どのチームも我々の期待値を超えていて大変面白かったです。

最優秀賞

食材のつながりについての分析:

このチームは食材のつながりについて分析した結果を可視化して発表してくれました。

「食材の組み合わせは無限にあるのに一部だけがレシピになっている。」

この点に注目して、以下の様な仮説から「あまり知られていないおいしい」を発見しようというプロジェクトでした。

  • レシピが多くある組み合わせ(人参に対してじゃがいもなど)はおいしい
  • レシピが少なくとも、そのレシピが人気であれば、それは「あまり知られていないおいしい」なのではないか。

着眼点が興味深く、可視化の手法もワクワクするものでとても勉強になりました。

日経電子版賞

日経のデータとクックパッドのデータの相関について分析:

このチームは日経の記事データとクックパッドの検索ログデータなどを組み合わせて、

いくつかの対象を決めて、世間の関心を記事や検索ボリュームから確認する分析に取り組みました。

全く違うデータに接点などあるのかなと思っていたのですが、

コーヒーとか話題の食材などいくつかは存在しているそうで着眼点自体が既に興味深かったです。

クックパッド賞

代替食材の獲得:

誰かが書いたレシピを使って料理を作るとき、全くその通りに作る人は意外と少ないようです。

この際、食材Aが食材Bで代用できるなどの知識が豊富にあると、美味しさを損なわずに家にある食材で料理ができたりします。

この代用食材の知識を言語処理やクックパッドのデータを組み合わせて獲得するプロジェクトでした。

当日公開されたデータの中でも「クックパッドにしか存在しないデータ」に注目し有用な知識を引き出すことに挑戦されていて、

評価方法なども納得性の高いものでとても勉強になりました。

当日の気付き

slack連絡網:

開催週にslackを開設して、事前に連絡網を構築しました。

チームを決めたり自由に話してもらえればと思っていたのですが、あまり使われませんでした。

しかし当日に全体向けのアナウンスやチームを超えたサンプルコードのやりとり、 チーム内での相談やコードのやりとりに活用されていました。

R, Pythonが人気、jqが便利:

R, Pythonの勢いを感じました。共通言語があるのはとても良いことだと思います。

JSONデータからフィールドをしぼったり、データを小さくしたり整形したりするのにjqが便利でした。

まとめ

当日の様子やアンケートなどからほとんど全員の学生に楽しんでもらえたと感じ安心しました。

開催するかどうか検討していた時、

「1日(作業は半日以下)のハッカソンで大きなデータを十分に扱えるのか。当日楽しんでもらえるのか。」という心配があり楽しんでもらえる確率を上げる工夫を考えました。

  • データのクレンジング
    • フィールドごとに取りうる値をまとめたり、例外的なデータは思い切って消すなど
  • データのサンプリング
    • 全部入りと少ボリューム版をあらかじめ用意
  • サンプルコードを準備
    • 参加者の普段使うツールをアンケートで聞いて使われそうな言語で書く
  • リハーサルと称して社内のエンジニアが実際に取組む
    • 問題点の洗い出しに有効

また、短い期間でのハッカソンを企画する場合、簡単な概要のみでも開催前に使えるデータについてアナウンスして、

学生の皆さんに想像をふくらませてもらうのが必須です。

今回は参加者皆さんが好奇心を持って取り組んでくれてとても楽しかったです。

協力してくださった方々、参加してくださった方々ありがとうございました。

@yoshiori、有賀(@chezou)、原島、星(@kani_b)、青木(@mineroaoki)、小室、兼山

グダグダな文字処理の話

$
0
0

技術部のヴァンサンです。

前回の記事ではiPhone 6対応の話を主に書きましたが、今となって殆どのアプリがiPhone 6対応を終えているので、今回は違った面白い話をしようと思います。ただ、書き加えたいことが1つだけあります。昔からあるSprings & StrutsがAuto Layoutより楽な時もあれば、コードで位置を決めることが一番ふさわしいこともありますね。

では、文字の話をしましょう。

「文字」って子供でも分かるものですよね。

でもパソコンにとって文字や文字の処理って複雑ですね。

パソコンが世界の殆どの言語を対応したいなら、複雑なところがたくさんありますね、例えば:

  • アラビア語(العَرَبِيةُ)やヘブライ語(עברית)は右から左へ書く言語です。その中に英字を入れると右から左への中に左から右への部分が混ざっています。
  • アラビア語は文字がつながっていて、1つの文字が単語の中にある位置によって形が変わります(単語の冒頭、末尾、中の3種類)。
  • ヒンディー語(हिन्दी)みたいにデーヴァナーガリー(देवनागरी)文字を使う言語は文字の組み合わせがさらに複雑です。
  • タイ語(ภาษาไทย)は日本語と同様、単語がスペースで区切られていないのですが、単語の間でしか行を区切れません。なので行を切っていいかどうかを確認するために文章を解析する必要があります。
  • などなど

でも日本語に絞っても、シンプルなものではありませんね、例えば:

  • 文字数が多いですね。特に珍しい漢字も含みますと。そのせいでフォントを作るのがかなり大変ですし、キャッシュとかにかなりスペースを使いますね。
  • 多くの言語は打った文字がそのまま文章に入るのですが、日本語はIME(入力方式エディター)が必要ですね。
  • 「イルカ」、「いるか」、「海豚」、(または「鯆」)って同じものを指しますが、文字が完全に別物なので同じ風に扱うのが難しいです。
  • あいうえお順でリストを表示したい時、各項目の読みが分からないと並び順を決められません。多くのアプリみたいにあいうえお順にしないと、リストの中に探すのがかなり大変ですよね。
  • 単語の間にスペース区切りがないため、単語がどこから始まるのかどこまで続くのか判断がかなり難しいです。内容を自動的に分析したい時にかなり不便ですね。
  • 縦書きを表示したい場合、文字が上から下へになるだけではなく、長音符や句読点を回転させなければいけませんね。英字が入っている場合、英字での単語を回転させるのかさせないのか。
  • ルビ(ふりがな)を表示したい時、難易度がまた上がりますね。
  • 日本語と中国語の判別も難しいですね、特に漢字だけで出来ている短い文章("了解"とか)や名前だけを表示する時。日本語が中国語として表示されると基本的に読めますけど読みにくいですよね。

英語だけを対象にするなんて英語圏楽すぎますね(笑)。

OSが標準で多くのことをやってくれるのがありがたいですね。多くの国、文化が絡んでいるところもおもしろいですね。

日本語の組版に興味ある方はW3Cの日本語組版処理の要件がいいかもしれません。斜めで見るだけでおもしろいと思います。

多言語の処理に興味ある方は結局、英語ですが、Unicodeの仕様がありますね。特に第二章。もっと読みやすい本あるんでしょうけど(笑)

iOS アプリの UI でこれだけはおさえたい細部のインタラクション3つ

$
0
0

Holiday 事業室の多田です。先日 Elasticsearch の記事を書いた内藤と共に Holiday ( https://haveagood.holiday ) の開発を行っています。

Holiday は、去年9月に Web 版をリリースしましたが、よりおでかけを楽しくするために今年3月に iPhone アプリをリリースしました(ダウンロードはこちら)。

アプリの開発過程ではコンセプトや仮説を立て、その検証や実現のために作っては壊すことを何度も繰り返し行いますが、実現したい価値を提供するためには、出来上がったプロダクトの細部のインタラクションも重要になってきます。細かい部分に気を配り使い心地を良くしてこそ、本当に提供したい価値をまっすぐに届けることができるためです。逆に言えば、最後の最後で細かい部分がちゃんとしていないばかりにそれまでの過程が無駄になったらもったいないですよね。

今回はそのような細部のインタラクションの中でも、iOS アプリで幅広く使われている基本的なインタラクションを3つ、Holiday の iPhone アプリでの例とともに紹介します。

ログインフォームでのキーボードリターンの挙動

f:id:tdksk:20150318170513g:plain

Holiday のログイン画面は、メールアドレスとパスワードを入力するフィールドとログイン処理を実行するボタンが存在する一般的なものです。

このようなログインフォームの使いやすさを考えてみます。すると、デフォルトの挙動ではテキストをキーボードで入力→画面の他の部分をタップ→またキーボードで入力…としなくてはならず、操作する場所があちこち変わって不便だなということに気づきます。

この問題を解決するために、多くの iOS アプリではキーボードのリターン部分の挙動をフィールドによって適切なものに変えています。具体的には、次にフィールドがある場合にはそのフィールドに移動し、最後のフィールドではサブミット処理を行うというものです。

そのような処理は以下のような実装で実現できます。

// MARK: - UITextFieldDelegatefunc textFieldShouldReturn(textField: UITextField) -> Bool {
    if textField === emailField {
        // 次のフィールドに移動
        passwordField?.becomeFirstResponder()
    } elseif textField === passwordField {
        // ログイン処理を実行
        login()
    } else {
        textField.resignFirstResponder()
    }

    returntrue
}

また、処理だけでなくキーボードのリターン部分の見た目も変えることができます。Interface Builder 上で Return KeyNextGoなどに変えると良いです。

f:id:tdksk:20150318170553p:plain

キーボードをスクロール時に隠す

f:id:tdksk:20150318170602g:plain

Holiday のおでかけプランをさがす画面では、検索バーにフォーカスがあたった時にキーボードが出現するだけでなく検索履歴もリストで表示するようなものになっています。このように一つの画面に検索バーとリストを表示しているようなアプリは多く存在します(Facebook, Instagram, Twitter など)。

普通にテキスト入力する場合はずっとキーボードを出しておけば良いのですが、既に表示されているリストのほうに注目している場合はどうでしょうか。この時はテキスト入力をしたいとは思っていないため、キーボードを隠したほうがリストの一覧性が上がって良さそうです。ということで、リストのほうを見ようとスクロールする時にはキーボードは隠しましょう。

// MARK: - UIScrollViewDelegatefunc scrollViewWillBeginDragging(scrollView: UIScrollView) {
    view.endEditing(true)
}

ボタンタップ時、通信処理が完了する前にフィードバックを返す

f:id:tdksk:20150318170650g:plain

サーバと通信するアプリでは、通信が伴う処理のフィードバックをどのタイミングでどのように行うかという問題が常につきまといます。

例えば Holiday ではおでかけプランをお気に入りに追加することができるのですが、そのデータは全てサーバ上に保存しています。そのため、お気に入りに追加・削除する処理が正常に行われたかどうかをチェックして、サーバ側のデータの状態とクライアント側での表示が矛盾しないようにする必要があります。その際の確実な方法は、リクエストを送ってレスポンスが返ってくるまでは読み込み中であることを示し、レスポンスが返ってきたらその状態を画面に反映させるという方法です。処理に時間がかかる場合や、その間ユーザの行動を妨げても問題がない場合はそれでも良いのですが、お気に入りなどのライトな行動においてはその後の行動を妨げず、かつすぐにフィードバックを返すことがストレスのない体験に繋がります。

具体的には、ボタンが押されたタイミングでまず先にボタンをお気に入り追加済みの状態(黄色)に変えてしまいます。またこれだけだとフィードバックとして弱いので、バウンスするアニメーションも同時に行うようにしています。その後レスポンスが返ってきたら正しい状態にする(ほとんどの場合は成功しているのでそのまま)という処理を入れています。レスポンスが返ってくるまでは数 m 秒ですが、それを待つかすぐにフィードバックを返すかで大きく使い心地に影響します。

このような実装を簡略化したコードは下記の通りです。

@IBAction func bookmarkButtonTapped(sender: AnyObject) {
    // モデルの状態を変更
    plan.toggleBookmark()

    // ボタンの選択状態を先に変えるフィードバック
    reloadBookmarkButton() // bookmarkButton.selected = plan.bookmarked// アニメーションによるフィードバック
    bookmarkButton.doBounceAnimation()

    // 非同期で POST/DELETE リクエストするメソッド
    updatePlanBookmark(
        plan: plan,
        completion: { (error: NSError?) inif error {
                // リクエスト失敗時はモデルの状態を元に戻し、ボタンの状態に反映
                plan.toggleBookmark()
                reloadBookmarkButton()
            }
        }
    )
}

ちなみに、バウンスアニメーションは下記のようなパラメータで行っています。

func doBounceAnimation() {
    UIView.animateWithDuration(
        0.05,
        animations: { () -> Void in
            self.transform = CGAffineTransformMakeScale(1.4, 1.4)
        },
        completion: { (Bool) -> Void in
            UIView.animateWithDuration(
                0.6,
                delay: 0.0,
                usingSpringWithDamping: 0.3,
                initialSpringVelocity: 0.0,
                options: .CurveLinear,
                animations: { () -> Void in
                    self.transform = CGAffineTransformMakeScale(1.0, 1.0)
                },
                completion: { (Bool) -> Void in
                    self.transform = CGAffineTransformIdentity
                }
            )
        }
    )
}

最後に

今回は iOS の UI の細部のインタラクションの中でも特に基本的な3つの例を紹介しました。実際のアプリではそれぞれの機能に応じた様々なインタラクションを考える必要がありますが、まず基本的な部分をおろそかにしてはいけません。基本的な細かい部分の使いやすさが、アプリ全体の使い心地に大きく影響し、実現したい価値提供に繋がるのだと思います。

なお、Holiday ではプロダクトの細部にまでこだわりたいエンジニアを募集しています。興味をお持ちいただけた方はぜひご応募ください。

Holidayで日本の休日を楽しくしたいiOSエンジニアを募集! by クックパッド株式会社

フェーズと目的に応じたプロトタイピングの手法と意味

$
0
0

こんにちは。ユーザーファースト推進室の元山です。

みなさんはスマートフォンアプリケーションやWebサービスの開発・改善をするときにどのようなプロセスで行っているでしょうか?アジャイルやリーンなどの最近では一般的なよくある開発プロセスの中で、今やプロトタイピングは当たり前に行うものとなっていると思います。プロトタイピングを支援するアプリやWebサービスも数多くありますが、ただ闇雲にプロトタイプを作ればいいわけではありませんし、プロトタイプモックを作ること自体が目的化されては意味がありません。

クックパッドでもプロトタイピングを取り入れた開発プロセスを行っていますが、開発のフェーズであったり、または目的によってどのようなアウトプットとしてプロトタイピングするのかは変わってくると思います。今回はクックパッドで実際に行っているプロトタイピングについてフェーズや目的ごとにご紹介しようと思います。

コンセプト・仮説をたてる

そもそもプロトタイピングとは何でしょうか?紙にワイヤーを描いたり、プロトタイピングツールを使ってテストすることだけが必ずしもプロトタイピングだとは限らないと思います。プロトタイプは完成品を作る前に試行錯誤するための叩き台のようなものですし、それ自体は何か一つのアウトプットに縛られるものではないと思います。

新しいアプリ・Webサービスもしくは既存サービスなどへの改善をしようと思った時に必ず最初に行うべきことはコンセプトや仮説をきちんとたてて大まかな方向性を立てることです。この場合の方向性とは一言で言える程度のことだけでなくもっと詳細に考え組み立てたものを指します。

アプリケーション定義ステートメントシート

クックパッドでは最近スマートフォンアプリケーションの開発に取り組むことも多いので、プロジェクトメンバーで話し合った上でアプリケーション定義ステートメントを書くことから開発をはじめるようにしています。クックパッドでは以下のような項目を設定しています。

  • サービス名称
    仮でも良いので最初に決めておく

  • サービスの価値
    ステートメント(サービスの概要を一言で書く)
    キラー要素

  • ターゲットユーザー
    必ずしも事細かく書く必要はないが、実際にこのサービスを使うところがイメージできるようなユーザーの属性や特徴を書く

  • ユースケース
    メインのユースケースを2〜3つ

  • コアタスク(コア機能)
    これがなくなってしまうとサービスの価値が変わってしまうもの
    機能名ではなく、ユーザーがやりたいこと・欲求をベースに書くのが良い
    多くても2つくらいまで

  • サブタスク(サブ機能)
    これがなくても良いが、あるとより効果的にサービスの価値を届けられるもの
    必ずしもすべての項目を書き出す必要はない

  • 諦めること
    最初にブレストをする中で今回の開発でやらないことも決めておく

  • 競合や参考になるアプリ・サービス
    どう差別化していくのかや市場調査、メンバーやユーザーとコミュニケーションをとるための言語として使用したりするので、あれば書き出す

クックパッドでは開発の各プロセスを進めていく上でデザイナー同士でレビューしあいながら進めています。基本的にはGithubのissueを使って行っており、まずはプロジェクトメンバーでステートメントシートを書いてそれをissueとしてアップし他のデザイナーなどがレビューし、気になる点・改善点があればコメントをして修正していくというような流れです。

これを書くことによって得られたメリットではクックパッドでも多くあり、例えば開発の途中で方向性の違いが発生する、コア機能が多かったりしてサービスの軸がぶれるといったことを防ぎ、解決すべき課題を明確化しフォーカスされたアウトプットにつながっていると思います。 このように叩き台を作ってそれを元に試行錯誤を重ねていくというプロセスがプロトタイピングであると思います。

f:id:kudakurage:20150319173548j:plain

おでかけサービス「Holiday」のiOSアプリの定義ステートメントは最初チームメンバーに考えて作成してもらったが、コア機能がフォーカスされておらず機能の羅列になっていたのを、レビュー後ターゲットユーザーにそって2つの軸でかき分けるように変更しました。 その後も開発しながら検証し、必要に応じて定義ステートメントも少しずつ修正していきました。

シナリオライティング

アプリケーション定義ステートメントを作る前後にはシナリオライティングをすることもプロジェクトによってはあります。想定されるユーザーがどのようなシーンでどのような欲求や課題を持っているのかをストーリーだてて記述していくものです。

ここで重要なのは「おでかけプランをお気に入りに追加します」のようにサービスやアプリの機能名で書かずに「気になったおでかけプランは後から見返して検討できるようにキープしておきます」というようにユーザーの欲求ベースで書くことです。その課題を解決したりや欲求を満たすための方法は必ずしも一つの解決方法ではないため、その場面で何を提供するのが良いか選択肢を含めて考える事ができるようにしておきます。 欲求をベースにしたシナリオ(アクティビティシナリオ)を書いた上で、各シーンごとに具体的にどのようなアクションをとるのか(欲求を満たすための機能名など・インタラクションシナリオ)を書いていきます。

ターゲットとなるユーザーや利用シーンなどが複数パターンある場合は各ユーザーやシーン別にシナリオを書きます。例えば、週末に友だちと行くおでかけ場所を探す・決めるとき、実際におでかけをするとき、おでかけした場所の記録を残すときなどに分けて書いて行きます。 シナリオをきちんと書いておくと後にユーザーテストのシナリオとしても利用することができます。

捨てることを前提とした動作モック・アプリケーション

最初に仮説をたてるタイミングで、ユーザーが具体的にどのような課題を感じているのかわからない、本当に仮説で立てたような欲求を持っているのか確証がもてないということもあるかもしれません。

そのような場合はその仮説を検証するためだけのプロトタイピングを行うこともあります。例えば「日々作った料理を写真に撮って記録する」というようなことはしているが、それを何のために行っているのか?その行動の裏にはどのような欲求があるのか?といったことを確かめたり、見つけ出したりというようなことです。このような場合は仮説や欲求を検証するためだけに最低限の動くモックや実際に使えるプロトタイピングを作って使っているところを観察したり、インタビューをしたりということを行います。そこで得られた結果をもとに別の仮説を立てなおしたり、方向性を微修正したりといったことを行います。

このように仮説やコンセプトを定めるという段階でもその状況に応じてどのようなプロトタイピングをするのかというのを考える必要があります。

f:id:kudakurage:20150319173623j:plainこれはお料理アルバムというアプリを開発する際に作成したプロトタイプです。コンセプトを決めるためにワイヤーフレームを触れるようにしたインタラクションモック(左:ワイヤーフレームをflintoを使ってプロトタイピング)や実際に使うことができるテスト用のアプリ(右:お手本の構図を参考に写真を撮ることができる)を作成したりしてテスト&インタビューをしました。

客観視してアイディアを考える

コンセプトや仮説がたてられたらそれを徐々に具体化していきます。ある機能を一つ提供するにしてもそれをどのようなアプローチ・見せ方にするのかというのは無数にあるため、それを適切に導き出す必要があります。頭の中だけで考えていて良さそうと思っていても、いざ作って見たらイマイチだったという経験はデザイナならかならずあると思います。

この時できるだけ頭で考えていることを外化*1して客観視できるような状態にしていくと、素早く適切な方法を導き出せると考えています。

スケッチ&ペーパーモック

f:id:kudakurage:20150319173639j:plain一般的なプロトタイピングとしてペーパープロトタイピングというものがありますが、クックパッドでもまずは手でスケッチすることから始めています。スケッチならすぐに形にすることができるので、アイディアベースでできるだけ多くのパターンをこの段階で考えてアウトプットします。

特に私の場合は紙ではなくてハンディーサイズのホワイトボードでやるのが好みです。 ホワイトボードでスケッチする利点はいくつかあり、まずすぐ書いてすぐ消せること。紙と鉛筆の場合だと消しゴムに持ち替えてゴシゴシってしないといけないですが、ホワイトボードなら指でもさっと消せるのでいろんなパターンをガンガン描いて思考していくのには適していると思っています。

描くときに細いペンは使わずに普通くらいの太さのペンで書くのがおすすめです。ディテールを書き込み過ぎず、ワイヤーや大枠を描くのに適しています。良さそうなスケッチがかけて思考がまとまったら、それを紙に書いてもう少しディテールに落としこんでいきます。

画面遷移図

ペーパーモックは画面遷移図としてまとめて、実際に使うフローや全体の規模感・優先度をわかりやすくしています。 達成したいこととUI・フローが合っているか、コアな機能にちゃんとアクセスしやすい形になっているか、遷移に矛盾しているところはないかなどを確認します。 シナリオが複数ある場合は、シナリオ別に遷移図を作ったりして想定通りに使えそうかなどを確かめるとわかりやすくできます。

ユーザに使ってもらって検証する

画面遷移図ではある程度想像して見る必要があるため、ある程度ワイヤーフレームが決まったら実際に触って見ることができるようにします。そしてUIが使いやすいものになっているか、どう使うかが分かりやすいものになっているかをユーザーを通してテストします。プロトタイピングツールを使ってまずはペーパーモックをつなげて大まかな方向性など全体の流れにユーザーが戸惑うことがないかテストし、徐々にビジュアルを作りこんで置き換え、ディテールのわかりやすさやユーザビリティの検証をしたりしています。実際にものとして作る前に何度も検証できることで出戻りが少なく結果的に品質的にもスピード的にも改善されていると感じます。

気になることがあれば割りと頻繁にユーザーテストをするようにしているので、毎回テストのためのリクルーティングをしているわけではなく、ターゲットユーザーに近い社内のスタッフなどにお願いしてテストすることも多くあります。 すべての画面遷移を網羅する必要はなく、一つのシナリオを通して体験できるような画面・遷移を用意できれば良いと思います。例えば、「レシピを探す」「レシピをのせる」といった具合にその都度必要なシナリオ別に用意して検証します。 ある程度触れるようにしたものはプロダクトマネージャーなどとの共有といったメンバーのコミュニケーションにも役立っています。

インタラクションモック(prott, inVision)

クックパッドではプロトタイピングツールとしてprottやinVisionを利用しています。スマートフォンアプリの場合でprottを使用することが多いですが、PCサイトの時にはinVisionを使用したりしています。 これらはプロトタイプ上にコメントを残せる機能があるので、非同期な(メンバーもしくはレビュワーが任意のタイミングで見る)レビューやリモートワークでのコミュニケーションなどでもスムーズに行うことができていると思います。

f:id:kudakurage:20150319173653j:plain

メンバーで作るイメージを共有する

動作モックを作ることで全体の大まかなイメージはメンバー(社内のデザイナ・エンジニア・ディレクタやその他の部署との)間で認識を合わせることができると思います。ただよりディテールによった(もしくは一般的なトランジションとは違う特殊な遷移などの)検証やメンバー間の確認・コミュニケーションが必要な場合は、これらのツールでは表現できないので別途それにフォーカスしたモックを作成することもあります。 どうしても平面だけのデザインだと、メンバー間で意図が伝わらなかったり、想定していたものと実際とは違うものができたりしてしまうことがあるので、重要だと思う部分に関しては共通認識をとれるように精巧なものも作ります。

これは実際におでかけサービス「Holiday」のiOSアプリを開発している段階で作成しました。Keynoteを使って細かい部分までトランジションやアニメーション再現し、実際に作るエンジニアなどとのコミュニケーションに役立ちました。

アニメーションモック(Keynote, HTML)

よりディテールなプロトタイプはKeynoteやHTMLで作ったりしています。 Keynoteの場合はIllustratorやSketchなどで作った画面から動かしたいパーツごとにコピペするなどした要素を、トランジッションのマジックムーブを使って細かいトランジション・アニメーションを表現します。

インタラクションも伴うディテールモックを用意する場合はHTML・CSS・JSで作ることもあります。特殊なトランジションで実際に触って検証したり、認識を合わせたりするのに使ったりというものですが、作成する手間もかかるのであまり多くは行われていません。

f:id:kudakurage:20150319173720g:plain

まとめ

今回ご紹介したのはクックパッドでのプロトタイピングの実例ですが、目的や用途に応じて必要なプロトタイピングの方法は他にも多くあると思います。重要なのはプロトタイプモックを作ること自体を目的化させず、検証をする・メンバーとコミュニケーションをとるなど何を目的にするかによって考える必要があると思います。どの開発でも共通で利用できるような汎用的な手法は多く知られていると思いますが、特殊な目的によった事例(例えばゲームのレベルデザインを検証するなど)も多く共有されていくとプロトタイピングについてより広く深く考えることができるのではないかと思っています。

またプロダクト開発はデザイナーだけが行うものではないですし、メンバー全員がプロトタイピングに参加することで方向性や認識の統一が図れようにしていけるとよりスピード感をもって開発に取り組めるのではないかと思います。 プロトタイピングをもっともっとして改善していきたい、ユーザーさんに提供していきたい価値はまだまだあります。クックパッドではこういった開発に取り組んでみたいと思っていただけるデザイナ・エンジニアを募集しています!

*1:外化とは自分の考えを他者に説明するために文章を書いたり、図を作ったりして理解の過程を見えるようにすること。「脳から出してみる」こと。


新米Android開発者が見落としがちな3つのポイント

$
0
0

こんにちは、投稿推進部の吉田(@101kaz)です。Androidアプリの投稿周りの開発を担当しています。
去年クックパッドに入社したことをきっかけに、本格的にAndroid開発をするようになりました。 今回は私のような開発をはじめて日が浅い人が見落としがちな「非同期処理時のNPE(NullPointerException)」と「Activity破棄に関する問題」と「ProGuardの設定忘れ」について実際の遭遇した事例をベースに紹介します。

非同期処理コールバック時のNPE

ある時Fragmentから非同期処理を行い、コールバック内でFragmentの内のviewにアクセスするコードを書きました。

@Overridepublicvoid onActivityCreated(Bundle savedInstanceState) {
    ApiClient.getRecipes(new ApiClient.Callback() {
                @Overridepublicvoid onSuccess(List<Recipe> recipes) {
                  View loadingView = getView()
                          .findViewById(R.id.loading_view);
                  loadingView.setVisibility(View.GONE);
                }
                @Overridepublicvoid onFailure() {
                }
    });
}

このコードは問題なさそうに見えますが、getView()がnullを返す可能性があり、その場合findViewById()でNPEが発生します。 これは、非同期処理のコールバックが返ってくる前にFragmentが画面の回転などの理由で既にデタッチされ、Viewが破棄される可能性があるためです。 そのためコールバックの処理を行う前にまず自分の状態を確認し、すでにデタッチされているようであれば処理を終了しなければなりません。

if(isDetached() || getActivity() == null) {
    return;
}

オフィス内のネットワーク環境ではAPIサーバーからのレスポンスが数十msほどで返ってきます。 そのため、コールバックが返ってくるより先にFragmentがデタッチされることは少ないので開発時に見落としがちですが、 このままアプリがリリースされてしまうと多くのユーザーの方の手元でアプリがクラッシュする可能性があるので注意が必要です。

非同期処理時のNPEを防ぐにはコールバックの冒頭でデタッチされてないか(もしくはアクティビティが既に終了していないか)チェックする事を習慣づけることが重要です。 とはいえ、忘れてしまうこともあるので可能であれば仕組みで解決したいところです。 クックパッドではPull Request上で、botが投稿するチェックリストを開発者自身がチェックすることでミスを事前に防ぐ試みをしています。

また、非同期処理を行うライブラリ側にチェックを委譲する方法もあります。
RxAndroidというライブラリでは、bindActivity()bindFragment()というメソッドがそれぞれ提供されています。
これらを利用すると、Activityが既に終了していたり、フラグメントが既にデタッチされている場合にそれ以上の処理が実行されないようにすることが可能です。
RxAndroidでhello worldをトーストするサンプルは以下のようになります。 このサンプルでは、Fragmentがデタッチされているとcall()は実行されません。

Observable<String> observable = Observable.just("hello world")
Observable<String> bindedObservable = AndroidObservable.bindFragment(this, observable);
bindedObservable.subscribe(new Action1<String>() {
    @Overridepublicvoid call(String s) {
        //Fragmentがデタッチされている場合呼び出されない
        Toast.makeText(context, s, Toast.LENGTH_SHORT).show();
    }
});

Activity破棄に伴うインスタンス変数の永続化

「写真撮影後に失敗した旨のメッセージが表示され、画像が取り込めない事がある。」というバグ報告があり調査したところ、以下の様なコードがありました。 ここでは、カメラアプリからSelectPhotoFragmentに戻ってきて、onActivityResultで画像ファイルにアクセスしようとしています。

publicclass SelectPhotoFragment extends Fragment {
    private Uri savedPhotoUri;
    // ....privatevoid launchCamera() {
        savedPhotoUri = Uri.fromFile(tempFile);
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, savedPhotoUri);
        this.startActivityForResult(intent, REQUEST_CODE_TAKE_PICTURE);
    }

    @Overridepublicvoid onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (savedPhotoUri != null) {
            listener.onSelect(photoUri);
        } else {
            ToastUtils.show(getActivity(), "写真撮影に失敗しました");
            return;
        }
    }
}

このバグは、SelectPhotoFragmentがバックグラウンドにまわった際に破棄され、savedPhotoUriの参照も消えてしまったことが原因でした。 AndroidのシステムはActivityがバックグラウンドにまわるとメモリの空き容量などの都合で順番に破棄していきます。 破棄のタイミングは環境や機種の性能によって異なるので、これも開発時には見落としがちになります。 Activityの破棄に起因するバグを防ぐためには、適切にインスタンス変数をBundleに格納して状態の永続化を行う必要があります。

// Activityの場合@Overrideprotectedvoid onCreate(Bundle savedInstanceState) {
        if (savedInstanceState != null) {
            if (savedInstanceState.containsKey(SAVED_PHOTO_URI_KEY)) {
                savedPhotoUri = savedInstanceState.getParcelable(SAVED_PHOTO_URI_KEY);
            }
        }
    }
    
    @Overridepublicvoid onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelable(SAVED_PHOTO_URI_KEY, savedPhotoUri);
    }


// Fragmentの場合@Overridepublicvoid onViewStateRestored(Bundle savedInstanceState) {
        if (savedInstanceState != null) {
            if (savedInstanceState.containsKey(SAVED_PHOTO_URI_KEY)) {
                savedPhotoUri = savedInstanceState.getParcelable(SAVED_PHOTO_URI_KEY);
            }
        }
    }

    @Overridepublicvoid onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelable(SAVED_PHOTO_URI_KEY, savedPhotoUri);
    }

また、Activityの破棄に起因する問題を未然に防ぐために、 開発者向けオプションにあるアクティビティを保持しない設定を有効にした状態で開発する方法も有効です。

さらにIcepickというライブラリを使うと、煩雑な状態の永続化をアノテーションを使って手軽に管理することが出来るようになります。

@Icicle String username;

  @Overridepublicvoid onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Icepick.restoreInstanceState(this, savedInstanceState);
  }

  @Overridepublicvoid onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    Icepick.saveInstanceState(this, outState);
  }

ProGuardの設定忘れ

ProGuardとは、アプリの最適化と難読化を行うツールです。
クックパッドのAndroidアプリもリリースビルド時にはProGuardを利用しますが、ProGuardを有効にするとリフレクションを使った実装が正しく動作しなくなります。 これはProGuardが参照のない箇所を削除したり、メソッド名や変数名を書き換えてしまうことが原因です。
リフレクションを正しく機能させるためには、該当範囲をProGuardの対象から外す必要があるのですが、 デバッグビルドでは通常Proguardを有効にしないので、ProGuardに関する問題も開発時に見落としがちです。
アプリ開発で直接リフレクションを使う機会は少ないですが、JavascriptInterfaceなどAPIの裏側でリフレクションが使われいるケースや、 利用しているライブラリの中で使われている可能性もあるのでこれも注意が必要です。

-keepclassmembers public class com.cookpad.android.sample {
    *;
}

上記のように書くと、com.cookpad.android.sample以下の全てのメンバ変数とメソッドがProguardの対象から外れます。

まとめ

開発時に発生しにくいバグは、どうしても見落とされてリリースされやすいです。
ライブラリの導入などでそもそも問題が起こりえない体制を整えることがベストですが、
全てはカバーすることは難しいので、QA期間を設けたり、botを活用したり、コードレビューなど様々な方法を組み合わせることによって、 クラッシュしにくいアプリ開発をしていきたいですね。

ユーザを理解しよう(クックパッド料理教室の先生編)

$
0
0

クックパッド料理教室の伊尾木です。

このエントリーでは、クックパッド料理教室での「ユーザ(先生)を理解」するための工夫をご紹介します。

クックパッド料理教室(https://cookstep.cookpad.com)とは、クックパッドが認定した料理教室への予約サービスになります。 クックパッドの厳正な審査を通過した料理教室だけが加盟でき、様々なジャンルの先生に参加頂いています。

例えば、ミシュランの星付きレストランでシェフをしていたフレンチの先生や、 タイ国大使館から認定されているタイ料理の先生などバラエティにとんだ先生方がそれぞれ得意な分野のレッスンを開催しています! よく誤解されるのですが、クックパッドのレシピを使ってレッスンを行うわけではありません。レシピは全て先生が独自に開発したものになります。

ユーザ(先生)を理解しよう

クックパッド料理教室では、ユーザは2人います。1人目は、予約してレッスンを受けてくれる生徒さまです。そして2人目はレッスンを開催してくれる先生です。

先生に対しては予約管理や、レッスン管理を行うための管理画面を提供しており、私(伊尾木)がメインで開発を行っています。 先生に便利な管理画面を開発していくために、先生や先生の業務を理解するために様々な施策を行っています。

先生の業務を理解しよう

「ドッグフードを食べる」つまりは、自分たちで作っているサービスを自分たちでヘビーに利用する、ということが重要だというのはよく知られています。 実際、クックパッド料理教室の開発チームでも、頻繁に自分たちで予約してレッスンに参加しています。

ところが、管理画面のユーザは料理教室の先生です。 私は料理教室の先生になれないので、管理画面をリアルに使うことができません。 生のドッグフードを食べることができないのです。

1. プログラミング言語の先生になってみた

料理教室の先生にはなれませんが、それでも先生業務を体験することは重要です。 そこで、料理教室の先生ではなく、プログラミング言語を教える先生になってみることにしました。

プライベートレッスンを開催するためのサービスが世の中にいくつかりますが、クックパッド料理教室とやや似ているサービスを選び、実際に先生として登録しました。 会社の許可を得て、毎週1、2回はレッスンを開催しています。

プログラミング言語のレッスンではありますが、レッスンを準備して運営する、レッスン前後に生徒さまとやりとりを行うというのは料理教室も同じです。 この中で実際にレッスンを開催するさいに、何を考え、どう感じるかをリアルに体験することができ、サービスを設計するうえでとても役にたっています。

特に他サービスの仕組みを具体的に利用することで、そのサービスの良い機能から発想を得て、クックパッド料理教室に組み込んだ機能がいくつもあります。

2. 頻繁にレッスンに参加する

先にも書きましたが、私たちのチームではスタッフ全員が頻繁に様々なレッスンに参加しています。 エンジニアも、月に数回はレッスンに参加します。 ただ、そこで得られる知見の多くは生徒さま側に関するものです。

ですが、色々な先生のレッスンに参加していると、各先生の様々な努力を知ることができます。 そんな努力の中から、良いと思うものは他の先生も活用できるように機能として実装したりしていします。

例えば、リピータの多い先生方はレッスン受講中に「次回はこんなレッスンを考えてますよー」といった会話をして、次回の予約に繋げているということが分かりました。 そこで、もっと自然に次回レッスンの案内ができるよう、レッスン案内チラシを自動で生成する機能を実装しました。 これは今では非常に重要な機能になっていますが、実際にレッスンに参加しないとこの施策の存在に気づきかなかったかもしれません。

3. 全国の先生に電話相談を行った

クックパッド料理教室は全国展開に展開していて、北は北海道から南は沖縄まで教室が存在します。 一方、私たちのチームは東京で仕事しています。なので、どうしても地方教室へのフォローが手薄になってしまいます。

各教室との連絡のやりとりはメールなどが主ですが、やはり直接会話をしよう!というわけで、全教室(当時100教室ていど)に電話相談を実施しました。 これは営業チームだけではなく、エンジニアも含めてチーム全員で100教室に架電しました。

この結果、地方を含めた全教室でどのような課題があり、あるいはどのような機能が喜ばれているのかを肌で知る事ができたのは大きな収穫でした。 特に、地方の先生の多くが管理画面上の便利機能の存在に気づいていないということが判明し、 もっと分かりやすい導線を考え管理画面のTOPを変更したり、あるいは積極的に便利機能の紹介を行うなどフォローを行いました。 また、レッスンページの書き方や写真などのを相談を先生と直接行った結果、レッスンページの質が向上し、予約のスピードがあがったように感じます。

4. 先生の活動結果を知る

先生の活動の結果を見るために、予約や口コミが入ると自動的にチームのチャットルームに通知される仕組みを導入しました。

教室数が増えてくると個々の教室の状況がみえづらくなってきます。全体の予約数や公開レッスンの数などは常にチェックしていますが、 個々の先生の活動結果をリアルタイムで実感することは難しいのです。 そこで、予約や口コミが入るたびに、チームのチャットルームに自動的に通知するようにしました。

この結果、どの教室がいつ予約が入ったのかがリアルタイムで分かるようになりました。 レッスン公開後1時間で100席近くが完売する教室などをすぐに見つけることが可能になりました。 実際、チームの会話の中でも「あ、XX教室に予約入った!写真を変えたからかな」とか「YY教室に初予約入った!」や「ZZ教室、一気に予約埋まっていきますね!!」 というような発言が増え、先生の活動により関心がむくようになりました。

ちなみに、これには副次的な効果もあります。予約や口コミは私たちにとっても嬉しいものです。 チャットにその通知が流れてくると、常にポジティブフィードバックを受けることになるので、モチベーション向上にとてもとても効果があります。

先生の意見を聞こう

先生の意見を聞くことも非常に重要視しています。やはり実際に使う人の意見は、こちらの想定できていなかったものであったりするため、貴重ですね。 しかし、そうはいっても中々、先生方からポンポンと意見や要望がでてくるものでもありません。 多くの方は、多少不便だと思っても「言ってもしょうがない」と考えて、意見を言ってくれません。これはお互いにとって不幸だと思うのです。

そこで「先生と共に成長する管理画面」というコンセプトを打ち立て、先生方が気軽に意見や要望を言えるような雰囲気を作ることにしました。 「先生の意見を反映していますよ」というのは、何度も何度も伝えていきました。 「先生方から要望のあった機能を実装しました!」というように新機能を紹介したり、簡単なものであれば要望を受けてから1時間以内に対応したりということを地道に続けました。 そして、誰でも意見を投稿できるように、クローズドな掲示板を立ち上げ、そこで多くの意見を吸い上げるようにしました。

これらの結果、先生から意見が言いやすくなったように感じます。そして、何より「意見を言えば聞いてくれる」と感じてもらえることによって、 先生のモチベーションが刺激される効果もあったと感じています。

まとめ

今回紹介したユーザを理解ための施策は、ほとんどがスケールしづらいものです。もっと大規模になった際に、ここであげているような施策がそのまま機能するのは難しいでしょう。

私たちのサービスは昨年本格リリースしたばかりであり、まだまだ始まったばかりのサービスです。 このようなフェーズでは、スケールするかどうかに関わらず、ユーザを深く理解しサービスの質を向上させることが重要だと考えています。

We are hiring!

クックパッド料理教室は、クックパッドのレシピを見ただけでは解決できない問題領域にチャレンジしています。 一緒にこの問題領域にチャレンジしたいと思ってくださる方や、ちょっとでも興味があるという方は是非ご応募ください!!

https://www.wantedly.com/projects/15674

雑な発想を活かすチーム作り

$
0
0

インフラストラクチャー部の成田(@mirakui)です。インフラストラクチャー部は、クックパッドで扱っている全サービスのサーバを設計・構築し、運用しているチームです。2015年3月現在、6人のメンバーで運用をしています。

さて、この運用というのは外から見ていると保守的な仕事に思えるかもしれませんが、その実、とてもクリエイティブな仕事です。クックパッドのサービスは一日平均で10回以上デプロイされており、アクセスも日々増え続け、状況は刻一刻と変化しています。今日動いているサーバ構成が、一年後に通用するとは限らないわけです。そんな変化に追従するためには、サーバを常に改善していかなければなりませんし、チームにも柔軟な発想が求められます。

「さあブレストしよう」→アイデア出ない問題

さあ業務を改善しよう、と意気込んでブレインストーミングを開いても、なかなか十分なアイデアが出きらないのはよくある話です。

一方で、「このサーバ、いけてないよね」「あの新しいミドルウェア入れてみたい」といったようなアイデアの種は、ランチや飲み会の時に日常会話でよく出てきます。こういった雑談の中には、仕事のアイデアが隠されています。

こうした柔軟な発想を生かし、業務をクリエイティブにしていくためのツールとして、私達のチームでは「雑」の概念を取り入れています。

雑な発言

id:bash0C7 さんのエントリでは、雑の概念を的確に説明しています。

今回述べたい「雑に発言する」とは、きっちりと推敲したり計画立ててやってるわけじゃないおおざっぱな、しかしそれを契機に話を膨らませたり、世界観の一端を伝えたりできるような、適当に役に立つライトウェイトな発言という事です。
雑に発言をしよう - id:bash0C7の進捗

私達の言う雑とはまさにこの事を指しています。決して低品質な仕事をしたり、誰かに迷惑をかけたりすることを指しているわけではありません。日頃から発言のハードルが低く、気軽に意見できることが重要なのです。

同エントリの本文で参照されている記事、月間38億PVを支える“チームの力”とはでは、「マクドナルド理論」について述べられています。

「マクドナルド理論」て知ってます? みんなでランチの行き先を考えているとき、「マクドナルドに行こう」と誰かが言うと、みんなが一斉に否決して、別の案が次々に出てくるというセオリーなんですが(笑)。つまり的外れな意見とか最悪のアイデアが自由に言える雰囲気が重要ってこと。それによって議論やアイデアが形になっていくわけです。
月間38億PVを支える“チームの力”とは [片桐孝憲] | ISSUES | WORKSIGHT

マクドナルド理論は雑を特徴的に捉えた考え方の一つです。まず、雑に発言をしてみる。そこから新しい発想が生まれていくのです。

図: マクドナルド理論の実践

zatsu リポジトリ

雑な発言は、雑すぎて記憶にとどまらず、すぐに忘れてしまいがちという問題点があります。特に、精神的に自由な状態の時(=酔っ払ってる時)に出たせっかくのアイデアが、翌日には覚えていないということがチームで問題になっていました。

そこで、私達のチームでは、GitHub Enterprise 上に「zatsu」というリポジトリを用意しました。このリポジトリでは特にファイルは管理しておらず、issues 専用のリポジトリです。

この zatsu リポジトリは、自由な状態の時や、日常の中で思いついたアイデアを issue として投稿する場です。

そして、精神的に冷静な状態の時(=酔っ払ってない時)に見返すことで、それが的外れだったのか、タスクにするべきかどうかを議論することができます。

f:id:mirakui:20150325201710p:plain
図: infra/zatsu リポジトリの様子

雑とは夢

雑とは、夢を語るためのスキームでもあります。

最高のサーバインフラとは、快適にサービスを提供でき、コストは低く抑え、障害が起こらず、メンテナンスフリーでセキュリティ的にも万全といったものになると思います。当然ですが、そんなものは存在しません。しかし、それを目指し、少しでもそれに近づくことはできます。

そのためには、自分たちの進むべき道とは何か、理想の状態とは何かについて、常に語り合えるチームでなくてはなりません。リーダーがビジョンを指し示すのはチームビルディングの基本ですが、全員が同じ方向を向き、納得をした上で仕事を進めていくためには、全員が自分たちの頭で何をやるべきかを考えることが大切です。

f:id:mirakui:20150325201757p:plain
図: 夢

良い雑、悪い雑

もちろん、発想の柔軟さは大切ですが、責任は伴います。いくら雑だからといっても、最終的にはタスクに落とし込まれ、自分たちでやらなければならないからです。実現できる技術力・行動力に裏付けされています。

勘違いしてはならないのは、インフラエンジニアには雑なオペレーションは許されないということです。サービスを守る立場の私達が、行動まで雑になってしまうことは絶対にあってはなりません。柔軟に発想しつつ、誠実に行動する必要があります。

雑さを許容するためには、チームメンバー同士の信頼関係と、互いの技術力に対するリスペクト、そしてユーザファーストの徹底が必須です。

図: 雑に発言し、誠実に行動する

雑アイデアをタスクへ

雑な発言を許容することは、常に志向を発散させることに他なりません。業務プロセスとして雑さを扱うためには、これを然るべきタイミングでうまく収束させる必要があります。

普段は issue で議論をし、ある程度発散できたタイミングでミーティングを開催し、合意形成をとります。そして、具体的な業務のタスクとして個人にアサインします。この段階で、雑だったアイデアは雑ではなくなり、責任をもってやり遂げなければならないタスクに変化します。このようにして、「夢」だった状態を少しずつ現実にしていけるのです。

おわりに

雑さを許容することで、発言のハードルが下がり、結果として変化に強いチームを作る方法を紹介しました。雑な発言が許容されつつ阿吽の呼吸で仕事が的確に進んでいくチームは、とても気持ちが良いものです。

インフラストラクチャー部では、柔軟に発想し、責任を持って理想に向かっていけるエンジニアを募集しています。

インフラエンジニア | クックパッド株式会社 採用情報

たとえば、CTOになる計画をたててみる

$
0
0

クックパッドで広告領域の企画や実装などを担当している大野です。

2015年期から広告領域ががふたつの事業部に分かれ、私は「新規広告開発部」に所属しています。この事業部は、新しい顧客や販路から収益を上げることと、既存を含む広告の配信を技術的に最適化して収益効率を向上させること、のふたつの目的から新設されました。

事業部に所属するメンバーは、営業やエンジニアといった職種に関わらず、それぞれ収益に対してコミットしています。そして、収益源やビジネスモデルはそれぞれ異なっています。

今回は、特にエンジニアがこうした環境において、やることおよびその優先度をどのように議論して決定しているかを紹介します。やりたいことやアイディアをどう出していくかについては本稿では議論しません。 ちょうど25日に公開された成田による議論が参考になります。

優先度 = 回収可能額 * 必要投資規模

いきなり結論めいた話ですが、優先度の決定は、どの程の投資に対してどの程度の回収が可能か。つまり、投資と回収の2軸から決定されるべきです。こう書くと (回収 - 投資) もしくは (回収 / 投資) で評価してソートすればいいような気もしますが、ご存知の通り、そんなに単純ではありません。

例えば、回収については時間軸で今期への算入額と将来まで含めた総額の二つで議論されるべきです。また、回収は目標金額を定められますが、投資の金額評価は難易度が高めです。そこに時間を使うのは無駄です。

というわけで、我々はそれぞれを感覚的に相対評価しています。そして、縦軸に回収、横軸に難易度をとる座標に施策をプロットして決めることにしています。

実例

それでは、早速今期の施策をプロットしたホワイトボードを、と言いたいところですが、あまりにリアルすぎて問題があるので、ご容赦ください。

今回は、読者の皆様に身近な話題ということで「CTOになる」を目標として、その達成のための施策について議論してみましょう。

なお、筆者は全くCTOになりたくありません。

とにかく書き出す

さて、早速施策を考えてみましょう。効果や優先度はあとから考えればいいので、思いついたことはしょうもなくても、とにかく書き出してみます。

  • a: 所属する会社の現CTOであるsec0ndlife氏に引退を迫る
  • b: 著名CTOであるn0oya氏に師事する
  • c: 一人でサービスを立ち上げて分社化し、そこのCTOを名乗る
  • d: 意識を高く保ってキャリアを積む

(※: IDは実在の人物とはあまり関係ありません)

評価

さて、それぞれの施策を評価しましょう。

まず、aに関しては、非常に筋が悪そうです。というのも、sec0ndlife氏はまだ30代であり、仕事にも燃えていそうです。そもそも、彼が引退することとあなたがCTOになることは全く直結しません。冷静になりましょう。

bに関しては、なんと、n0oya氏が最近弟子を募集する旨のブログを書いていました(ということにしてください)。また、様々な環境でCTO的な仕事をしてきた方の指導はわりと効果がありそうです。とはいえ、CTOも経営者なのでつまるところ実践あるのみな気もします。

cに関しては必勝です。ただ、分社化を認められるほど成功するサービスを作るのは難易度が高く時間もかかりそうです。

dに関しては、まぁ、頑張ってくださいという感じでしょうか。cよりは確度が低いが、投資もそれなりという感じですね。

プロットしてみましょう。

f:id:shin1ohno:20150329225811p:plain

実際には、もっとたくさんの、数十個の施策をプロットするわけですが、ここで、「こっちのほうが効果がたかそうだ。なぜなら〜〜」とか、「こっちのほうが難易度高いでしょう」みたいな議論をすることに大きな意味があります。メンバー間の施策に対する理解を揃えたり、思い込みを考え直せたりといった効果があります。目的に対する情熱が高すぎるとaのような勘違いが生まれがちです。

我々は、4半期に1日、割り込みを入れないように社外の会議室を借りて優先度設定をします。このなかで、アイディア出しに30分、プロットに3〜4時間かける感覚で進めています。プロットしているうちに新しいアイディアが出てくることもあります。

そして、最も大事なのは、回収の評価を「達成できたこと」を前提にするということです。達成の難易度に関しては横軸の議論なので、議論を分ける必要が有ります。ほとんどの人が直感としてdの評価に違和感があると思います。ただ、「日々の誘惑に負けずに毎日意識を高くた保てる」ことに成功すればこのくらいの効果がありそうという評価をすればこのくらいが正当なのではないでしょうか? 2軸評価、論点を分けることの優位な点です。ソフトウェア開発における、 "divide and conquer"ですね

さて、プロットされる位置によってこういう判断になります。

f:id:shin1ohno:20150329225619p:plain

と、いうわけで、ひとまず意識を高めて日常を送りつつ(d)、自分のサービスを立ち上げて分社化する夢を追う(c)。というのが直近の動きでしょうか。

アップデート

とはいえ、CTOになるのが夢の方なら、日常的にアイディアがでてくるはず。そうしたら、いままで出てきたのとの相対比較をして追加します。

わりと大事なのが、このアップデートの作業です。我々は四半期に1回くらいアップデートします。

優先度付けが終わったら

こうした優先度付けは、粒度としてやるべき「領域」を定義したにすぎません。それぞれの領域を分解して「仕事(タスク)」として定義し、スケジュールに落としていく必要があります。仕事として定義されてしまえば、Pivotal Trackerなどに入れてしまえばいいでしょう。

また、今回の例のdのような、日々の意識に関しては、チームミーティングなどで定期的にどういった意識付けができているか、そのテーマでどういった成果を出すことができたかの確認をするといいです。

また、各領域にコミットするのは誰か、という担当者の決定もしておくと良いです。実際にはチームとしてタスクを共有するわけですが、日々の意思決定を主体性を持ってできる人がいることは重要です。毎日の小さな意思決定が全体の大きな差を生むというのは、日々のコーディングを積み重ねてアプリケーションを作る作業と一緒です。

というわけで

さて、今回は、ある目標の達成に対して、やるべきことや優先度をどうやって決定するかを紹介しました。肝になるのは

  • 投資と回収をそれぞれ独立した議論で相対評価する
  • 投資と回収の2軸で2次元座標にプロットする。ここで議論を深める
  • プロットを元にやるべきことを決め、担当者とスケジュールを決める

ということです。優先度評価というものは正解のない議論ですが、皆様の役にたてると幸いです。また、クックパッドでは、何をするべきかから議論を主導して、決めた目標の達成をドライブすることに燃えるエンジニアを募集しています。

コードレビューに費やす時間を短くする

$
0
0

はじめに

こんにちは、広告事業部の芳賀(@func09)です。普段はクックパッドの広告配信周りや純広告・タイアップ広告などの商品開発を行っています。

私が広告事業領域の仕事をするようになって、そろそろ1年になるのですが、初めはエンジニア以外の人(営業、編集、広告入稿、レポート、メール配信、などなど様々な担当者がいます)と業務をすることが多くてコミュニケーションが上手くいかず業務がスムーズに進まないことがありました。

当たり前のことではありますが、エンジニアにしかわからない言葉は使わないとか、できるだけ相手の業務を理解し相手の考え方や視点に立って話すなど、ちょっと工夫することで、長引きがちなMTGや相談がすんなり終わったり、お互い良い気分で終わることが多くなって、費用対効果が高いなと感じています。

一方でエンジニア同士のコミュニケーションでも時間がかかってコストが高いと感じることがあります。それはプルリクエストを使ったコードレビューでのコミュニケーションです。

今回はコードレビューにかかる時間的コストをさげて、生産性を上げてこ!という話をしてみようと思います。

コードレビューの良さと悪さ

弊社では自分の書いたコードをいきなり本番環境にマージするなんてことはなく、まずプルリクエストを送ってチームメンバーからコードレビューを受けたり議論したり、コラボレーションしながら開発を進めています。

コードレビューにはプロダクトの品質向上に繋がるという素晴らしい良さがある一方で、コードレビューにかける時間やプルリクエストが送られてくるタイミングは予測しづらく、結果的に自分の作業にかけられる時間が減ったり、頻繁なコンテキストスイッチが発生して集中力が下がってしまうデメリットを感じています。

それらはコードレビューから受ける恩恵と比べたら、大したデメリットではないとも思うのですが、できるだけ自分の生産性は下げたくないし、同様に他のエンジニアの生産性も下げずに済むにはどうすればいいか?そんな事を考えていました。

コードレビューを開始するまでの時間を短縮する

私にとってコードレビューに時間のかかるプルリクエストは

  • 規模が大きく、差分が多い
  • 知らない機能、文脈のわからない変更についてのプルリクエスト
  • コードを読まないと変更点の意図が理解できない

というような特徴を持っていました。「規模が大きい」という点は、プロジェクトによっては差分を小さくできないこともあるので仕方ないと思います。 しかし他2点は、コードを読む前にレビュアーに効率良くインプット可能で、回避可能なことなので、そこは工夫していこうと思っています。

そこで私は「レビュアーがコードレビューを開始するまでの時間を最短にする」という事を目標に、プルリクエストを送るようにしています。

レビュアーがコードレビューを開始するまでの時間を最短にするには

コードレビューでは

  • マージする価値があることがわかる
  • リスクについて十分考慮されている(明らかなバグが見つからない)ことがわかる
  • コードのメンテナンスが可能であることがわかる

という点のいくつか、またはすべてをレビュアーは見ていると思います。 文脈を良く理解している、もしくは熟練のレビュアーならコードを読むだけでもチェック可能かと思いますが、コードを読む負担を可能な限り減らすために以下のような工夫をしています。

  • 伝える内容を選択する
  • 無駄な情報は捨てる
  • 注意を引く

規模にもよりますが、説明文を読んで1分くらいでコードレビューに入れる状態が望ましいと考えています。

伝える内容を選択する

たくさん書きたくなってしまったり、逆にめんどくさくてちょっとしか書かなかったり、そういうブレがないように伝える項目を決めておきます。

私は次の2つの項目を必ず入れるようにしています。

  1. このプルリクエストの目的
  2. 目的達成のためにやったこと(手段)

「目的」はできるだけひと言で表現し、「手段」は箇条書きで表現します。

目的は、そのプルリクエストをマージするとどういった価値があるのかシンプルに表現します。 手段は、目的を達成するためにどういう実装をしたのか表現します。

それらは「何をやるの?」と詳細に降りていったり、逆に「なぜ必要なの?」と遡れる関係にあるとレビュアーの理解の助けになると思います。

f:id:func09:20150330174121p:plain

無駄な情報は捨てる

説明文を読むのに時間がかかりすぎるのは、逆に混乱を招くので必要な部分だけにそぎ落としておきます。

チームメンバーにメンションを送る前に、もう一度読み返してみて、長文になっていたり、論点からはずれている説明があれば削除するか、ページの下の方に補足として読みたい人だけ読んでね、というスタンスに組み立てなおします。

その他の内容はケースバイケースで選びます、レビュアーに他部署の人がいれば、文脈共有のために「経緯」を書いたり、UIの変更点が重要な場合は画面キャプチャを貼ると理解が早いので積極的に利用しています。

下記はこれまでの内容を元に架空のプルリクエストの説明文を作文してみました。 私が送るプルリクエストの説明はだいたいいつもこれくらいの文量です。

f:id:func09:20150330174211p:plain

単純な例ですが、これを見た人がどんなファイルにどんな変更点が加わるのか、Diffを見なくてもなんとなくわかる状態が作れていれば、「コードレビューを開始するまでの時間の短縮」に繋がって、レビュアーの時間が少しでも多く確保できるのではないかと思っています。

注意を引く

f:id:func09:20150330174228p:plain

伝える内容を選択して無駄を捨てた上で、レビュアーの理解を助けるために注目すべき点を明瞭にします。

例えば

レビュアーをサポートしたいなら

  • どこから読み始めると理解しやすいのかコメントする
  • 読む必要がない箇所を教える

レビュアーに安心感を与えたいなら

  • テストがきちんと通っていることを伝える
  • マージ後の動作確認フローを伝える

レビュアーにヘルプを求めたいなら

  • 実装で迷っている点を伝える
  • リスクがありそうな箇所を伝える

などです。

すべてを毎回いれるというよりは、必要に応じていれることがあります。 相手の思考を先読みして、相手にどういう行動をとって欲しいのか注意を引くことでコントロールします。

さいごに

いかがでしたでしょうか? 本記事ではほとんど触れていませんが、コードそのものが読みやすいことが何より1番大事なのは言うまでもありません。

エンジニアはリーダブルコードのような、よりシンプルに明瞭にするという考え方が根付いているので、あえて書く必要がないようなことだったかもしれません。いろいろと書きましたが最終的には「変更点が相手に伝わっている」状態が作れていれば、どんなプルリクエストでも良いと思います。

この記事が皆さんや周囲のエンジニアの生産性向上の助けとなり、より良いアウトプットに繋がれば幸いです。

最後に、私の所属している広告事業部ではエンジニアを募集しておりますので、興味を持たれた方は是非ご応募ください!

新卒ソフトウェアエンジニアのための技術書100冊

$
0
0

こんにちは、技術部 高井です。

春といえば、フレッシュマンの季節ですね。このブログを読む方の中には、明日からエンジニアとして新社会人になるという方もいらっしゃるのではないでしょうか。クックパッドでも新しい仲間を迎えるための準備をしていたところで、その準備の一環として「新卒ソフトウェアエンジニアのための技術書100冊」というものを作成しました。

この100冊は、職業ソフトウェアエンジニアとしてキャリアを積むにあたって、読むべき技術書に悩んだら、まずはこのリストから選ぶとよいのではないでしょうかという提案です。

リストに多少の趣味や主張がはいっているのは、まあご愛嬌ということでお許しいただければとおもいますが、職業プログラマとして知っておくべき知識を網羅できるように心がけました。古典と呼ばれる名著についてはできるだけ取りいれ、独習が難しい難解なコンピュータサイエンスの教科書は避けています。これは必読書だろうというものでも、絶版や版元品切れなどで入手が難しいケースであれば、リストから外してあります。その他、(本当は臆せずに読んで欲しいのですが)洋書は避けました。

また、リストには便利のためにジャンルと難易度を付してあります。★なしは、ソフトウェアエンジニアを目指さない方でも読んで理解できる本、★ひとつは入門者向けの本で、★が増えるにつれて難度が増していきます。

この100冊は、読みごたえのある本ばかりです。ですから、経験によっては読んでもまったく理解できないという場合もあるでしょう。新しい知識を身に付けようというのですから、分からないこと、知らないことが書いてあるのが当たり前です。

私が尊敬するソフトウェアエンジニアの先輩のうちの一人である、ただただしさんも「改訂新版 コンピュータの名著・古典100冊」という本に寄せた文章で、技術書を読んで「さっぱりわからなかった」経験について触れています。そして、そういった本は、経験を積むにつれて「一読しても理解できなかった本は、しばらく時間をおいてから読み返すと、その価値が何倍にもなって返ってくる」とも書いています。

ソフトウェアエンジニアの仕事は難しく、色々なことを知らなければならない場面も多くあります。ぜひこの100冊のリストが、あなたのキャリアにとって役に立ちますように!

新卒ソフトウェアエンジニアのための技術書100冊

ジャンル難易度タイトル
AndroidJava言語プログラミングレッスン第3版
Android★★Android Layout Cookbook アプリの価値を高める開発テクニック
Android★★Android Security
Android★★Effective Java 第2版
iOSミクシィ公認 スマホアプリ開発実践ガイド
iOS★★iOSアプリ テスト自動化入門
iOS★★iOS開発におけるパターンによるオートマティズム
JavaScriptJavaScript本格入門 ——モダンスタイルによる基礎からAjax・jQueryまで
JavaScript★★JavaScriptパターン ——優れたアプリケーションのための作法
RubyたのしいRuby 第3版
RubyRuby 1 はじめてのプログラミング
RubyRuby 2 さまざまなデータとアルゴリズム
RubyRuby 3 オブジェクト指向とはじめての設計
Ruby★★Rails3レシピブック 190の技
Ruby★★The RSpec Book
Ruby★★初めてのRuby
Ruby★★Ruby on Rails 4 アプリケーションプログラミング
Ruby★★★Rubyのしくみ ——Ruby Under a Microscope
UNIX入門UNIXシェルプログラミング
UNIX★★ふつうのLinuxプログラミング
UNIX★★プロのための Linuxシステム構築・運用​技術
WebWebを支える技術 ——HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)
WebスラスラわかるHTML&CSSのきほん
Web★★ハイパフォーマンスブラウザネットワーキング
Web★★ハイパフォーマンスWebサイト
Web★★実践 Web Standards Design ——Web標準の基本とCSSレイアウト&Tips~
アルゴリズム★★★アルゴリズムクイックリファレンス
アルゴリズム ディジタル作法 ——カーニハン先生の「情報」教室
アルゴリズム CODE ——コードから見たコンピュータのからくり
エッセイ ウェブ進化論 ——本当の大変化はこれから始まる
エッセイ伽藍とバザール
エッセイ理科系の作文技術
エッセイ ハッカーと画家
オブジェクト指向オブジェクト指向のこころ
オブジェクト指向★★増補改訂版Java言語で学ぶデザインパターン入門
オブジェクト指向★★★オブジェクト指向入門 第2版 原則・コンセプト
オブジェクト指向★★★オブジェクト指向入門 第2版 方法論・実践
サービス開発ユーザビリティエンジニアリング 第2版
サービス開発★★Running Lean ——実践リーンスタートアップ
サービス開発★★Lean Analytics ——スタートアップのためのデータ解析と活用法
サービス開発 リーンスタートアップ
セキュリティ暗号解読
セキュリティ★★新版暗号技術入門
セキュリティ★★体系的に学ぶ安全なWebアプリケーションの作り方 ——脆弱性が生まれる原理と対策の実践
セキュリティ★★Hacking: 美しき策謀 第2版 ——脆弱性攻撃の理論と実際
ソフトウェア開発UNIXという考え方
ソフトウェア開発リファクタリング・ウェットウェア
ソフトウェア開発ピープルウェア
ソフトウェア開発コーディングを支える技術
ソフトウェア開発達人プログラマー
ソフトウェア開発★★スーパーエンジニアへの道
ソフトウェア開発★★継続的デリバリー
ソフトウェア開発★★★組織パターン
データベースSQL ゼロからはじめるデータベース操作
データベース楽々ERDレッスン
データベース★★★ハイパフォーマンスMySQL
データベース★★★プログラマのためのSQL 第4版
デザイン情報デザインの教室
デザイン誰のためのデザイン
デザイン★★インタフェースデザインの心理学
デザイン★★マイクロインタラクション
デザイン ノンデザイナーズ・デザインブック
テスト知識ゼロから学ぶソフトウェアテスト 【改訂版】
テスト★★ソフトウェアテスト技法ドリル
テスト★★★テストから見えてくる ——グーグルのソフトウェア開発
テスト★★★ソフトウェア・テストの技法
テスト★★★実践アジャイルテスト
ネットワークネットワークはなぜつながるのか 第2版
ネットワーク★★インフラ/ネットワークエンジニアのためのネットワーク技術&設計入門
ネットワーク★★マスタリングTCP/IP 入門編
バージョン管理入門Git
バージョン管理GitHub実践入門
プログラミングリーダブルコード
プログラミング★★実践テスト駆動開発 ——テストに導かれてオブジェクト指向ソフトウェアを育てる
プログラミング★★プログラミング言語C
プログラミング★★珠玉のプログラミング
プログラミング★★詳説 正規表現
プログラミング★★★クリーンコード
プログラミング★★★コードコンプリート
プロジェクトアジャイルサムライ
プロジェクト★★熊とワルツを ——リスクを愉しむプロジェクト管理
プロジェクト★★デスマーチ 第2版 ——ソフトウエア開発プロジェクトはなぜ混乱するのか
プロジェクト★★アート・オブ・アジャイルデベロップメント
プロジェクト★★アジャイルな見積りと計画づくり ——価値あるソフトウェアを育てる概念と技法
プロジェクト★★人月の神話
関数プログラミングプログラミングの基礎
関数プログラミング★★関数プログラミング入門
機械学習★★集合知プログラミング
機械学習★★入門 機械学習
数学いかにして問題をとくか
数学論理学
数学★★プログラミングのための確率統計
数学★★プログラマの数学
設計★★UML モデリングのエッセンス 第3版
設計★★ユースケース駆動開発実践ガイド
設計★★リファクタリング
設計★★間違いだらけのソフトウェア・アーキテクチャ ——非機能要件の開発と評価
設計★★★エンタープライズ アプリケーションアーキテクチャパターン
設計★★★エリック・エヴァンスのドメイン駆動設計
設計★★★レガシーコード改善ガイド

PCサイトのデザインをスマートフォンサイトに移植しようとして苦労した話

$
0
0

クックパッド検索・編成部の須藤耕平です。

昨年の夏に担当した、PCサイトのトップページリニューアルに引き続き、今年の2月にスマートフォンサイトのトップページをリニューアルしました。

前バージョンのスマートフォンサイトは、設計時から約1年半が経過していたため、コンテンツが増えた現在では全体的にかなり煩雑になっていたこと、また、PCサイトでの成功事例をスマートフォンサイトにも取り込むことを主眼として取り組んだリニューアルでした。

本エントリーでは、今回のリニューアルにあたって、主にデザイン的な面で工夫した点、及び、それにまつわる苦労話を、具体的な事例と合わせて紹介したいと思います。

PC版をそのまま移植したらものスゴイ長いページになった

今回のケースでは前述のPCの事例が先行してあり、結果も概ね良好であったため、当初は特にコンテンツを絞らず、PC版とほぼ同内容を移植する形で進めていたのですが、実際にやってみると、ものすごく長いページになってしまいました。

f:id:sudokohey:20150401115644p:plain

実は、事前の情報収集として、普段クックパッドを利用しているユーザーの方が他によく閲覧している国内サイトを中心に、いろいろなサイトのトップページを調査していたのですが、よく整理されていると感じるサイトで3000px程度、比較的長い状態が許容されている傾向にある女性系のサイトでも5000px前後であったのに対し、上記の案は6000px弱ありました。
一番アクセスの多い画面サイズ(320x568)で見た場合、10画面以上スクロールしないと、ページの最下部に到達しない計算です。

そこで、クックパッドのサービス全体を端的に表現しているPCサイトのファーストビューの構成要素を中心に、いくつかの利用頻度の高い要素を厳選して縦に展開する構成を検討しました。

f:id:sudokohey:20150401115755p:plain

PC版のファーストビューの構成を決める際に意識したのは以下の2点で、

  • サービス全体を俯瞰できるナビゲーションを備える
  • 鮮度が高い情報を高い更新頻度で掲出し、いつアクセスしても魅力的な情報がある

最低限この2点を満たせば、クックパッドの「顔」として成立させることができるという仮説のもと、大半の要素を削る方向で再検討しました。

最小限の要素を最小限のラベルで残したら、無味乾燥なページになった

厳選して要素を絞ったこともあり、なんとか3000px程度に全体をまとめることができたのですが、 従来のサムネイルを多用した、賑やかな雰囲気とは一転し、固定のナビゲーションとテキストリンクが連続する、単調で、非常にドライな印象のページとなってしまいました。

f:id:sudokohey:20150401115825p:plain

機能的な面での利便性を追求することは悪いことではありませんが、クックパッドの根幹にはコミュニティサイトとしての非常にセンシティブな側面があります。

レシピを始めとする様々な食関連のサービスを便利に使って貰ったその先に、多くの人たちが料理をしたり食事をしたりするというリアルな体験が確実に存在し、そうした本来的な価値が日々産み出されているということが十分に伝わるだろうか?「毎日の料理を楽しみに」を体現するサービスの「顔」としてふさわしいものになっているだろうか?というブランドイメージの観点で見ると、ちょっと機能的な方向に振りすぎた嫌いがありました。

機能的訴求と情緒的訴求のバランスをとる

PC版ではファーストビュー内にコンパクトに収まったサービス一覧ですが、スマートフォンの画面に展開すると、それだけでかなりの面積を占有してしまいます。
とは言え、「レシピの他にも様々なサービスを展開している」という全体感を伝えるためには、これを削れません。

そこで、以前から固定のレシピが大きく掲載されていたピックアップレシピの部分に、カルーセルUIを採用しました。

f:id:sudokohey:20150401115846p:plain

カルーセルの個々の内容は、従来のピックアップレシピを先頭に、

  • 10人以上からつくれぽが届いた話題のレシピ
  • 「春野菜」など、旬のカテゴリを含んだ主要なレシピカテゴリからのおすすめレシピ
  • プロのレシピや、プレミアム献立などの有料コンテンツに含まれるレシピ

といった、各コンテンツへの送客導線を兼ねた魅力的なレシピの紹介で構成されています。
これによって、理路整然と整理された機能的な訴求では満たせない、情緒的な面での訴求を両立できればと考えました。

また、特定の作りたいレシピが決まっているわけではなく、今日の夕飯どうしよう?とぼんやりした気持ちでサイトに訪れた際にも、ページをスクロールせずに、この部分を何度かスワイプするだけでヒントがもらえるという状態は、ユーザーの視点に立っても(こちら側の思惑を無視した状態でも)十分便利に使ってもらえると感じたことから、採用を決めました。

目的次第では賛否の分かれるUIではありますが、今回のケースでは、自動スクロールではないにもかかわらず、2枚目以降も最初のパネルの10%程度のタップ数を獲得することができており、概ね想定通りに利用して頂けているようです。

もう一点、単調な印象を与える原因となっていた、テキストリンクの連続を解消するために、要素の変化しないサービス一覧の中に、1箇所だけ、動的なテキストリンクを挿入するといった工夫も全体のスリム化に効果がありました。

f:id:sudokohey:20150401115900p:plain

固定のナビゲーションの中でこの部分だけがランダムに切り替わるため、変化に気付きやすいといった視覚的な効果もあり、結果としても、面で展開していた状態と比較しても遜色ないタップ数を獲得できました。

また、プレミアムサービスの一覧にも同様の仕様でリンクを設けており、非常にバリエーションの多いプレミアムサービス用のクリエイティブを、デザイン的な一貫性を保った状態で掲出できるようになりました。

f:id:sudokohey:20150401115913p:plain

まとめ

上記の変遷を経て、最終的には以下の構成での公開となりました。

f:id:sudokohey:20150401115926p:plain

前バージョンの半分程度に全体を収め、コンテンツの数(配置したリンクの総数)も60%程度にまで絞りましたが、トップページ全体のCTRとしては前バージョンをわずかに上回りました。

また、興味深い点としては、長大で具体的なコンテンツを多数並べた構成から、簡潔なナビゲーション中心の構成に切り替えたにもかかわらず、ページの平均滞在時間にほぼ変化がなかった点で、これは前述の機能的訴求と情緒的な訴求のバランスをうまくとることができたことによる結果と考えています。

最後に

最近では新規に立ち上げるサービスや、新コンテンツのトップ画面などをデザインする際、必ずスマートフォンでのデザインを先に検討するようにしています。
本当に大事な要素は何かを見極めたり、それ以上削っては意図が伝わらなくなる一歩手前のラベリングを検討するなど、制約の多い中で取捨選択を繰り返すことで、サービスの骨子が明確になるからです。

今回は、逆の順番となってしまったため、非常に苦労しましたが、学びの多いリニューアルとなりました。

最後となりますが、クックパッドでは、こうしたスマートフォンならではのでのUI改善に興味を持っている、デザイナー及びエンジニアを随時募集しています。今回のエントリをきっかけにクックパッドに興味を持っていただけた方がいらっしゃいましたら、ご応募お待ちしております!


新規サービスの管理画面を短期間で見栄え良く実装する

$
0
0

こんにちは、クックパッド料理教室の京和です。

管理画面はほとんどのウェブサービスに存在し、ユーザサポートやサービスの状況・KPIなどを確認するために、スタッフが毎日利用するとても重要なものです。にも関わらず、新規サービスでは人員が不足していることから、ついおざなりなデザインや実装になりがちなのではないでしょうか。

今回はクックパッド料理教室で採用している、RailsのMountable EngineとBootstrapのデザインテンプレートを使った、見栄えがよくメンテナンスしやすい管理画面を短期間で実装する方法についてご紹介します。

Mountable Engineとは

Mountable EngineはRailsアプリケーション上で動く、ミニRailsアプリケーションのようなものです。 ミニと書きましたが、Railsアプリケーション(Rails::Application)はRails::Engineクラスを継承しており、Mountable Engineの実態はまさにこのRails::Engineクラスです。
こうした事ができるのはRailsのマイクロカーネルアーキテクチャのおかげです。

Railsの内部構造やその思想・哲学については少々古い資料にはなりますが、@amatsudaが執筆したWeb+DB PRESS Vol.58の特集「詳解Rails 3」で詳しく解説されており、非常にオススメです。

簡単な使い方紹介

Mountable Engineは以下のコマンドで簡単に作ることができます。

rails plugin new asterisk --mountable

実行すると #{Rails.root}/asterisk 以下にファイルが生成されます。 下記のとおり、Railsアプリケーションとよく似た構成になっていますね。

asterisk
├── Gemfile
├── Gemfile.lock
├── MIT-LICENSE
├── README.rdoc
├── Rakefile
├── app
│   ├── assets
│   ├── controllers
│   ├── helpers
│   ├── mailers
│   ├── models
│   └── views
├── bin
├── asterisk.gemspec
├── config
├── db
├── lib
└── spec

ControllerやViewはもちろん、Gemfileやroutes.rbなども用意されています。bin/railsコマンドもあるので、scaffoldなどのGeneratorを利用することも可能です。
こうして生成したMountable Engineは、本体のGemfileとRoutingに定義することでアプリケーションから呼び出す事が可能です。

Gemfile

gem "asterisk", path: "asterisk"

config/routes.rb

mount Asterisk::Engine => '/', constraints { subdomain: 'asterisk' }

ここではconstraintsのsubdomainオプションを使ってドメインを分けています。
Mountable Engineは通常のRailsアプリケーションとほとんど同じ感覚で書くことができます。Engineについての詳しい解説はRailsGuidesをご覧ください。

なにが嬉しいのか

Mountable Engineを使った際の利点は、端的に言うとアプリケーションを分ける場合と分けない場合の「いいとこ取り」ができることです。

管理画面を別アプリケーションとして実装する場合、コードの共通化、特にモデルのメソッドやValidation・Scopeなどの共通部分をどのように管理するかが課題となることが多いです。多くの場合シンボリックリンクやgit submodulesを使うと思いますが、実運用は厳しいのが実情だと思います。一方で、管理画面を同じアプリケーション内に実装した場合、ユーザ用アプリケーションと密結合することになり、セキュリティやメンテナンスコストについて不安を抱えます。

Mountable Engineの場合、名前空間とコードベースを分離しつつ、モデルのコードは共通して呼び出すことができますし、更には管理画面でしか使わないテーブルであれば、Engine側にのみモデルを定義すると言ったことも可能です。別のアプリケーションとして切り出したいといった場合も比較的容易にできるでしょう。

料理教室ではスタッフ向けの管理画面と先生向けの管理画面を別々のMountable Engineとして実装していますが、1つのアプリケーション内にこれらが混在していた場合、メンテナンスするのはかなり難しかったと思います。

Bootstrap Templateを組み込む

Bootstrapのデザインテンプレートは有償・無料含めて様々なものがあります。少しググれば沢山のものが見つかるでしょう。 クックパッドではAce - Responsive Admin Template(有償)やAdminLTE Template(無料)などが使われています。
数年前は有償の方がクオリティが高かった印象がありますが、最近は無償でもハイクオリティなものが増えてきているように感じます。

導入はほとんどの場合、ファイルをapp/assets以下などのAsset Pipelineのパス以下に置くだけでOKです。Mountable Engineの場合はアセットファイルも分離されるので、本体とは異なるデザインテンプレートも気軽に設置することができます。

欠点

Mountable Engineは名前空間が分かれてるとは言え1つのRubyプロセスで動いているため、相互で参照することが可能です。 モデルを共通化できることは便利ですが、一方でコンテキストが異なるアプリケーションが混在している状況でもあるため、さじ加減を間違えると技術的負債になってしまいます。例えば(どんなアプリケーションにも言えることですが)default_scopeは原則使わないほうがよいでしょうし、Concernに分けて実装すると言ったことも必要です。

また、子(Engine)から親(Application)への参照以外にも、やろうと思えば親から子のモデルを参照することもできてしまいます。当初は管理画面でしか使わなかったためMountable Engine内に実装したモデルが、仕様変更が続いた結果ユーザ向けアプリケーションからもよく参照されるようになっていた、と言った事例がありました。

サービスは日々変化していきますから、コードベースもそれに合わせて適宜リファクタリングをしていくべきでしょう。とはいえ、こうした問題は、通常発生する技術的負債とあまり変わらないと思います。

Appendix:

その他、管理画面で気をつけている事などを書いてきます。

管理画面に名前をつける

料理教室では私たちの管理画面を「Daddy」と呼んでいます。名前の由来はクックパッドの歴史に触れるので詳しくはお話できないのですが、他のサービスの管理画面でも同じように名前を付けている事が多いです。
名前を付けることで、ともすれば無機質な印象のする「管理画面」から、毎日扱う「サービス」となり、より愛情を注ぐことができます。愛着をもてる名前を考えることはとても重要だと思います。

ガイドラインをつくる

クックパッドではドメインの分離や認証方式・監査ログの取得など、管理用アプリケーションを実装する際のガイドラインが定められています。原則として、すべてのサービスの管理画面でこのガイドラインを満たすことが求められており、これらがセキュリティやコンプライアンスを担保しています。そして、それらの実装を支援するため、共通ログ基盤Figlogなどの様々なライブラリが存在しています。

おわりに

Mountable EngineとBootstrap Templateによる管理画面の実装は、私が料理教室事業部に配属された当初、管理画面がとても貧弱だったことからついカッとなってリニューアルしようと考えたことがきっかけでした。調査やBootstrap Templateの選定なども含めて3日程度で実装は完了し、メンバーからはとても喜ばれました。また、幾つかの新規サービスでも同様の構成が採用されることになり、サービス間での管理画面の実装方式が共通化されると言ったメリットも生まれました。
配属直後で高まっている意識と比較的余裕のある状況は、チャレンジのしやすい大きなチャンスと言えるかもしれません。

管理画面は毎朝必ず見るもので、利用する時間は業務の中でも大きな割合を占めます。管理画面を素敵にして毎日の業務を楽しくすることは、毎日の料理を楽しみにし続けていくためにとても重要なことだと考えています。

クックパッド料理教室ではエンジニアを募集しています。 サービスに興味をお持ちの方はぜひこちらからご連絡ください。

クックパッドで新規事業の立ち上げをやりたい若手エンジニア募集! by クックパッド株式会社

安定したリリースを継続するためのテストとテストレベルの話

$
0
0

こんにちは。技術部の松尾(@Kazu_cocoa)です。

安定したリリースを継続して回す為には、開発プロセスや実装も大事ですが、その中でどのような確認、テストを継続して行うかも大切になります。そこで、開発プロセスにおけるテストをどのように切り分けて、構築していくかという考え方に関して少し整理してみようと思います。

これにより、実施されているテストによって検出できる/できない不具合がどのようなものか、それが開発中のどこで防ぐことができるのかを整理できるようになってくると思います。また、安定したリリースを実現するためのボトルネック解消に向けて、どのレベルでテストを充実させると効率的にそれが達成できるかという所も考えることができるようになります。

テストレベルによるテストの区分け

テストレベルという言葉にも様々な定義がありますが、ここではざっくりとテスト対象となる範囲や領域を意味することにします。その中で、ここではUnit Test、Integration Test、Feature Test、という用語を使います。主にテスト対象となる範囲が順に広がることを想像してください。それらがどのようなことを確認するのかは後述していきます。

このようなテストレベルを考えると、開発物をリリースするまでに、どこで、どんなテストを構築して、安定したリリースにもっていくかという流れとその実際を考え易くなります。(多くのテストエンジニアの方々は単なる共通の理解を促進する為の枠組みなだけであることも知っているでしょう。)

以下ではそのテストレベルに関して、Androidアプリ開発において自動化されるテストを例に書いていきます。

  • テスト対象
    • プッシュ通知を受け取るAndroidアプリ
  • シナリオ

    • GCMにより端末がプッシュ通知を受け取ってから、intentによりActivityを起動する(アプリの外側の話(OSやネッワークの状態など)は除外)
      • 前提: GCMによるプッシュ通知がテスト対象の端末に到達する
        1. システムによりBroadcastされたプッシュ通知をアプリが受け取りintentを生成する
        2. 生成したintentをActivityにセットする
        3. ユーザが起動されたActivityを利用する
  • 補足

    • Androidはintentの受け渡しによってそれぞれのActivityを連携させます(Activityが画面を構築する一要素)
    • 開発中にこのintentが壊れたり、Activityを表示するときに参照される要素のいずれかが壊れると容易にアプリがクラッシュするという状態になります

この例に対して、Unit Test、Integration Test、Feature Testといったテストレベル毎に、よく実施される形のテストコードを例として書きます。それにあわせて、それぞれのテストレベルではどのようなことに注意しながら確認やテストを行うのかと補足していきます。

Unit Test

テスト対象のオブジェクトが正しく振る舞っているか?を確認するテストです。AndroidだとよくActivityUnitTestCaseやAndroidJUnit4を使って、特定の関数の動作を確認します。

例えば、以下のようなintentを生成するクラスがあった場合、その中の関数を対象にテストコードを書きます。

テスト対象例

publicclass ExampleIntent {
    publicstatic Intent createIntent(Context context) {
        Intent intent = new Intent(context, MainActivity.class);
        intent.setAction(Intent.ACTION_VIEW);
        intent.putExtra("example", "extra data")
        return intent;
    }
}

テストコード例

@RunWith(AndroidJUnit4.class)
publicclass ExampleIntentTest {
    @Testpublicvoid createIntentTest() {
        Intent intent = new ExampleIntent.createIntent(InstrumentationRegistry.getTargetContext());
        assertThat(intent, is(notNullValue()));
        assertThat(intent.getStringExtra("example"), is("extra data"));
    }
}

この段階では、非常に限定された範囲における確認を高速に実施できます。オブジェクトに対して使われる値の組み合せや異常な値が代入される時のテストコードも書いておけば、限定された領域において常に動作を確認できます。

Integration Test

複数の関係性を持つオブジェクトやActivityを絡めたテストを実施します。複数の関係したオブジェクトを跨いだ処理が、期待する動作をするかを確認します。

ここでは、実際にintentを受け取ったとして、そこから期待するActivityが正しく起動することを確認します。Unit Testよりもテスト実行に時間がかかります。ただ、後述するFeature Testと比べると十分に高速です。

テストコード例

以下では、先ほどのintentを使いMainActivityを起動する、ところを確認するテストコードになります。

@RunWith(AndroidJUnit4.class)
publicclass LaunchActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
    private Instrumentation instrumentation;
    private Instrumentation.ActivityMonitor activityMonitor;
    private Context context;

    public LaunchActivityTest() {
        super(MainActivity.class);
    }

    @Before@Overridepublicvoid setUp() throws Exception {
        instrumentation = InstrumentationRegistry.getInstrumentation();
        injectInstrumentation(instrumentation);

        super.setUp();  // injectInstrumentationを先に実施しないとsuper.setUp()が失敗するため
        context = InstrumentationRegistry.getTargetContext();
        activityMonitor = instrumentation.addMonitor(MainActivity.class.getName(), null, false);
    }

    @Testpublicvoid launchActivityByExampleIntent() {
        launchActivityWithIntent(
                context.getPackageName(), 
                MainActivity.class, 
                new ExampleIntent.createIntent(context));

        activityMonitor.waitForActivityWithTimeout(2000);
        assertThat(activityMonitor.getHits(), is(1));
    }
    
    @After@Overridepublicvoid tearDown() throws Exception {
        instrumentation.removeMonitor(activityMonitor);
        super.tearDown();
    }
}

上記では要素の表示までは気にしていません。表示も確認したい場合、例えばEspressoを使い、起動したActivityに期待する要素が表示されているとをassertとして確認も可能です。

この辺のレベルでは複数要素が絡むため、依存関係が発生することが多いです。その場合、Dependency Injectionを使うなりして、依存関係が発生する箇所をうまいこと分離して、独立した形でテストを実施する動きが活発です。

Unit Test、Integration Testの段階で、表示要素に対する操作を除く要素の組み合せをテストできていることが多いと思います。

Feature Test

特定の機能に注目し、それが目的を達成できるか確認します。ユーザがxxxを達成できること、というような粒度を意図しています。そのため、ここのレベルでのテスト自動化を行おうとするとEspressoやAppiumを使うことが多くなると思います。人手による確認やテストも増える領域です。

例えば、プッシュ通知を受け取ったアプリが正しくActivityを起動したとして、そのActivityに対して何らかのユーザ操作が実施される、ということを確認します。AndroidではEspressoで提供される関数を使ってみると以下のようなことができます。プッシュ通知の模倣は、実際にサーバから送るでも良いし、adbによるBroadcastの送信でも良いと思います。

@Testpublicvoid usersCanClickItems() {
    onView(withId(R.id.example)).check(matches(withText(R.string.example)));
    onView(withId(R.id.example)).perform(click());
}

このレベルでは人手で実施する必要があったり、自動化されたテストを行うにしてもIntegration Testなどに比べると十分に遅いです。そのため、このレベルで表示条件の網羅などを行うと必要以上に無駄を作ってしまいます。さらには、十分に確認を網羅できずに不具合を作り込んだままリリースしてしまうかもしれません。そのため、Unit TestやIntegration Testで実施可能なテストをできるだけ増やす方が望ましいです。(一般的にその方が良いとも言われますね。)

なお、このテストレベルのテストに多くを頼っている場合、リリース毎に大きな心労と労力を払うことになりますね。

ただ、このレベルにならないと確認できないことがあります。例えばシングルサインオン(以下、SSO)を行うような複数アプリの連携が必要になる類いのテストです。

弊社ではクックパッドアプリの他にも、撮るレシピといったアプリをリリースしています。これは、"写真を撮る"ことでレシピなどの情報を保存、必要なときに見るというアプリです。このアプリは、クックパッドアプリ本体アプリがインストールされているとSSOで簡単にログインすることができます。この場合、複数のパッケージを跨いだ形でテストを行う必要があります。Appiumやuiautomatorの機構を使うと以下の流れでテストを自動化可能です。

  1. クックパッド本体をインストール・ログインする
  2. 撮るレシピをインストール・(SSOで)ログインする

これにより、"SSOにより撮るレシピでログインできる"という機能を確認することができます。

この他、端末システムに依存するような類いのテストも、この粒度では自動化されたテストの一環として実施できます。例えば以下です。

  • シナリオの特定のタイミングでフライトモードに設定を変更して意図的に通信を遮断するようなテストケース
  • 特定の時間に設定を変更してアプリを操作するようなテストケース

テストレベルによる区分の締めとクックパッドにおける実情

Androidアプリを例に、大きくUnit Test、Integration Test、Feature Test、という3種類にテスト対象の領域を区分し、テスト対象とする範囲とどのようなことを確認するか、という流れを記述しました。

テストレベルの定義自体、属する組織や開発スタイルに依存するものなので一概にこれが正しいとは言い切れません。一方で、ある程度テスト対象の領域をこういう言葉で区分しておけば、どのレベルでどんな確認を担保するという話、安定したリリースサイクルをまわすにはここの機能で不具合発生が多いことがボトルネックだから、どのレベルでどういうふうに対応しよう、といった話ができるようになります。

各テストレベルに対する説明を書きましたが、弊社Androidアプリにおいても十分なテストはまだ整備されてません。ここでいう十分とは、コードの変更に対して正しくテストが失敗してmasterへマージする前に開発者が気づけるとか、変更に伴う不具合を検出しきれずにリリースすることを防ぐ、という粒度の話をさします。現在はテストを実施する速度に対するボトルネックとなるFeature Testのレベルからある程度テストを自動化してカバーしつつも、Integration / Unit Testへと手を入れ始めています。今後、さらにIntegration/Unit Testを増やしたり、テストし易いコードに修正していき、より安定したリリースの実現を目指しています。そして、より人は人に近いところのテストに力を注ぐことができる環境を作りたいですね。

その他のテスト

少しテストレベルとは脱線するのですが、最近よく耳にするxxxTestと呼ばれるテストをあげておきます。テストレベルでは単純にテスト対象とする範囲に注目していましたが、他にも周辺環境や確認したい箇所に集中して呼び名がある、という例です。

Hermetic Test

今回はテスト対象の周辺環境には言及していません。テスト対象の環境まで言及してみると、モックやフェイクサーバを用意した上で、閉じられた環境で実施するテストをHermetic Testと呼びます。

時折私はWireMockにJsonを与えたスタンドアローンのサーバと、iOS/Androidアプリを起動し、限定された範囲内でクライアントに着目したテストします。これも単純なHermetic Testの環境ですね。

GUI Test

最近よく耳にする各種ボタン操作に対する動作を確認したり、スクリーンショットでレイアウト崩れを確認する、ということに焦点を当てるテストはGUI Testとよく呼ばれます。(ソフトウェア品質知識体系ガイド -SQuBOK Guide-(第2版)より)

GUI越しに操作可能なテストを行うときは、自然とこの観点のテストを行う人が多いと思います。Androidでは、 screenrecordコマンドを使うことで、スクリーンショットだけれなく動画の撮影も用意に行えるので、iOSに比べて特別なツールなく確認できる範囲が広いのではないでしょうか。

まとめ

今回は、テスト対象の範囲を変化させながら、テストレベルという切り分けで実施されるテストの話を書きました。また、これらを使うことで、不具合作り込みのボトルネック解消であったり、よりプロジェクトに必要なテストの領域を考える手助けとなることを書きました。

テストエンジニアの方々からするとおそらく息をするような範囲の話ですが、多くの人も意識している/していないにせよ想像に沿ったものだったと思います。いずれにせよ、ここは定義問題であるだけ、という見方もできますが、その定義をある程度持っているだけで目的とその実施内容に対する考えを行い易くなります。

モバイルアプリの開発は、多くがサービス主体の開発に移ってきたように思えます。そのような中で、長く改善を続けていくにはどこかのタイミングで少しずつ整備された開発/テスト環境を整えることが大事です。そのとき、必要なテスト設計を行い、それを実施・評価するサイクルを回すことができるということは、質の高い製品の開発体制を築く礎になってきます。そんな時に今回の話が少しでも寄与できると嬉しいですね。

弊社ではこのような取り組みを共に実施し、より良いサービスを提供し続ける為にエンジニアを募集しています。ご興味のある方は、是非とも覗いてみてください。

クックパッドの課金を支える技術

$
0
0

f:id:eisuke-oishi:20150409133149j:plain

こんにちは、技術部の大石です。開発基盤グループで課金システムの担当をしています。

インターネットサービスの決済・課金システムの開発や運用は、サービスの根幹を支えるために正確性と機能性を満たさなくてはなりません。また同時に、価格や料金体系、決済手段のバリエーションでユーザーに利便性を提供する必要もあります。「堅牢性」「信頼性」と「柔軟性」「開発スピード」という相反する要素の両立が求められます。

その結果、決済・課金システムは適切な設計や運用を意識しないと複雑になってしまいがちです。

課金システムの開発、運用でよくある問題

複数の決済方法を同じサービスの上で共存させる難しさ

例えば、最初にクレジットカード決済を導入して、その後にコンビニ決済、キャリア決済やアプリ内決済と決済方法が増えていくことはよくあることです。 最初の導入の際にクレジットカード決済への設計だけでなく、その後に増えていく決済を見据えた適切な設計を行なわないと、コンビニ決済やキャリア決済、アプリ内決済をクレジットカード決済の流れに無理矢理押し込めてしまうような実装になってしまいます。

導入当初の段階で複数の決済の流れを把握した上での設計を行なうのは、決済導入の予定があるいくつかの決済のインターフェースや仕様の知識などが必要になったりするため、過去に導入を行なったことがある経験や知見が必要となったりします。 また一度走り始めると根本的な設計を見直すというのは難しくなってしまいます。

割引キャンペーンといった施策の残骸が残ってしまう

ユーザーにサービスの魅力を伝えるために、割引キャンペーンや無料キャンペーンなどの価格面での施策は必ずといっていいほど行ないます。 期間限定であったり、納期の短かいものが多いので、長く運用されるとそのような施策の残骸が残ってしまうことが起きがちです。 そのような残骸がコードの品質を低下させていくことになります。

複数サービス間での運用

クックパッドでも、プレミアムサービスの他にも、プロのレシピ産地直送便などのユーザー課金を行なうサービスがあります。 複数のサービスで決済を導入する場合、課金機能をそれぞれの部署で開発すると外部の決済サービスとの連携などは大半が同じようなコードになったり、運用の知見が部署毎に分散してしまうことが起きてしまいます。

クックパッドの解決方法

以上のような問題を解決するために、クックパッドでは共通決済基盤を開発するというアプローチを取りました。

共通決済基盤とは

クックパッドを起点とする全てのサービスから各種決済の導入ができるようにした、社内向けの独立したWEBサービスです。主な機能としては、

  • 各種決済サービスとの連携
  • 接続先との差異を吸収し統一的なAPI
  • 決済情報、履歴の管理
  • 決済情報の突合や有効性の確認
  • 決済に必要なユーザー情報の管理
  • 経理処理における集計機能
  • カスタマーサポートが履歴を確認したりキャンセルなどを行える管理画面

があります。

なぜWEBサービスとして独立させたのか

たとえば、ロジックをgemとして切り出したり、Rails Engineを使うなど共通のライブラリを使用して管理する方法も考えられます。 しかし、なぜ私達は共通決済基盤をWEBサービスとして独立させたのでしょうか。

それは、WEBサービスとして独立させることにより、共通決済基盤を利用するサービスへの言語、環境的な依存が無くなるというのも理由の1つです。それ以上に大きな理由として、クックパッドのサービスは「プレミアムサービス」を始め、継続課金モデルを採用しているサービスが多いということが大きな動機です。

継続課金は、その名の通り課金状態が継続されていくものなので課金の状態を常に確認する必要があり、都度課金に比べ購入後の管理が複雑になります。 運用の煩雑さを軽減することを考えた場合に、ノウハウが集約されたプラットフォーム上で管理することが必要だったからです。

サービス連携の流れ

それでは、具体的に共通決済基盤が行なっている継続課金の利用開始から、決済情報の突合、サービスとの連携を追ってみることにします。

継続課金の利用開始

f:id:eisuke-oishi:20150409133625p:plain

  1. ユーザーが利用開始のアクションをとる
  2. 決済開始の手続きを決済APIへリクエストする
  3. ユーザーが決済の手続きを行う
  4. 共通決済基盤がサービスへ決済の結果をAPIコールバックする
  5. ユーザーがサービスを利用できる

サービス側での付与が失敗した場合は、適切に共通決済基盤の情報をロールバックします。

決済情報の突合

突合とは、それぞれのサービスが持つ継続決済の一覧同士を比較し差分を検出することです。

f:id:eisuke-oishi:20150409133618p:plain

  1. 共通決済基盤が決済サービスのステータスと同期する
  2. 共通決済基盤からサービスへ差分情報を送信し、サービスは利用できないユーザーの有料機能利用を解除する
  3. 共通決済基盤とサービス側での差分がないかをチェックし、差があれば検知する

上記の処理を、各種決済サービスの情報と共通決済基盤との継続契約の差と共通決済基盤とサービスとの差が無いように毎日行ないます。

以上のような処理が決済手段ごとに存在するするため、各サービスの管理の手間を省くと同時にノウハウが集積された確実な運用を行なうためWEBサービスとして独立させました。

共通決済基盤と利用サービスとの責務の分担

次に共通決済基盤とそれを利用するサービスとの責務の分担について説明します。

課金に関連するストーリーとして、クックパッドでの代表的な例を考えてみます。

  • ユーザーが280円のプレミアムサービス(商品)をクレジットカード決済(決済方法)で、継続課金を契約(注文)し、毎月280円を課金(請求)する。
  • 課金ユーザーがプレミアムサービスの利用ができるようにする(認可)
  • 毎月ユーザーに280円を請求し、結果を検証する

共通決済基盤は「注文」や「請求」の管理や外部APIとの連携を担い、サービス側は「商品」と「決済方法」の選択、サービス利用「認可」を担うように責務を分担しています。

このように、

  • 利用サービス側では商品を割引するクーポンの発行を行なうなど、商品に関する様々な施策が実施
  • 注文や請求の管理、外部の決済サービスとの連携、決済情報の突合などは共通決済基盤のノウハウが集約されたプラットフォーム上で管理

することで、明確な責務の分担を行なっています。

都度課金も対応

WEBサービスとして独立させた理由として継続課金の例を示しましたが、都度課金への対応も行っています。 これにより、サービス開発者は都度課金と継続課金のどちらにも対応でき、ユーザーは継続課金で利用しているものと同じクレジットカード情報を使って都度課金も行なうことができます。

共通決済基盤を導入した結果

各サービスから共通決済基盤を利用することで、冒頭に挙げたよくある問題も解決することができました。

「複数の決済方法を同じサービスとして共存させる難しさ」は1箇所に知見を集約することでよい設計の上で動作させることができています。また、「割引キャンペーンといった施策の残骸が残ってしまう」ことは責務の分担を明確に行なうことで利用サービスでの柔軟性と共通決済基盤の信頼性を担保できています。「複数サービス間での運用」も新規サービスなどでの決済導入コストの削減や運用の負荷軽減に繋がっています。

このように、共通決済基盤を導入することで「堅牢性」「信頼性」と「柔軟性」「開発スピード」の両立ができるようになりました。

まとめ

決済、課金に関する開発や運用は、外部環境の影響が大きく、運用コストが高くなりがちなので頭を悩ますことが多い部分かと思います。クックパッドの例がそういった開発や運用のご参考になれば幸いです。

決済に関する環境は今後テクノロジーの進化や新しい決済方法の登場などによってこれからも進化していく分野です。 そういった外部要因とあわせてクックパッドのサービスの発展に対応していくためにも、共通決済基盤をより良いものに進化させていきたいと思っています。

クックパッドでは、そのような課題に取り組んでみたいというエンジニアを募集しています。

コードで行うMySQLのアカウント管理

$
0
0

インフラストラクチャー部の菅原(@sgwr_dts)です。

インフラストラクチャー部のメンバーはオペレーションのため強力な権限のMySQLアカウントを使用していますが、サービス開発をするエンジニアも業務のためにサービスのDBの参照・更新権限を持ったアカウントが必要になることがあります。

セキュリティやオペレーションミスのことを考えると、すべてのエンジニアのアカウントをスーパーユーザーにするわけにはいかないため、都度適切な権限を付与していますが、手動での作業は地味に手間がかかります。

そこでクックパッドではMySQLのアカウント情報をコード化し、リポジトリで管理するようにしています。

gratanによるコード化

MySQLのアカウント管理はgratanという自作のツールを使って行っています。 gratanを使うとMySQLのアカウントをRubyのDSLで記述することができるようになります。

require'other/grantfile'# 他の権限定義ファイルを読み込む

user "scott", ["127.0.0.1", "%"] do
  on "*.*"do
    grant "USAGE"end# test DBへの権限付与は2014/10/08まで
  on "test.*", expired: '2014/10/08'do
    grant "SELECT"
    grant "INSERT"end # test2 DBのresipe_*テーブルに対して権限を付与
  on /^test2\.recipe_/do
    grant "SELECT"
    grant "INSERT"endend

上記のDSLをMySQLに適用すると、たとえば以下のようなログが出力されます。

$ bundle exec rake apply[db_foo][WARN] User `scott@%`: Object `test.*` has expired
[WARN] User `scott@127.0.0.1`: Object `test.*` has expired
REVOKE SELECT ON `test`.`*` FROM 'scott'@'%'
REVOKE INSERT ON `test`.`*` FROM 'scott'@'%'
REVOKE SELECT ON `test`.`*` FROM 'scott'@'127.0.0.1'
REVOKE INSERT ON `test`.`*` FROM 'scott'@'127.0.0.1'
GRANT SELECT ON `test2`.`recipe_photos` TO 'scott'@'%'
GRANT SELECT ON `test2`.`recipe_photos` TO 'scott'@'127.0.0.1'
FLUSH PRIVILEGES

gratanは冪等性を保証しているので、MySQLの権限がすでに定義ファイル通りである場合には、なにも変更を行いません。

$ bundle exec rake apply[db_foo]
No change

また、expiredを記述すると、指定した日付以降にDSLを適用した場合に、期限の切れた権限をREVOKEするようになります。このため、現在の運用では定期的にDSLの適用を行い、期限が切れた権限がDBに残らないようにしています。

このような定義ファイルを各エンジニアごとに作成し、以下のようなディレクトリ構成でGitに保存しています。

repo
├── Gemfile
├── Rakefile
├── db_bar
│   ├── Grantfile
│   ├── alice.grant
│   └── bob.grant
└── db_foo
    ├── Grantfile
    ├── scott.grant
    └── tiger.grant

権限付与のワークフロー

エンジニアへの権限の付与は次のようなワークフローで行われます。

  1. エンジニアがリポジトリをFork
  2. 必要な権限を追加した修正をPull Request
  3. Pull Requestをレビューして問題がなければマージ
  4. マージしたリポジトリのアカウント情報をMySQLに適用

何がうれしいのか

オペレーションのしやすさ

権限付与の作業はrakeタスクで自動化されているので作業者はrakeコマンドを実行するだけでよく、オペレーションミスを防ぐことができます。

アカウントの見通しの良さ

すべてのエンジニアのMySQLアカウントはテキストファイルとしてGitに保存されているため、誰が、どのDBに対して、どのような権限を持っているかがすぐに把握できます。そのため不必要なアカウントがずっと残ってしまうような問題を防ぐことができます。

履歴が残る

Gitで管理しているので、いつ誰にどのDBの権限を与えたかがすべて残ります。またどのような理由にで権限が必要になったかについても、Pull Requestの形でレビューとともに履歴が残ります。

おわりに

今のところMySQLへの適用作業はインフラエンジニアが手動でコマンドを実行しているのですが、Pull Requestがマージされたタイミングで自動的に適用するのもそれほど難しいことではないため、できれば完全に自動化したいと考えています。

インフラストラクチャー部では、刺身タンポポを撲滅できるエンジニアを募集しています。

インフラエンジニア | クックパッド株式会社 採用情報

7つのサンプルプログラムで学ぶRxJavaの挙動

$
0
0

会員事業部の山下(@tomorrowkey)です。
RxJavaが流行ってますね。最近Android版クックパッドでもRxJavaが導入されました。この記事は私がRxJavaを使うにあたって検証用のテストコードを書いたものをベースに、RxJavaの挙動をみなさんに紹介したいと思います。

目次

  • リスト操作でおさらいする基本的なRxJavaの使い方
    • Observable
    • Operator
    • Observer / Subscribe
  • 実行順序を確認するサンプルプログラム
    • 7つのサンプルプログラム

リスト操作でおさらいする基本的なRxJavaの使い方

RxJavaはAPIアクセスやイベントトリガーやリスト処理などを多岐にわたる処理に使うことができます。このエントリでは初学者に一番分かりやすいリストの処理を例に解説します。
これは1から10までの値を渡し、偶数だけにフィルタリングしたうえ、値を10倍にして、ログ出力するというプログラムです。

Observable.from(new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) // 1
  .filter(new Func1<Integer, Boolean>() { // 2@Overridepublic Boolean call(Integer i) {
      return (i % 2) == 0;
    }
  })
  .map(new Func1<Integer, Integer>() {  // 2@Overridepublic Integer call(Integer i) {
      return i * 10;
    }
  })
  .subscribe(new Observer<Integer>() {  // 3@Overridepublicvoid onNext(Integer integer) {
      Log.d("Hoge", integer.toString());
    }

    @Overridepublicvoid onCompleted() {
      Log.d("Hoge", "completed");
    }

    @Overridepublicvoid onError(Throwable e) {
    }
  });

実行してみると以下のように出力されます。

20
40
60
80
100
completed

RxJavaの大まかな流れを説明すると以下のようになります。

  1. Observableを作る
  2. filterやmapなどのOperatorを使って値を加工する
  3. Observerを使ってObservableをsubscribeする

1つずつ解説します。

1.Observableを作る

データの元となるものをデータソースといいます。これはAPIのレスポンスだったり、ディスクに保存されているファイルだったり、単純にメモリ上の変数だったりします。 RxJavaではまずデータソースを提供するObservableを作る必要があります。
データソースを提供するObservableを作成する為のstaticメソッドがいくつか定義されています。今回は説明しませんが、データソースを提供するObservableは自作することができ、ファイルアクセスやAPIアクセスする場合は、自作する必要があります。
Observableを作るメソッドをいくつか紹介しましょう。

from

Observable.from(new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})

サンプルプログラムで使われているメソッドです。 配列を渡すことで、各要素をOperatorに渡します。 配列の他にListやIterableなどのOverloadがあります。

just

Observable.just(1, 5, 6)

10個までのオブジェクトをOperatorに渡します。 可変長引数ではないので、それ以上のオブジェクトを渡すことはできません。 配列やListではないオブジェクトをObservable化したい場合に使います。

range

Observable.range(1, 10)

startからcountまでのint値をOperatorに渡します。 サンプルプログラムではわざわざIntegerの配列を作っていましたが、これを使うことでより簡潔に書くことができます。

2.Operatorを使って値を加工する

Observableで作成されたデータを1つずつ受け取り、加工したり、フィルタリングしたり、他のObservableとマージしたりなどします。 いくつかのOperatorを紹介します。

filter

Observable.filter(new Func1<Ingeger, Boolean>() {
  @Overridepublic Boolean call(Integer i) {
  return (i % 2) == 0;
  }
})

名前の通り値をフィルタリングするOperatorです。trueを返せばその値を採用し、falseを返せばその値を取り除きます。 このコードではリストの値が偶数だけになるようにフィルタリングしています。

map

Observable.map(new Func1<Integer, Integer>() {
  @Overridepublic Integer call(Integer i) {
  return i * 10;
  }
})

受け取った値を違う値に変換するOperatorです。 このサンプルプログラムではIntegerの値を10倍にしています。 値の変換だけではなく、例えばIntegerからStringにしたりなど、違う型に変換することもできます。

3.Observerを使ってObservableをsubscribeする

ObserverではOperatorで加工した値を受け取ります。

onNext()は1つの値の処理が終わる度に実行されます。 onCompleted()ではすべての値の処理が終わったら実行されます。 onError()は一連の流れの中で例外が発生した時に呼ばれます。

Observable.subscribe(new Observer<Integer>() {
  @Overridepublicvoid onNext(Integer integer) {
  Log.d("Hoge", integer.toString());
  }

  @Overridepublicvoid onCompleted() {
  Log.d("Hoge", "completed");
  }

  @Overridepublicvoid onError(Throwable e) {
  }
})

Observable.subscribe()を実行することで、Observableがデータソースの準備をしてOperatorにデータを渡します。 リストをデータソースとして指定した場合、すぐにデータを提供できるのですぐに実行されます。 サンプルプログラムを分解して、解説すると

Observable observable = Observable.from(new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
  .filter(new Func1<Integer, Boolean>() {
  @Overridepublic Boolean call(Integer i) {
    return (i % 2) == 0;
  }
  }); // 1. まだobservableは実行されない

observable = observable.filter(new Func1<Integer, Boolean>() {
  @Overridepublic Boolean call(Integer i) {
    return (i % 2) == 0;
  }
  }) // 2. まだobservableは実行されない

observable.subscribe(new Observer<Integer>() {
  @Overridepublicvoid onNext(Integer integer) {
  }

  @Overridepublicvoid onCompleted() {
  }

  @Overridepublicvoid onError(Throwable e) {
  }
}); // 3. 実行される

1、2ではsubscribeするためのものを作っている段階なので実行されませんが、3で初めてsubscribeされるので一連の流れが実行されます。

サンプルプログラムをいくつか見よう

前節でRxJavaの基本的な動きはなんとなく分かったんじゃないかなと思います。
ここからは実行順序に関する7つのサンプルプログラムを提示します。変数の値がどのようになるか想像してみましょう。

ちなみにこれらのサンプルプログラムはこちらで公開しています。 https://github.com/tomorrowkey/RxAndroidTest

サンプルプログラム1

まずは簡単なサンプルプログラムです。 sbの内容はどうなるでしょうか

final StringBuilder sb = new StringBuilder();

Observable.just(1)
  .map(new Func1<Integer, Integer>() {
    @Overridepublic Integer call(Integer i) {
      sb.append("1");
      return i;
    }
  })
  .subscribe(new Observer<Integer>() {
    @Overridepublicvoid onNext(Integer integer) {
      sb.append("2");
    }

    @Overridepublicvoid onCompleted() {
      sb.append("3");
    }

    @Overridepublicvoid onError(Throwable e) {
      sb.append("4");
    }

  });
sb.append("5");

テストコード1

assertThat(sb.toString(), is("1235"));

前述の説明を理解できていれば簡単に分かったと思います。
Integerの値1をデータソースにリスト処理します。mapのOperatorが1度だけ実行され、ObserverのonNext()、onCompleted()の流れで実行されます。

サンプルプログラム2

今度は実行順序を確認するためにsleepを入れてみましょう。
sbの内容はどうなるでしょうか

final CountDownLatch latch = new CountDownLatch(1);
final StringBuilder sb = new StringBuilder();

Observable.just(1)
  .map(new Func1<Integer, Integer>() {
    @Overridepublic Integer call(Integer i) {
      sleep(500);
      sb.append("1");
      return i;
    }
  })
  .subscribe(new Observer<Integer>() {
    @Overridepublicvoid onNext(Integer integer) {
      sb.append("2");
    }

    @Overridepublicvoid onCompleted() {
      sb.append("3");
      latch.countDown();
    }

    @Overridepublicvoid onError(Throwable e) {
      sb.append("4");
    }
  });

sb.append("5");

latch.await(10, TimeUnit.SECONDS);

テストコード2

assertThat(sb.toString(), is("1235"));

特に何も指定をしなければ、subscribeを実行した時点で実行した時のスレッドを使って最後まで実行されるため、サンプルプログラム1と同じ挙動になります。

subscribeOn

RxJavaの一連の処理を実行するスレッドを指定したければ、subscribeOnを使います。

Observable.subscribeOn(Scheduler)

例えばAndroidのmainスレッドを使いたければ以下の様に指定します。

Observable.subscribeOn(AndroidSchedulers.mainThread());

mainではない別スレッドを使いたければ以下のように指定します。

Observable.subscribeOn(Schedulers.newThread());

リストの要素一つ一つに対してネットワーク処理を行いたい場合や、ディスクIOをしたい場合など、Operatorで重たい処理をしたい時には、スレッドを指定できるので便利ですね。

サンプルプログラム3

subscribeOnを使った際の実行順序を確認してみましょう。
sbの内容はどうなるでしょうか

final CountDownLatch latch = new CountDownLatch(1);
final StringBuilder sb = new StringBuilder();
Observable.just(1)
  .subscribeOn(Schedulers.newThread())
  .map(new Func1<Integer, Integer>() {
    @Overridepublic Integer call(Integer i) {
      sleep(500);
      sb.append("1");
      return i;
    }
  })
  .subscribe(new Observer<Integer>() {
    @Overridepublicvoid onNext(Integer integer) {
      sb.append("2");
    }

    @Overridepublicvoid onCompleted() {
      sb.append("3");
      latch.countDown();
    }

    @Overridepublicvoid onError(Throwable e) {
      sb.append("4");
    }
  });
sb.append("5");

latch.await(10, TimeUnit.SECONDS);

テストコード3

    assertThat(sb.toString(), is("5123"));

Operatorはmainではないスレッドで500msec待ってから実行されるので、先にmainスレッドが実行されました。

サンプルプログラム4

さらにどの部分がどのスレッドで実行されるか確認しましょう。
listの内容はどうなるでしょうか。

final CountDownLatch latch = new CountDownLatch(1);

final List<String> list = new ArrayList<>();
Observable.just(1)
  .subscribeOn(Schedulers.newThread())
  .map(new Func1<Integer, Integer>() {
    @Overridepublic Integer call(Integer i) {
      list.add("1:" + Thread.currentThread().getName());
      return i;
    }
  })
  .subscribe(new Observer<Integer>() {
    @Overridepublicvoid onNext(Integer integer) {
      list.add("2:" + Thread.currentThread().getName());
    }

    @Overridepublicvoid onCompleted() {
      list.add("3:" + Thread.currentThread().getName());
      latch.countDown();
    }

    @Overridepublicvoid onError(Throwable e) {
      list.add("4:" + Thread.currentThread().getName());
    }
  });

latch.await(10, TimeUnit.SECONDS);

テストコード4

assertThat(list.size(), is(3));
assertThat(list.get(0), is(matches("1:RxNewThreadScheduler-\\d")));
assertThat(list.get(1), is(matches("2:RxNewThreadScheduler-\\d")));
assertThat(list.get(2), is(matches("3:RxNewThreadScheduler-\\d")));

subscribeOnでのスレッド指定はObserverまで影響受けるので、OperatorだけではなくObserverのメソッドも別スレッドで実行されました。
AndroidではUIスレッド以外でViewの更新ができないので、このまま通信処理などのために利用することはできませんね。

ObserveOn

observeOnを使えばObserverが指定されたメソッドで実行されます。
subscribeOnで別スレッドを指定し、Operatorで重たい処理を実行して、Observerで処理した内容をUIに反映したいといった時にobserveOnを使います。

サンプルプログラム5

observeOnを使った時の実行順序とスレッドを確認しましょう。
listの内容はどうなるでしょうか

final CountDownLatch latch = new CountDownLatch(1);

final List<String> list = new ArrayList<>();
Observable.just(1)
  .subscribeOn(Schedulers.newThread())
  .map(new Func1<Integer, Integer>() {
    @Overridepublic Integer call(Integer i) {
      list.add("1:" + Thread.currentThread().getName());
      return i;
    }
  })
  .observeOn(AndroidSchedulers.mainThread())
  .subscribe(new Observer<Integer>() {
    @Overridepublicvoid onNext(Integer integer) {
      list.add("2:" + Thread.currentThread().getName());
    }

    @Overridepublicvoid onCompleted() {
      list.add("3:" + Thread.currentThread().getName());
      latch.countDown();
    }

    @Overridepublicvoid onError(Throwable e) {
      list.add("4:" + Thread.currentThread().getName());
    }
  });

latch.await(10, TimeUnit.SECONDS);

テストコード5

assertThat(list.size(), is(3));
assertThat(list.get(0), is(matches("1:RxNewThreadScheduler-\\d+")));
assertThat(list.get(1), is("2:main"));
assertThat(list.get(2), is("3:main"));

Operatorは別スレッドで実行され、Observerはmainスレッドで実行されるようになりました。 AsyncTaskのように見えてきませんか?だいぶRxJavaへ親しみがもてるようになってきたかと思います。

サンプルプログラム6

もうすこしsubscribeOnの挙動を見てみましょう。
2回subscribeOnを実行した場合にどうなるでしょうか。

final CountDownLatch latch = new CountDownLatch(1);

final List<String> list = new ArrayList<>();
Observable.just(1)
  .subscribeOn(AndroidSchedulers.mainThread())
  .map(new Func1<Integer, Integer>() {
    @Overridepublic Integer call(Integer i) {
      list.add("1:" + Thread.currentThread().getName());
      return i;
    }
  })
  .subscribeOn(Schedulers.newThread())
  .map(new Func1<Integer, Integer>() {
    @Overridepublic Integer call(Integer i) {
      list.add("2:" + Thread.currentThread().getName());
      return i;
    }
  })
  .observeOn(AndroidSchedulers.mainThread())
  .subscribe(new Observer<Integer>() {
    @Overridepublicvoid onNext(Integer integer) {
      list.add("3:" + Thread.currentThread().getName());
    }

    @Overridepublicvoid onCompleted() {
      list.add("4:" + Thread.currentThread().getName());
      latch.countDown();
    }

    @Overridepublicvoid onError(Throwable e) {
      list.add("5:" + Thread.currentThread().getName());
    }
  });

latch.await(10, TimeUnit.SECONDS);

テストコード6

assertThat(list.size(), is(4));
assertThat(list.get(0), is("1:main"));
assertThat(list.get(1), is("2:main"));
assertThat(list.get(2), is("3:main"));
assertThat(list.get(3), is("4:main"));

subscribeOnはOperatorが実行されるスレッドを指定するもので、実行した時点でスレッドを変えるような効果はありません。 よって、先に実行されたmainスレッドの指定が採用され、すべてmainスレッドで実行されました。

サンプルプログラム7

さきほどのサンプルプログラムでだいたいsubscribeOnの挙動は分かったと思いますが、もう1つだけ確認してみましょう。 途中からsubscribeOnを指定した場合どうなるでしょうか。

final CountDownLatch latch = new CountDownLatch(1);

final List<String> list = new ArrayList<>();
Observable.just(1)
  .map(new Func1<Integer, Integer>() {
    @Overridepublic Integer call(Integer i) {
      list.add("1:" + Thread.currentThread().getName());
      return i;
    }
  })
  .subscribeOn(Schedulers.newThread())
  .map(new Func1<Integer, Integer>() {
    @Overridepublic Integer call(Integer i) {
      list.add("2:" + Thread.currentThread().getName());
      return i;
    }
  })
  .subscribe(new Observer<Integer>() {
    @Overridepublicvoid onNext(Integer integer) {
      list.add("3:" + Thread.currentThread().getName());
    }

    @Overridepublicvoid onCompleted() {
      list.add("4:" + Thread.currentThread().getName());
      latch.countDown();
    }

    @Overridepublicvoid onError(Throwable e) {
      list.add("5:" + Thread.currentThread().getName());
    }
  });

latch.await(10, TimeUnit.SECONDS);

テストコード7

assertThat(list.size(), is(4));
assertThat(list.get(0), is(matches("1:RxNewThreadScheduler-\\d+")));
assertThat(list.get(1), is(matches("2:RxNewThreadScheduler-\\d+")));
assertThat(list.get(2), is(matches("3:RxNewThreadScheduler-\\d+")));
assertThat(list.get(3), is(matches("4:RxNewThreadScheduler-\\d+")));

途中でsubscribeOnを指定したとしても最初のOperatorからスレッドが変わります。
でも途中にsubscribeOnを書くと可読性が落ちるので最初に書くと分かりやすいでしょう。

最後に

リスト操作を通じてRxJavaの挙動を確認しました。
今回は実行順序を確認するために最低限の説明しかしませんでしたが、その他にRxJavaをAndroid向けに機能追加したRxAndroidがあったり、オリジナルのOperatorを提供する拡張ライブラリもあります。RxJavaを使いこなせるようになった!と言えるようになるためにはまだまだやることは多そうです。
RxJavaはJavaにはない独自の世界があり、いままでJavaやAndroidのコードを書いてきた人にとってはどこかとっつきにくいところがあるように思いますが、このエントリを通してすこしでも理解が深まればと思います。

クックパッドではRxJavaを始めその他最新ライブラリを駆使して、スピーディに高品質なモバイルアプリを作っていくモバイルエンジニアを募集しています!興味がある方はぜひご応募ください。
iOS/Android アプリエンジニア | クックパッド株式会社 採用情報

Viewing all 726 articles
Browse latest View live