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

Spotlight 検索に iOS アプリのコンテンツを表示させる

$
0
0

 こんにちは。検索・編成部の中村です。いよいよ来月は WWDC 2016 が開催されますね。どんな発表があるか今から楽しみです。本エントリでは、Core Spotlight APIs を使用してアプリ内のコンテンツを Spotlight 検索に表示させる方法について解説していきます。

Spotlight 検索

 Spotlight 検索はホーム画面を右や下にスワイプして表示します。画面上部の検索窓からアプリ内のコンテンツを検索でき、ヒットした項目をタップするとアプリが起動して目的のコンテンツが表示されます。この仕組を利用してユーザーは素早く目的を達成できます。数多くアプリをインストールしているユーザーは、アプリの検索に利用しているのではないでしょうか。

f:id:nkmrh:20160520094836p:plain

 iOS クックパッドアプリ(v16.3.0.0 以降)では、特売情報を掲載している店舗が Spotlight 検索にヒットします。店舗をタップすると、アプリが起動して目的の店舗ページが表示されます。また、右端の矢印をタップすると地図アプリが起動し店舗の位置を表示します。

f:id:nkmrh:20160520094824p:plain

f:id:nkmrh:20160520094843p:plain

 このように Spotlight 検索に表示するには、コンテンツのメタデータをあらかじめ OS にインデックスさせておく必要があります。以降はその具体的な実装方法を紹介します。

Search API

 Spotlight 検索にコンテンツをインデックスさせるには3つのアプローチがあります。

  • NSUserActivity

NSUserActivityを使用する方法です。NSUserActivity は Handoff の実装にも使用するクラスです。ユーザーが閲覧したコンテンツをインデックスする際はこのクラスを使用します。パブリックコンテンツの場合は、メタデータを Apple のサーバに送信することで、検索結果ランキングを向上させることもできるようです。

  • Core Spotlight APIs

Core Spotlight Framework を使用する方法です。アプリに保存されているコンテンツをインデックスする際に使用します。

  • Web markup

同じコンテンツがWebサイトにある場合、ウェブページにコンテンツのメタデータをマークアップしておきます。Apple のウェブクローラにインデックスさせることで Spotlight 検索や Safari の検索結果に表示されます。

Core Spotlight APIs In Batch Mode

 冒頭で紹介した店舗情報の例で店舗情報はアプリに保存されているため、Core Spotlight APIs を使用しています。以降は Core Spotlight APIs を使用したバッチインデックスの実装を紹介します。

// 1letattributeSet= CSSearchableItemAttributeSet(itemContentType:kUTTypeContentas String)

ifletimage= UIImage(named:"thumb") {
    letthumbnailData= UIImagePNGRepresentation(image)
    // 2
    attributeSet.thumbnailData = thumbnailData
}

// 3
attributeSet.title ="title (best to limit your title to 90 characters)"// 4
attributeSet.contentDescription ="content description (best to limit your description to 300 characters)"// 5
attributeSet.keywords = [title, contentDescription]

attributeSet.identifier ="1234"// 6
attributeSet.contentURL = NSURL(string: @"http://sample/1234")

// 7
attributeSet.latitude = xx.xxxxxx
attributeSet.longitude = xx.xxxxxx
attributeSet.supportsNavigation =1// 8
attributeSet.phoneNumbers = ["xxxxxxxxxxx"]
attributeSet.supportsPhoneCall =1// 9
attributeSet.languages = ["ja", "en"]

// 10letdomainIdentifier="com.core-spotlight-search-sample"letsearchableItem= CSSearchableItem(uniqueIdentifier:attributeSet.identifier, domainIdentifier:domainIdentifier, attributeSet:attributeSet)

letsearchableIndex= CSSearchableIndex(name:"com.core-spotlight-search-sample-searchable-index")

// 11
searchableIndex.fetchLastClientStateWithCompletionHandler({ (clientState, error) in
    searchableIndex.beginIndexBatch()
    
    // 12
    searchableIndex.indexSearchableItems(searchableItems, completionHandler:nil)
    
    // 13letclientState= NSKeyedArchiver.archivedDataWithRootObject(NSDate())
    searchableIndex.endIndexBatchWithClientState(clientState, completionHandler:nil)
})
  1. 引数の itemContentType にはコンテンツの種類に応じた Uniform Type Identifier を指定します。Uniform Type Identifier については Uniform Type Identifier Overviewを参照してください。
  2. コンテンツに関連したサムネイル画像を指定します。アプリのアイコン画像は避けてください。(指定しない場合は自動的にアプリのアイコン画像が表示されます)画像が正方形であれば 180 x 180 pixels を用意します。横長の画像は横 180 pixels に調整され、縦長の画像は縦 270 pixels に調整されます。詳細はこちらの Provide a thumbnail image that captures the item in a relevant and appealing way. に記載されています。
  3. タイトルはデバイスの横幅より長い場合はトランケートされます。90 文字以内に収めるといいようです。
  4. 説明も長いものはトランケートされます。300 文字以内に収めるといいようです。(タイトルは最大1行、説明は2行表示できます)
  5. 検索キーワードを指定します。コンテンツに直接関係のないものは避けてください。
  6. 対応するWebサイトがある場合はそのURLを指定します。
  7. 緯度・経度を指定し supportsNavigation を有効にすると、矢印アイコンが表示されます。タップすると地図アプリが起動します。
  8. 電話番号を設定し supportsPhoneCall を有効にすると、電話アイコンが表示されます。タップすると電話をかけることができます。(表示できるのは地図・電話のどちらか1つです)
  9. コンテンツに含まれている言語を指定します。
  10. コンテンツのドメインIDを設定します。ドメインIDを指定してまとめて削除する APIも用意されています。
  11. 前回実行したバッチインデックスの状態を取得できます。ここで取得した情報をもとに、次にインデックスするものを決めることができます。
  12. アイテムをインデックスします。
  13. 引数の clientState には 250 バイトまでの情報が保存できます。今回の例では日付を保存しています。

 以上が Core spotlight APIs を使用したバッチインデックスの実装です。

Batch Index On Background Task

 このバッチインデックスはバックグランドタスクと組み合わせて使用すると、ユーザーのタスクを邪魔をせずにアプリ内のコンテンツをインデックスさせることができます。Github にサンプルプロジェクトがありますので、参考にしていただけると幸いです。

終わりに

 いかがでしたでしょうか。Spotlight 検索を実装してみると、アプリの種類やインデックスするコンテンツの内容によっては思いのほか有用な機能を作ることができるかもしれません。是非試してみてください。

参考

App Search Programming Guide


システム障害で消耗してるあなたに:失敗から学ぶための取り組み「Failure teaches Success」

$
0
0

こんにちは!広告エンジニアのレオです。最近、システム障害を起こしていますか?クックパッドも例外ではないです。毎月、何かしらのシステムに何かしらの障害が起きてしまいます。その際、早く気づき、速やかに対応することによって被害を最小限に留めるように努めます。そして、システムやデータを正常な状態に復旧させます。

正常な状態に戻した段階では対応はまだ完了していません。問題の本当の原因は何なのか、またその再発をどうやって防止するかを考えて手を打つまでは、障害の対応が完了したといえません。予防しない限り、また同じ過ちを繰り返すことになってしまいます。

失敗は成功のもと

根本原因分析、そして再発防止は大事な作業ですが、とても難しい作業です。クックパッドでは、これらを少しでもやりやすくするために、ルールと仕組みをまとめています。この仕組みを「Failure teaches Success」(略してFtS)と名づけています。直訳すると、「失敗は成功のもと」です。失敗の中には学びがあることを意識したネーミングです。失敗を共有することでみんなで学んで、同じ問題が発生しないようにすることを目指します。

FtSのルール

  • 問題が発生したら、チームで共有して、再発防止策を話しあう
  • "Failure teaches Success"の報告を作成する
  • 再発防止策を実施する
  • 他のチームと共有して、他の対策が必要か、よりよい対策ができないかについて話しあう

根本原因分析と再発防止策の手助けとして、考え方やアドバイスをまとめたテンプレートを用意しています。

FtS報告のテンプレート

## タイトル
- トラブルの内容を簡潔に表現するタイトルをつける

## 概要
- トラブルでおこった現象、影響範囲、経緯(必要に応じて)、技術的要因を簡潔に記述する
- 影響範囲については、技術的知識がない人であっても理解できる用語で記述する
- 技術的要因についてはトラブルに直接関係する事項だけを記載する

## 発生原因
- トラブルが発生した原因を記載する
    - 技術的要因ではなく、なぜその技術的要因を発生させてしまったのかを記載する

## 再発防止策
- チーム、全社で実施する行動だけを書く
    - 技術的要因の発生を技術的に防止できるなら、その対策をおこなう
    - 「気を付ける」「注意する」といった主観的な防止策は書かない
    - ルールが実施されていないのであれば、その原因を探ったうえで対策する
- うっかりミスや知識不足であっても、それを防止するための対策をおこなう
- 実施済みの項目と実施予定の項目を分ける
    - 実施予定についてはいつ実施するのかを記載する
        - 実施予定のものを実施したら、再発防止策の適用完了日に追記する

## 再発防止策の適用完了日
- 各対策の実施日を記入する
- 実施予定の再発防止策については、適用次第に追記する

FtSの考え方

技術的要因と根本原因は違う

システム障害等の問題が発生した場合、直接的な原因がソフトウェアやハードウェアの不具合や設定ミスであることが多いでしょう。しかし、原因は技術的要因だけではありません。仕組みや環境から由来する原因も考えられます。なぜなぜ分析などを用いて、発生原因を見つけ出します。

たとえば、こんな具合に障害を分析します。

  • データベースにテーブルが存在しなかったため、サイトが表示されなかった
    • なぜテーブルが存在しなかったのか?
  • 担当者がDROP TABLEを誤って実行した
    • なぜ誤ってDROP TABLEを実行したか?
  • 担当者が本番環境を開発環境だと誤認した
    • なぜ本番環境を開発環境だと誤認したか?
  • 作業端末で本番環境と開発環境を区別する方法がない
    • もしかして、それが根本原因かもしれない?(仕組みに問題があることを発見)
    • そもそも区別する必要があるのか?(別の疑問)
  • 開発環境と本番環境で同じ手順で作業するため
  • 開発環境と本番環境は同じ権限でアクセスできるため(複数の解答が考えられる)

それぞれの段階で複数の疑問を思い浮かぶこともありますし、原因は1つだとは限りません。

個人ではなく、チームと会社

根本原因の追求は、個人の過ちの追求ではありません。人は確率的に間違えるのが自然ですから、個人を責めても価値は生まれません。チームや会社が提供する環境・プロセスで対策・防止すると効果的です。これはとても大事なことです。

問題の直接的な原因が個人のミスであっても、ただ「気をつける」「注意する」だけでは何も変わりません。人間ですから、「気を抜ける」「不注意になる」ことは必ず発生するので、仕組みで人的要因を緩和して再発防止を目指します。

  • 手順・チェックリスト・ルールを整備する
  • ツールを導入する
  • 外部に委託する
  • 手順を自動化する
  • 作業自体をなくす

ルールのコンプライアンス問題

ルールを追加して、手順を整備するなど、人間の行動を改善するような再発防止策は費用対効果が高いこともあります。しかし、ルールや手順はいつの間にか複雑になりすぎて、ついつい忘れられていきます。「ルールを実施しなかった」といったミスの発生原因になりやすいです。ルールが実施できていなければ、ルールに何故従えないかを探って対策を行います。

よくあるルールを実施しない理由

  • 手順、ルールが多すぎる
  • ルール項目の実施が難しく、時間がかかる
  • 手順項目が退屈
  • 無駄な項目が多い
  • 手順、ルールの存在が知られていない

「対策しない」という選択

完全に対策するのが難しい問題、または非常にレアな問題は敢えて「対策しない」という選択もあります。費用対効果の問題ですが、事業への影響とリスク許容を理解した上で取る選択肢です。

再発しないように対策しなくても、該当問題の検知・計測を自動化することをおすすめします。事業や環境の変化によって、費用対効果の計算が変わりますので、再発した際に速やかに対策して再評価することができます。

人間は過ちを繰り返す

人間は必ず過ちを繰り返します。それは仕方のない現象ですから、同じ組織で同じ過ちを繰り返さないように、失敗から学ぶ仕組みを構築して対策しましょう。

「現在時刻」を外部入力とする設計と、その実装のこと

$
0
0

こんにちは。技術部 開発基盤グループの諸橋です。

クックパッドでは昨今の多くのWeb企業と同じように、GitHub EnterpriseのPull Requestを使ったコードレビューを広範に実施しています。わたしたちのコードレビューでは、ソースコードの字面にとどまらず、サービスの機能として魅力的かどうかや、保守性を含めた設計が適切かといった議論に発展することも良くあります。

きょうはそんななかで話題に上がった「現在時刻」の扱いかたに関する設計の話を書きます。

背景

サービスを開発・運営している我々には、時間帯によって出し分けたり、特定の期間のみに表示したいコンテンツがたくさんあります。 そのたびにデプロイし直すというのはつらいので(特に24:00に出なくなるコンテンツなど)なんとかしたくなりますが、一方で時限式のコンテンツはその時になるまでちゃんと動いているか確証が取れないので怖いです。

このつらさをなんとか軽減できないものかと考えました。

つらさの整理

たいへん身近な概念なので私たちは忘れがちですが、 現在時刻というのはプログラマが制御出来ない外部からの入力です。そのため、プログラムのいろいろなところで自由に入力を受け入れると外部環境への依存度が上がってしまい、自由に動かしたりテストしたりするのが難しくなります。

さらに、前述のような時限式コンテンツの判定をする場合、その取得した日時がある基準時刻の以前/以降であるか、あるいは時間帯にかぶっているかなどの判定をすることが多いはずです。こういった判定は、それ自体はけして難しいものではありませんが、他のロジックと混在すると煩雑になりがちです。

言い換えると、時限機能の作りづらさは、このような問題をひとまとめに解こうとしてしまうことに由来します。

またこういった時限機能は、自動テストを書く場合にも考えるべきことが増えてしまいます。たとえば、自動テスト時に日時をスタブするする定番ライブラリとしてTimecopというgemがあります。このgemは、Time.nowの振る舞いを書き換えることで日時をスタブしますが、capybara-webkitを使ったEnd-to-Endテストではうまく動きません。これは、テスト対象のRubyコードの日時はスタブできても、capybara-webkitが起動するブラウザプロセスの日時はスタブ出来ず、齟齬が生まれるためです。このように、外部プロセスとのやり取りが発生することになると、単純に「言語レベルで現在日時をスタブ」という方法では行き詰まってしまいます。

解法

この問題との向き合いかたには特別なことはありません。外部からの入力に対しては、読み込む箇所を局所化し、いったん読み込んだ値を各所で使っていきます。また時間帯にかぶるかどうかといった判定も抽出していきます。

日時の判定処理を抽出する

例えばビューにこういった処理があったとします。

-if@start_at<= Time.now && Time.now < @end_at&& current_user.target? && some_condition?(current_user)
  = render(:special_event) # 時間になると現れるコンテンツ

まずは時刻がかぶっているかどうかの判定を、クラスやメソッドに抽出します。

defenabled_now?@start_at<= Time.now && Time.now < @end_atend
-if enabled_now? && current_user.target? && some_condition?(current_user)
  = render(:special_event) # 時間になると現れるコンテンツ

さらに、抽出したenabled_now?の中も、もっと整理できそうです。 2回.newされているTimeは同一オブジェクトであるべきです。また、前述のように日時のカバーの判定をしている処理と外部入力の読み込みであるTime.nowはわけたほうがメソッドの責務は少なくなります。

修正範囲を最小にしたい場合、Rubyであればデフォルト引数などにするのがもっとも簡単でしょう。

defenabled?(at: Time.now)
  @start_at<= at && at < @end_at# あるいは (@start_at ... @end_at).cover?(at) などend

こうしておくと、ビュー全体のテストではなく、この判定に関心事を絞ってテストもできるようになります。例えば自動テストにて検証したい場合でも、Timecopを使う必要がなくなります。

下記ではヘルパーメソッドからさらに、判定を行う小さなクラスに抽出しています。

context 'while being enabled'do
  let(:policy) { TimePeriodPolicy.new(start_at: 1.second.ago(at), end_at: 1.second.since(at)) }
  let(:at) { Time.now }

  it { expect(policy).to be_enabled(at) }
end

describe 'Xmas period'do
  let(:xmas_policy) doTimePeriodPolicy.new(
      start_at: Time.zone.parse('2015/12/20'),
      end_at:   Time.zone.parse('2015/12/25').end_of_day
    )
  end

  context 'in 12/19'do
    it { expect(xmas_policy).not_to be_enabled(Time.zone.parse('2015/12/19 00:00:00')) }
  end

  context 'in 12/20'do
    it { expect(xmas_policy).to be_enabled(Time.zone.parse('2015/12/20 00:00:00')) }
  end

  context 'in 12/26'do
    it { expect(xmas_policy).not_to be_enabled(Time.zone.parse('2015/12/26 00:00:00')) }
  endend

実際のサービスでのコンテンツ出し分けは、時間帯だけでなくユーザの状態やその他データも勘案する必要があるケースが多いでしょう。それでも、日時を外部化しておくことでテストを完全にコントロールできるメリットは大きいはずです。

日時を取得する処理を局所化する

さて、日時を元に条件を判定する箇所は抽出できました。では外部入力の局所化、つまり現在日時を取得する箇所はどのようにすればよいでしょうか。こちらも定石通り進めていきましょう。すなわち

  • 外部環境を読み込む箇所を一箇所にする
  • ロジック内からは、そこで読み込んだ局所化したインターフェースから中身を参照する

というアプローチです。

今回の例で言えば「現在時刻」として欲しかったものは、実は厳密な意味でのコード実行時点の現在、ではなく「リクエストされた時間」で十分です。そのため、それを取得するインターフェースを一箇所に限定します。

現在クックパッドでは、そのインターフェースを統一するためにTriceというgemを作り、使っています。

このgemは、Railsのコントローラにリクエストが到達した時間を表すrequested_atメソッドを提供します。

classApplicationController< ActionController::BaseincludeTrice::ControllerMethodsend
classSpecialEventsController< Applicationcontrollerdefshow
    ...
    if@event.policy.enabled?(requested_at)
      do_something
    else
      head :not_foundendendend

モデルの処理でこの時刻を使うには、コントローラからその時刻を渡してあげます。 なぜなら、現在を知るのは「外部からの入力を適切にモデルに渡す」というコントローラの責務の範囲であり、モデル側はそのTimeオブジェクトの由来を関知すべきでないからです。

ビューでも同様に、Time.nowを直接呼ぶのではなく、requested_atから取得できる値を基準として時限機能を判定します。

- @event.policy.enabled?(requested_at)
  = render(:special_event) # 時間になると現れるコンテンツ

さて、このように現在時刻を取得するインターフェースを抽出すると、自動テストの中から参照する現在時刻を変更するのも簡単になります。

Timecopなどのようにプロセス全体で共有されるTime.nowをスタブするのではなく、controller#requested_atのみをスタブすればよくなるため、スタブが影響する範囲をコントロールしやすくなります。

before do
  controller.stub(:requested_at) { Time.zone.parse('2015/12/20 00:00:00') }

  get :show, id: xmas_event.id
end

システム時刻以外の入力も受け付けられるようにする

さらに、ここまでリファクタリングを進めた結果、当初は「現在時刻によって挙動が変わる」機能であったものが、実は「リクエスト時刻とみなしたTimeオブジェクトに基づいて挙動が変わる」という機能だったことに気付きます。 ということは、Time.nowを呼んでシステム時刻を取得する以外の方法でリクエスト時刻を設定できれば、自動テストや動作確認がとてもやりやすくなります。

Triceでは実際に、リクエストのHTTPヘッダを使って外部から、基準日時を設定できるようになっています。 自動テスト内からこのリクエストヘッダを利用して基準日時を自由に設定するためのテストヘルパーもあります。

このような実装があれば、本番に近い構成の手動テスト環境でも時限機能の動作を無理なく確認できます。また、CapybaraでのEnd-to-Endテストのような高レベルな自動テストからも同じように基準日時を設定してテストできるようになります。前述のように、Timecopなど処理系のTime.nowをまるごとスタブするライブラリは動かないことがありますが、Triceの方式であれば問題なく動作します。

まとめ

現在時刻の取得(Time.now)は外部入力の読み取りにほかなりません。現在時刻に基づいて分岐するような処理は、一見簡単に見えますが、少しずつアプリケーションを複雑にしていきます。

それを、

  • 入力取得と判定部分を分離する
  • 入力取得する箇所を統一・局所化する
  • 必要に応じて、外部の値で上書きできるようにする

とリファクタリングしていくことで、動作確認や自動テストでの扱いやすさを取り戻しました。

こういった方法は、決して特別で難しいことをしているわけではありません。どちらかといえばオブジェクト指向だったりプログラミング一般だったりの基本的な考え方を実際のアプリケーションに適用し、そのために必要な小さなライブラリをつくっただけです。それでも、実際のサービスでよく見るつらさを軽減できているのではないでしょうか。

あわせて読みたい

前述のように、この現在日時を適切に抽出してコードを整理する方法は、決して目新しいものではなく、多くの書籍やサイトで語られている考え方です。先達の多くの情報のうち、特によくまとまっていると感じたURLも示しますので、よかったらこちらも合わせてどうぞ。

複数サービス間の整合性の取り組みについて

$
0
0

こんにちは。技術部 開発基盤グループの大石です。

本日は開発基盤グループが社内の各サービスに提供している共通基盤サービスの1つである共通決済基盤を例にサービス間の整合性を維持するための取り組みを紹介したいと思います。(共通決済基盤については以前紹介した クックパッドの課金を支える技術を参照ください)

決済における整合性を考える

サービス間連携は決済に限らず発生するものですが、共通決済基盤の場合、組織外にあるサービスと通信する必要があり、コントロールができない外的要因に影響を受けやすい点と、決済という確実性が求められる処理を含んでいるということの間で整合性について考える必要があります。

まずは、共通決済基盤上で行われるサービス間通信の種類とそれぞれで通信を行っている際にエラーが起きた場合にどのようにハンドリングすれば整合性を維持できるかを考えてみます。

サービス間通信の種類と流れ

共通決済基盤で行われるサービス間通信には2種類あります。

  1. 共通決済基盤と決済ゲートウェイとの通信
  2. 共通決済基盤と自社で運用する各サービスとの通信

(※決済処理は自分たちのシステム内では完結せず、クレジットカード決済であれば決済代行会社や、キャリア決済であればモバイルキャリアの決済システムとの接続が必要となります。本稿ではそれらをまとめて決済ゲートウェイと呼称します。)

これらの通信の流れの具体的な例として、継続決済の契約完了から有料サービスをユーザーが利用できるまでの流れとして下記に示します。

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

  1. 決済ゲートウェイが決済手続き完了を共通決済基盤へ通知する
  2. 共通決済基盤は通知を受け取り、決済情報をデータベースへ保存する
  3. 共通決済基盤は決済結果を連携先の自社サービスにコールバックする
  4. 共通決済基盤は決済ゲートウェイへ応答を返し、決済が確定する

という流れになります。

エラーが起きたとき

上記の図のどこかで障害が発生しエラーが起きた場合、どのようにハンドリングするかを考えてみます。

一番わかり易くシンプルな方法としては、データベースにおけるトランザクションの概念のように、一連の処理の途中のどこかで失敗した時点で共通決済基盤はすべてロールバックできるようにすることです。

先程の流れで考えてみると、(1)あるいは(2)が失敗した場合はそのまま決済ゲートウェイに失敗の応答を返します。 (3)が失敗した場合は、(2)のデータベースをロールバックし、決済ゲートウェイに失敗の応答を返します。

この方法は一見問題ないように見えますが、いくつかの問題が発生します。

すべてロールバックするときの問題点

共通決済基盤は、

  • 決済ゲートウェイと共通決済基盤間
  • 共通決済基盤と自社サービス間

という2つのシステム境界を持っています。

例にあげた手続きの流れ全体に対して、トランザクションを確保しようとするアプローチの問題は、そのシステム境界間での通信においてエラーが起きたときにロールバックを完璧にできない場合が発生してしまうことです。

例えば、共通決済基盤と自社サービス間において、連携先の自社のサービスへ共通決済基盤がコールバックを送信する部分での通信時にタイムアウトが起きたとき、共通決済基盤上ではエラーとして処理を行い、決済ゲートウェイへは決済が失敗したと応答したのにもかかわらず、実は自社サービス側ではリクエストが成功しており、決済が成立した状態になることがありました。 またその頻度は、通信先の自社サービス内において、他のサービスのAPIなどの通信が発生している場合、共通決済基盤に間接的に繋がるいずれかのサービスへの通信が失敗した時点で全てロールバックすることになるため、サービス分割が進むにつれて上昇しやすくなっていくという問題もありました。

もちろん、自社サービス側でそういったことが起きないように適切にエラーハンドリングをしたり、2フェーズコミットのような方法を要求することもできますが、そういった方針は外部の設計に依存するため、共通決済基盤側で独立してうまく対処する方法をとる必要があります。

可能な限り成立させる

上記のような問題があったため、共通決済基盤でとった解決策は、障害が発生するまでの状態をできる限り保存し、エラーが起きたとしても可能な限り成立させるようにすることでした。

具体的には先程述べたの2つのシステム境界、

  • 決済ゲートウェイと共通決済基盤間
  • 共通決済基盤と自社サービス間

それぞれを1つの処理単位として考え、決済ゲートウェイと共通決済基盤間の連携が成功すればそこですべて成功とみなす方法です。

図にある流れ、

  1. 決済ゲートウェイが決済手続き完了を共通決済基盤へ通知する
  2. 共通決済基盤は通知を受け取り、決済情報をデータベースへ保存する
  3. 共通決済基盤は決済結果を連携先の自社サービスにコールバックする
  4. 共通決済基盤は決済ゲートウェイへ応答を返し、決済が確定する

で具体的にみてみます。

まず、(1)、(2)を1つの単位として、それが失敗した場合は決済ゲートウェイに失敗の通知をして終了します。 成功した場合は、(3)の処理を行い、この処理の成否に関わらず(4)では必ず成功の通知を決済ゲートウェイに対して送信します。 また、(3)が失敗した場合、ジョブキューなどを利用してリトライを試みたりすることで、自動的に整合性のある状態にするように努めます。

このアプローチによるメリットは、障害の影響をなるべく小さくし共通決済基盤の独立性を高めることで、外部環境への複雑な依存が少なくなるという点です。

デメリットとしては、一時的に不整合が発生することです。もし不整合が発生した場合、共通決済基盤はできるだけ短い時間で不整合を修正するように振る舞う必要があります。

しかしそのデメリットよりも、2つのシステム境界それぞれに対して独立して問題分析や対応を行うことができる点は複雑性を下げる大きなメリットとなると判断しました。

整合性を保つことができているのか

ここまで紹介したものは、システム境界間での整合性を保つ方法でした。 私達が採用した「可能な限り成立させる」方法は整合性に対して、完璧を目指すのではなくエラーが起きる前提で、エラーの影響を最小化する方法をとっているとも言えます。

そこで必要になるのが、整合性が保つことができているのかをチェックする機構です。

この機構によってリトライも失敗し不整合が解決できないままのケースや、決済ゲートウェイと共通決済基盤間の通信でタイムアウトなどが発生した場合は、そもそも共通決済基盤ではなにも起きていないのに、決済だけが成立してしまっている場合など、共通決済基盤が責任をもつすべてにおいての整合性を担保するようにします。

具体的には、クックパッドの課金を支える技術に紹介した、決済ゲートウェイと共通決済基盤、共通決済基盤と自社サービスのそれぞれで決済の情報すべてを定期的に突合することで整合性が保たれているかの確認を行っています。

また最近では、比較的大きめの不具合や障害が起きない限り、ここまで到達するケースはほとんど無く、もし到達した場合は Issueを作成し、どのような原因で整合性を回復できなかったのかを記録し、その改善を共通決済基盤へフィードバックするということでより精度を高めるようにも機能しています。

最後に

すべてロールバックすることのメリットは、失敗したときの状態がわかりやすいという点、最終的に成功しているのか、失敗しているのかの2つに結果が収束する原子性ではありますが、うまくロールバックすることができないことが多く、私達はこのアプローチを改善する必要がありました。

そこで、強い整合性よりも結果整合性という考え方を優先して「可能な限り成立させる」方針で整合性を保つようにしています。

ただし、強い整合性を否定しているわけではなく、どちらか一方にはっきり固定しなければいけないということではありせん。決済ゲートウェイと共通決済基盤間において、安全にロールバックが可能な決済方法の場合は部分的に強い整合性に近い方法を採用している箇所も存在しています。

また、「可能な限り成立させる」方針によって、

  • 独立性を高めつつ、なにか1つの方法に縛られることなく柔軟に対応できる点
  • 完璧を目指すのではなくエラーが起きる前提で、通常のハンドリングから漏れてどうしても整合性を維持できなかったものが発生した場合は、改善のフィードバックをすぐに行うようにすることで外的要因の変化に対して柔軟に対応できる点

のような別のメリットもありました。

これらの対応によって不整合の発生する割合は以前よりも低下したので、このアプローチは今のところ上手く行っているのではないかと思います。

6/18(土) Think User First - Cookpad × Fablic 第4回を開催します!

$
0
0

こんにちは。Holidayの多田です。

このたび、フリマアプリ「フリル」でおなじみの Fablicさんと共同で行っているデザイナー向けイベントの第4回を開催します!

今回は Fablic 流の サービスの立ち上げユーザーインタビュー手法を体感することができるワークショップです。新規事業の立ち上げや、事業会社でのサービスデザインに興味がある方はぜひご応募ください!

Think User First - Cookpad × Fablic

イベント概要

フリマアプリ「フリル」や「RIDE」を手がける Fablic と、レシピサービス「クックパッド」休日のおでかけサービス「Holiday」などを手がける Cookpad。Think User First は“ユーザーファースト”を掲げるこの2社で、開発現場でのデザイナーの取り組みについて紹介するイベントです。

今回は Fablic 社のデザイナーが実践している新規サービスの立ち上げプロセスを紹介し、新規サービス開発に有効なインタビュー手法を体験できるワークショップを開催します。実際にインタビューから得られた発見をサービス設計に反映していく流れに興味のあるデザイナーは是非ご参加ください!

タイムスケジュール

2016/6/18(土) 13:00 -

Fablic社オフィス(恵比寿)

※今回は、いつもと会場が違います!ご注意ください。

  • 13:00 オリエンテーション&講義
  • 14:00 ワークショップ
  • 18:40 発表
  • 19:30 懇親会

体験できる手法

  • ユーザーインタビュー
  • Fablic 流の新規事業立ち上げプロセス

Fablic やクックパッドのデザイナーが、メンターとして参加します

対象

  • ユーザーファーストな開発プロセスに興味がある方
  • 今現在、業務としてデザインやサービス設計に携わられている方
  • 事業会社でのサービスデザインに興味がある方

持ち物

  • 名刺(終了後に懇親会がありますので、多めにお持ちください!)
  • ノートPC

参加費

無料

参加方法

こちらよりご応募ください(外部サイトに移動します)

connpass.com

締め切り

6/8(水) 12:00

過去の開催情報

エンジニアが0からのサービス開発で学んだこと

$
0
0

こんにちは、買物情報事業部の三浦です。

私たちのチームでは1つのプロダクトの開発が佳境を迎えています。 私は普段エンジニアとしてチームに所属していますが、今回はプロダクトオーナーとして開発に携わっています。そこで単に実装者としてだけでなく、初期のコンセプト立てやユーザーインタビューなどのプロセスを経て学べたことをご紹介します。

価値を見つけるところからスタート

普段のチームでの開発はディレクターが開発を伴わない価値検証を行った上で仕様を考えます。それを基に立てられたissueをエンジニアは引き継いで実装を進めています。issueに記載された施策の背景やユーザーストーリーから、ユーザーの利用シーンをイメージして開発します。

今回はその価値となるものを探す作業からディレクターと共に行いました。まずはユーザーインタビューを実施し、計10名以上の方からお話を聞きました。

課題をよりリアルに感じた

インタビューの質問の1つに「平日の1日の過ごし方を教えてください」というものがありました。 ある1人のユーザーさんは共働き世帯でお子さんがいる女性の方でした。その方の1日の予定は本当にタイトでした。育児、家事に仕事。そして、その中で食品の買い出しをこなしたり息つく暇がありませんでした。

もちろん自分たちのコアなユーザーさんたちが忙しい毎日を過ごしていることは認識しているつもりでいました。しかし、目の前のユーザーさんから聞くリアルな生活とタイムスケジュールは、自分の想像を超えた忙しい毎日を過ごしていることに本当に驚きました。それと同時に、この目の前にいるユーザーさんに自分たちのプロダクトによって、ほんの少しだけでも”便利だな” と実感してもらえるようなものを作りたい!と。その人が抱えている課題を自分事としてより身近に感じることができたのです。

ペルソナによってできた軸

ユーザーインタビュー後にプロダクトにおけるペルソナを作りました。

今回はインタビューを行ったユーザーさんの中に、サービスとしてコアに価値提供したい像のど真ん中の方がいたこともあり、その方をベースにペルソナを作りました。チーム内でも納得のペルソナです。

自分でも直接話しを聞き、目にしたユーザーさんをベースにペルソナが作られていたこともあり、いつでもそのペルソナを中心に考えることが当たり前になっている自分に少し驚きました。

チーム内で議論が生じた時にも、共有されたペルソナを軸に、常にそのペルソナが何を求めているのかを中心として議論が進みました。

開発作業に入ってしまうと、つい実装のことや構造のことなどプロダクト自体に考えがいきがちです。書いている仕様やコードの先にペルソナを思い浮かべることができると、本来あるべき姿が自ずと見えてきました。

ストーリーベースでの視野を持ち得た

ユーザーインタビューのデータを基にペルソナを固めた上で、下記のような時間軸に沿ったストーリーボードを作成しました。

1日のユーザーの行動の中でどんなインタラクションがあるのかを視覚化しています。 そのプロダクトがいつ、どこで、どのくらい、なにを目的として利用されるかを俯瞰して見ることができます。

これを作っていて感じたのは、1日の中で使ってもらえるタイミングなんて本当に数回しかないんだという焦りと共に、ユーザーとの1回1回の接点を本当に全力で臨みたいと思いました。

また、エンジニアとしては開発をしていると、画面単位や機能単位での実装になるため、そこにフォーカスした狭い視野になりがちです。それがストーリーベースで捉えた視点を持つことからスタートしたことにより、単一画面だとしてもその画面がどんな場面で、どのように利用されるか、常に全体で把握するように自然と矯正されていることに気づかされました。 これは仕様面でディレクターやデザイナーと意見に相違が生じた場面でも、俯瞰した視野から意見を交わすことができ、比較的スムーズなコミュニケーションを行うことができました。

まとめ

今回は、私が0ベースのサービス開発の中で、エンジニア視点で得られたことをまとめてみました。 0からの開発に入ることは不慣れなことも多く、チームのメンバーには常に助けてもらってきました。 ただエンジニアだからこそ感じることができたこと、貢献できたことがありました。

少し領域を広げて、プロダクトを別の視点から見ることで、日々の開発に役立つものも多くありました。よりユーザーに価値を提供したいと考えるエンジニアの方は、ぜひ直接ユーザーに触れ視野を広げてみてください。

クックパッド 2016 サマーインターンシップ開催します!

$
0
0

こんにちは! 人事部長兼エンジニアの @yoshioriです。

クックパッドでは、今年も夏に 2 つのインターンシップを開催します!!!!

Cookpad Tech Internship - Summer 2016 -

「この夏、クックパッドで腕試しをしませんか。」ということで Cookpad Tech Internship - Summer 2016 - を開催します。 このインターンシップは前後半にわかれています。 まず、前半で Rails によるアプリケーション開発フロー、iOS アプリ開発、Android アプリ開発、サービス開発論、プログラミング論、機械学習と 6 つの分野の講義を受け、課題を行ってもらいます。

そして、後半は実際に社内でメンターとなるエンジニアと一緒に開発してもらいます。そのためエンジニアとほぼ同じ権限が与えられます。全てのソースコードは見れますし、データベースにアクセスでき、個人情報などのセキュアなデータ以外はすべて見れます。もちろん社内の主要なサーバに入る環境も与えられます。 そのため、前半の講義・課題を経て、実際の業務についても問題無いだろうと認められた人が対象となります。

自分自身の技術のレベルを知る事もできますので、是非チャレンジしてみてください!!

去年の資料はコチラ(課題は去年のものを更にブラッシュアップして行う予定です)

Cookpad Tech Internship - Summer 2016 -

実践型サービス開発・5-days Internship

f:id:cookpadtech:20160610174450p:plain

そしてもう一つは、エンジニア・デザイナーで小さなチームを組み、サービス開発を行う5日間のインターンシップです。 こちらはクックパッドで 5 年間同じ形式で行なっているインターンシップで、ハッカソンのような形で集中して実際にお題にそったサービスを作ってもらいます。 ユーザーの課題についてひたすら考え抜き、悩み、それをどうやって解決するのか、そのためにはどんな物を作ればいいのかをさらに考えぬく。それを通じて実際にクックパッドでどのようにサービス開発が行われているのかを経験してもらいます。

毎日実際にサービス開発しているクックパッドのエンジニアやデザイナーからフィードバックを受けつつ、"ユーザーファーストなモノづくり"を実践してみましょう!!

実践型サービス開発・5-days Internship


僕は、今年は両方に監修として参加して、本当に会社をあげて最強の講師陣を揃えました! クックパッドの技術や開発手法に興味がある学生のみなさん、ぜひ夏休みを利用してクックパッドの夏のインターンシップに参加してみてください! みなさんからのご応募をお待ちしています!!!

API クライアントを書きつつ Swift らしいコードを考える

$
0
0

こんにちは、技術部モバイル基盤グループの茂呂(@slightair)です。

クックパッドは Garageという RESTful Web API 開発を楽にする Rails のためのライブラリを作り、内部通信やモバイルアプリケーションのためのAPIサーバの開発に利用しています。

過去の Garage の紹介記事はこちらです。

この Garage を使って実装された Web API を iOS アプリから気軽に呼べるように、 Swift で Garage のクライアントを実装してみました。

この記事では、GarageClientSwiftの紹介をしつつ、これを作りながら Swift らしいコードってどんなコードなんだろうと考えたことをつらつらと書いていきたいと思います。

Garage

Garage は RESTful Web API 開発のためのライブラリです。OSSとして公開しています。 https://github.com/cookpad/garage

今回はクライアントサイドの話をしたいので Garage 自体の説明は過去の記事にまかせます。

記事で紹介されているサンプル実装を使ってクライアントの開発・動作確認を行います。 手元で動作を確認しながら読みたい場合は、リポジトリからコードをチェックアウトして動かしてください。 https://github.com/taiki45/garage-example

クライアントアプリケーションの動作確認時には、サーバアプリケーションのアクセストークンが必要になるので、過去の記事の手順にしたがって取得してください。

GarageClientSwift

GarageClientSwift はその名の通り、GarageClient の Swift による実装です。 https://github.com/slightair/GarageClientSwift

GarageClientSwift は僕が趣味でなんとなく書いたものなので、クックパッドのアプリでもうバッチリ使っているぜ!…というわけではありません。ただ基本的な機能はそろっているのではないかと思います。

GarageClientSwift は HimotokiAPIKitというSwiftのライブラリに依存しています。 これらのライブラリについては後述します。

GarageClientSwift の使い方

GarageClientSwift は Carthageでプロジェクトに導入できます。 詳しくは README.mdを読んでください。 この記事では GarageClientSwift 1.1.0 の実装を使った例を出します。

GarageClientSwift の workspace に Demo.playgroundを同梱しているので、コードを触りながら動作を確認したければこれを利用できるでしょう。 Demo.playgroundを動かす際は一度 GarageClient iOSの scheme でビルドしてから playground ファイルを開いてください。

この節で説明するものは、この playground ファイルに記述されているものです。

リソースのモデルを定義する

Web API Client を使うということは、なんらかのリソースを取得したいと考えているはずです。 ここでは User リソースを取得することを考えます。 以下のように User構造体を定義します。 リソースモデルは Himotoki の Decodableに準拠するようにします。

structUser:Decodable {
    letid:Intletname:Stringletemail:Stringstaticfuncdecode(e:Extractor) throws ->User {
        return try User(
            id:e<|"id",
            name:e<|"name",
            email:e<|"email"
        )
    }
}

リクエストを定義する

次にリソースを得るためにどのようなリクエストを投げるか定義します。 /usersに GET リクエストを送信してユーザーの一覧を取得しましょう。 このようなリクエストを表現する構造体を定義します。

structGetUsersRequest:GarageRequestType {
    typealiasResource= [User]

    varmethod:HTTPMethod {
        return .GET
    }

    varpath:String {
        return"/users"
    }

    varqueryParameters:[String: AnyObject]? {
        return [
            "per_page":1,
            "page":2,
        ]
    }
}

なんとなくやりたいことがわかると思います。 APIKitを知っている人はそのまんまだと感じていると思います。

Garage の設定を定義する

次にGarageアプリケーションへ接続するための情報を用意します。 GarageConfigurationTypeというプロトコルがあるので、それに準拠する構造体かクラスを定義してそのインスタンスを作ります。ここでは単純にGarageアプリケーションのベースURLとアクセストークンをただ保持している構造体を作りました。実際にはアクセストークンを認可サーバから取得してそれを返してくれるような認証・認可機能を実装したクラスになると思います。

structConfiguration:GarageConfigurationType {
    letendpoint:NSURLletaccessToken:String
}

letconfiguration= Configuration(
    endpoint:NSURL(string:"http://localhost:3000")!,
    accessToken:"YOUR ACCESS TOKEN"
)

リクエストを送信する

あとはリクエストを送信するだけです。 GarageClient のインスタンスを作って、sendRequest メソッドでリクエストを送信します。 リクエストのコールバックには Result.Success.Failureが引数に渡されるので結果に応じた処理を記述します。 .Successの場合には、取得したリソースやページングのための件数などの情報を含む GarageResponse構造体を取得できます。

letgarageClient= GarageClient(configuration:configuration)
garageClient.sendRequest(GetUserRequest()) { result inswitch result {
    case .Success(letresponse):
        debugPrint(response)

        letusers= response.resource
        debugPrint(users)
    case .Failure(leterror):
        debugPrint(error)
    }
}

以上が GarageClientSwift を使ったリクエスト送信までの流れです。

Himotoki

Himotoki はJSONをデコードしてモデルにマッピングするためのライブラリです。 https://github.com/ikesyo/Himotoki

この記事では Himotoki 2.0.1 の実装を使った例を出します。

Himotoki を使って以下の様な JSON を User構造体にマッピングするにはこのように記述します。

JSON

{"id": 2,
  "name": "bob",
  "email": "bob@example.com"
}

User.swift

structUser:Decodable {
    letid:Intletname:Stringletemail:Stringstaticfuncdecode(e:Extractor) throws ->User {
        return try User(
            id:e<|"id",
            name:e<|"name",
            email:e<|"email"
        )
    }
}

e <| "id"のような見慣れない構文が登場しますが、これは Himotoki の Extractor のためのオペレータです。JSONから指定したキーの要素を期待通りの型で取り出すための工夫です。

Himotoki の実装をのぞいてみる

<|はどのような実装になっているのか見てみましょう。

https://github.com/ikesyo/Himotoki/blob/2.0.1/Sources/Operators.swift

infix operator <| { associativity left precedence 150 }

/// - Throws: DecodeError or an arbitrary ErrorTypepublicfunc<|<T: Decodable>(e:Extractor, keyPath:KeyPath) throws ->T {
    return try e.value(keyPath)
}

Swift ではオペレータを定義することができるので、その結合の仕方と優先度、処理を定義しています。 <|は Extractor の e.value(keyPath)を呼んでいることがわかりました。

https://github.com/ikesyo/Himotoki/blob/2.0.1/Sources/Extractor.swift

privatefunc_rawValue(keyPath:KeyPath) throws ->AnyJSON? {
    guard isDictionary else {
        throw typeMismatch("Dictionary", actual:rawValue, keyPath:keyPath)
    }

    letcomponents= ArraySlice(keyPath.components)
    return valueFor(components, rawValue)
}

/// - Throws: DecodeError or an arbitrary ErrorTypepublicfuncvalue<T: Decodable>(keyPath:KeyPath) throws ->T {
    guardletrawValue= try _rawValue(keyPath) else {
        throw DecodeError.MissingKeyPath(keyPath)
    }

    do {
        return try T.decodeValue(rawValue)
    } catch letDecodeError.MissingKeyPath(missing) {
        throw DecodeError.MissingKeyPath(keyPath + missing)
    } catch letDecodeError.TypeMismatch(expected, actual, mismatched) {
        throw DecodeError.TypeMismatch(expected:expected, actual:actual, keyPath:keyPath+ mismatched)
    }
}

value メソッドはつまり、与えられた keyPath で Dictionary から要素を取り出し、返り値の型の decodeValueを呼びだして値を返しています。TypeConstraintsを使って Decodable プロトコルに準拠していることを制限に課しているので、e <| "id"の返り値が Decodable に準拠する型でないといけません。

上記のJSONの "id"要素は数値なので Int になることを期待します。Int や String のようなよく使う型に対しては Himotoki ですでに Decodable に準拠するための実装が extension で追加されています。 https://github.com/ikesyo/Himotoki/blob/2.0.1/Sources/StandardLib.swift

さて、User 構造体の decode メソッドは以下のように実装していました

staticfuncdecode(e:Extractor) throws ->User {
    return try User(
        id:e<|"id",
        name:e<|"name",
        email:e<|"email"
    )
}

User 構造体では、 id は Int、name と email は Stringと型宣言してあるので、コンパイラが e <| "id"Inte <| "name"Stringが返ると推論します。型推論がうまく働いてくれるのですっきりした記述になるわけです。

Decodable プロトコルはどのような定義になっているのでしょうか。 https://github.com/ikesyo/Himotoki/blob/2.0.1/Sources/Decodable.swift

static func decode(e: Extractor) throws -> Selfを定義していることを要求しています。 これが、先ほど見つけた decodeValueメソッドから呼ばれます。

Swift の Protocol にはデフォルト実装を Protocol extension で追加できます(Swift2 から) なので Decodable に準拠している構造体は decodeValueメソッドを実装していなくてもデフォルトの実装が使われます。

Himotoki の Decodable プロトコルに準拠していれば、JSON から作られたDictionaryを以下のようにしてモデルにマッピングできます。

letuser:User? = try? decodeValue(JSON)
letusers:[User]? = try? decodeArray(ArrayJSON)

便利ですね! 期待した型とJSONの要素の型が一致しない場合は例外が投げられマッピングに失敗します。

Himotoki は Generics と型推論、Protocol をうまく使った例だと思います。

APIKit

APIKit はリクエストとレスポンスを抽象的に表現できて使いやすいAPIクライアントライブラリです。 https://github.com/ishkawa/APIKit/

リクエストを表す構造体を定義し、それに対応するレスポンスをモデルで受け取れるのが特長です。 リクエストに渡すパラメータの型を明示できます。 リクエスト結果は、成功と失敗のどちらかの状態を表現する Result型で受け取れます。Result には成功時に目的のオブジェクトを、失敗時にエラー情報を含めることができるので、Optional な変数を用いることなくリクエスト結果を受け取ることができます。

この記事では APIKit 2.0.1 の実装を使った例を出します。

使い方を見てみましょう。 https://github.com/ishkawa/APIKit/blob/2.0.1/Documentation/GettingStarted.md

まずはリクエストを定義します。 サンプルは GitHub API の Ratelimit を取得する API を実行するようです。 RequestTypeプロトコルに準拠した RateLimitRequestとそのレスポンスを表すモデル RateLimitを定義します。

structRateLimitRequest:RequestType {
    typealiasResponse= RateLimit

    varbaseURL:NSURL {
        return NSURL(string:"https://api.github.com")!
    }

    varmethod:HTTPMethod {
        return .GET
    }

    varpath:String {
        return"/rate_limit"
    }

    funcresponseFromObject(object:AnyObject, URLResponse:NSHTTPURLResponse) throws ->Response {
        guardletdictionary= object as? [String:AnyObject],
            letrateLimit= RateLimit(dictionary:dictionary) else {
                throw ResponseError.UnexpectedObject(object)
        }

        return rateLimit
    }
}

structRateLimit {
    letlimit:Intletremaining:Intinit?(dictionary:[String: AnyObject]) {
        guardletlimit= dictionary["rate"]?["limit"] as? Int,
            letremaining= dictionary["rate"]?["limit"] as? Int else {
                returnnil
        }

        self.limit = limit
        self.remaining = remaining
    }
}

RateLimitRequest構造体には API の baseURLmethodpathなどのリクエストを構築するために必要な情報を記述します。 また、レスポンスをどのようにモデルにマッピングするかを responseFromObjectメソッドに記述します。

リクエストの定義ができたらそれを使ってリクエストを投げます。 コールバックには Result<T, Error>が渡されるのでそれに応じた処理を記述します。 .Successの場合はレスポンスをマッピングしたモデルが含まれているので、後は好きなように扱えばよいでしょう。 RateLimitRequestのレスポンスはRateLimitと定義してあるので、resultResult<RateLimit, Error>であり、.Success<RateLimit>が渡されるわけです。なので limitremainingのプロパティにアクセスできます。

letrequest= RateLimitRequest()

Session.sendRequest(request) { result inswitch result {
    case .Success(letrateLimit):
        print("limit: \(rateLimit.limit)")
        print("remaining: \(rateLimit.remaining)")

    case .Failure(leterror):
        print("error: \(error)")
    }
}

APIKit の実装をのぞいてみる

APIKit の RequestTypeプロトコルの実装を見てみましょう。 https://github.com/ishkawa/APIKit/blob/2.0.1/Sources/RequestType.swift

RequestTypeはリクエストを表現する構造体が準拠すべきプロトコルでした。 baseURLmethodpathqueryParametersheaderFieldsなどなど様々なプロパティがありますがほとんどにデフォルト実装が用意されており、オプションのパラメータはリクエストを定義する際に指定したいものだけ実装すれば良いようになっています。

受け取ったレスポンスをパースしたオブジェクトをどのようにモデルにマッピングするかを以下のメソッドに記述します。デフォルトではレスポンスに JSON を期待しています。dataParserプロパティを指定すれば JSON 以外も受け付けることができます。

funcresponseFromObject(object:AnyObject, URLResponse:NSHTTPURLResponse) throws ->Response

他にも以下の様なメソッドが宣言されています。

funcinterceptURLRequest(URLRequest:NSMutableURLRequest) throws ->NSMutableURLRequestfuncinterceptObject(object:AnyObject, URLResponse:NSHTTPURLResponse) throws ->AnyObject

これらのメソッドをリクエストの構造体に実装することで送信する URLRequest に追加の情報を付与したり、レスポンスに応じて独自のエラーを投げてエラーレスポンスを処理することができるようになっています。 これらのメソッドも必要でなければデフォルト実装が利用されるので定義を省略することができます。

APIKit は Swift の Protocol をうまく利用していると思います。

次に Sessionを見てみましょう。 https://github.com/ishkawa/APIKit/blob/2.0.1/Sources/Session.swift

Singleton の Sessionオブジェクトを持っているので、通常の利用範囲であればクラスメソッドの Session.sendRequestメソッドを使えば良いようになっていることがわかります。

sendRequestメソッドは TypeConstraints で引数 request の型 RequestRequestTypeに準拠しているべきと制約を課しています。

publicfuncsendRequest<Request: RequestType>(request:Request,
                                        callbackQueue:CallbackQueue? =nil,
                                              handler: (Result<Request.Response, SessionTaskError>) ->Void= {r in})
                                              ->SessionTaskType? {
...

RequestType には以下の様な記述がありました。

publicprotocolRequestType {
    /// The response type associated with the request type.associatedtype Response
...

これは Protocol の Associated Types という機能で定義するプロトコルに関連する型を指定できるものです。 以下のように RateLimitRequest の Response 型を typealiasキーワードで指定することができます。

structRateLimitRequest:RequestType {
    typealiasResponse= RateLimit
...

これにより先ほどの sendRequestメソッドの handler 引数にある Result<Request.Response, SessionTaskError>の記述が、RateLimitRequestの場合は Result<RateLimit, SessionTaskError>に定まるわけです。 こうして、リクエストとそれに対応するレスポンスのモデルの型を明示できるようになっています。

このようにして APIKit はリクエストとレスポンスを表現するモデルをわかりやすく定義できるように作られています。 僕のお気に入りのライブラリです。

GarageClientSwift の実装

GarageClientSwift はこれまで説明してきた Himotoki と APIKit を組み合わせて作ったライブラリです。 すでに利用例で見せたように、Himotoki を使った Decodable なリソースのモデルを用意し、APIKit のようにリクエストを表現してリクエストを送信します。

やっていることは APIKit をラップして、Garage アプリケーションの認証に必要なアクセストークンをリクエストに付与したり、Garage のレスポンスに共通で含まれるページング等の情報を持った値を表現する GarageResponseを返すようにしています。

少し工夫したところはリソースの型に User[User]のようにモデルの配列も指定できるようにしたところです。

GarageClientにふたつの sendRequestを定義しています。 https://github.com/slightair/GarageClientSwift/blob/1.1.0/Sources/GarageClient.swift

publicfuncsendRequest<R: GarageRequestType, D: Decodable where R.Resource == D>
    (request:R,
     handler: (Result<GarageResponse<D>, SessionTaskError>) ->Void= { result in })
    ->SessionTaskType? {
        letresourceRequest= RequestBuilder.buildRequest(request, configuration:configuration)
...publicfuncsendRequest<R: GarageRequestType, D: Decodable where R.Resource: CollectionType, R.Resource.Generator.Element == D>
    (request:R,
     handler: (Result<GarageResponse<[D]>, SessionTaskError>) ->Void= { result in })
    ->SessionTaskType? {
        letresourceRequest= RequestBuilder.buildRequest(request, configuration:configuration)
...

リクエストの ResourceDecodableまたは Decodableを要素に持つ CollectionTypeを受け付けています。

RequestBuilder にもふたつの buildRequestを定義しており、それぞれ SingleResourceRequestMultipleResourceRequestを作ります。 https://github.com/slightair/GarageClientSwift/blob/1.1.0/Sources/RequestBuilder.swift

structRequestBuilder {
    staticfuncbuildRequest<R: GarageRequestType, D: Decodable where R.Resource == D>
        (baseRequest:R, configuration:GarageConfigurationType) ->SingleResourceRequest<R, D> {
        return SingleResourceRequest(baseRequest:baseRequest, configuration:configuration)
    }

    staticfuncbuildRequest<R: GarageRequestType, D: Decodable where R.Resource: CollectionType, R.Resource.Generator.Element == D>
        (baseRequest:R, configuration:GarageConfigurationType) ->MultipleResourceRequest<R, D> {
        return MultipleResourceRequest(baseRequest:baseRequest, configuration:configuration)
    }
}

SingleResourceRequestMultipleResourceRequestの違いは、中で呼んでいる Himotoki のメソッドが decodeValuedecodeArrayかの違いです。 ともに ResourceRequestプロトコルに準拠しており、このプロトコルは APIKit の RequestTypeを継承しています。 前述した GarageRequestTypeは APIKit の RequestType風のプロトコルですが、実際には APIKit の sendRequestに渡す ResourceRequestRequestBuilderが作り GarageRequestTypeから値を取っていたのでした。 https://github.com/slightair/GarageClientSwift/blob/1.1.0/Sources/GarageRequestType.swift

今回のような範囲では Class の継承ではなく Protocol を使うとすっきりと書けます。 Swift の Protocol は Protocol extension によるデフォルト実装の提供が強力で、継承ができない struct であっても Protocol の組み合わせで拡張していくことができます。 このような Protocol を組み合わせていくプログラミング手法を Apple は Protocol Oriented Programming として提唱しています。

まとめ

GarageClientSwift というライブラリを紹介しつつ、このライブラリの実装に利用した Himotoki、 APIKit と GarageClientSwift 自身の実装を読み、Protocol や Generics を使った実装例の説明をしました。 Swift は新しい言語であり、おもしろい機能や新しいプログラミング手法を提供してくれます。単なる Objective-C の置き換えでアプリケーションを楽に記述するための言語とは捉えずに、 Swift の言語機能を使ってより柔軟で安全なコードを記述して素敵なアプリケーションを作りましょう。


インフラ新卒研修と社内ISUCONのはなし

$
0
0

インフラ部の荒井(@ryot_a_rai)です。

今年の4月、弊社には11名の新卒エンジニアが入社しました。そして現在、3ヶ月間の研修を受けています。ビジネスマナーから技術研修まで幅広く行われていますが、その中で5月下旬におこなったインフラ研修とその後の社内ISUCONについてご紹介します。

インフラ研修(講義)

f:id:ryotarai:20160610152008j:plain

インフラ研修はインフラ部に配属されるエンジニアに限らず、全新卒エンジニアが参加する研修です。日常業務でコードを書いてサービスを開発していくうえで知っておいてほしい、インフラに関する基礎知識や共通言語を獲得することを目的としています。合計3日間をインフラ部の@kani_bと分担して講義しました。研修内容の内容はざっくりと以下のようなものです。

1日目

  • インターネットとは
    • ブラウザでウェブサイトを閲覧する際になにが起きているのか
    • IPからHTTPまでざっくりと
  • Webインフラアーキテクチャ概観
    • 三層アーキテクチャ(Webサーバ層、Webアプリーケーション層、データベース層)の話
    • スケールイン/アウト
  • Vim超入門
  • アプリケーションサーバ
    • Rack, Ruby on Railsの仕組み、役割
    • WEBrickやUnicornなどのHTTPサーバの話

2日目

  • データストア
    • MySQL
      • スロークエリ, explain, インデックス
      • トランザクション, ロック
      • レプリケーションとスケールアウト
    • Memcached
      • slab allocator, consistent hashingなど使う上で知っておきたい知識
    • Redis
      • Memcachedに比べた利点や使いどころについて
    • 全文検索
      • Solr, Elasticsearchの役割や利点について
  • Webサーバとプロキシ
    • nginx
      • 静的ファイルの配信
      • Unicornなどアプリケーションサーバの前段に配置する意味

3日目

  • その他の構成要素
    • キャッシュ
      • Railsキャッシュストア
      • Varnishによるレスポンスのキャッシュ
    • CDN
      • CDNの役割や利点について
    • バッチ処理
      • バッチ処理とはなにか
      • 弊社のバッチ処理環境について
      • バッチを書く際に気をつけるべきこと
  • Infrastructure as Code
    • なぜコードで記述するか
    • Itamae, Serverspecによるサーバプロビジョニング
    • これまでの手作業を自動化してみる
  • いわゆる”クラウド”について
    • AWSなどいわゆるクラウドサービスの利点と各種サービスについて

全体として自分で触って覚えられるようにハンズオンを多くおこないました。ひとりひとりVirtualBox( + Vagrant)でVMを構築し、その中で演習をできるようにしています。例えば、自分で実装したRackアプリケーションをWEBrickやUnicornで動かしたり、nginxを前段に挟んでベンチマークを取って効果を確認したりしました。

段階グランプリ(社内ISUCON)

4日目は新卒研修のフィナーレとして段階グランプリ(社内ISUCON)を開催しました。ISUCONは年に一回開催されているパフォーマンスチューニングコンテストで、段階グランプリはその社内版です*1。段階グランプリは新卒研修の一環ですが、せっかくなら新卒以外の社員にも参加してもらおう、ということでエンジニア全体で参加者を募り開催しました。参加者は新卒11名(4チーム)、新卒以外23名(9チーム)となり大盛況でした。

準備

今回の段階グランプリの準備・運営はインフラ部の3名(@mirakui, @kani_b, 私 @ryot_a_rai)でおこないました。具体的には以下の準備をしました。

  • 参加者ポータルサイト(mirakui)
    • ベンチマークを実行したり、ベンチマーク結果を見たり
    • 社内の参加者以外からも見えるようにして、お気に入りのチームを応援できるようにしました
    • 素敵なドメイン(段階.jp)でアクセスできるようにしました
  • 参考実装(ryot_a_rai)
    • 複数言語実装は用意せず、Ruby on Railsでの実装のみを用意しました(弊社で最も多く利用されている言語・フレームワーク)
    • 新卒研修で学んだことを活かせるよう、N+1クエリ、スロークエリなど日常でお目にかかるような消耗ポイントを用意しました
    • テーマは「雑実装なクックパッド」でした
  • ベンチマーク(ryot_a_rai)
  • サーバの準備(kani_b)
    • 参加者の環境やベンチマーカなどを用意
    • 今回はAWS EC2上で1チームにつきc4.largeを3台(io1 EBS 100IOPS)を用意しました
      • gp2を使っていないのはバーストを防ぐため
    • メインのAWSアカウントとは別のアカウントを利用していたため、インスタンス数の制限に引っかかって、急いで緩和申請を上げました…

余談ですが、新卒研修準備、社内ISUCONの開発合宿に利用したヴィラージュ伊豆高原がよかったです。会議室や部屋によっては大きめのテーブルがあり夜中まで開発をやっていけますし、いい感じの温泉がありました。

結果

f:id:ryotarai:20160610151941p:plain

最終結果は上のようになりました。本家ISUCON本選出場勢(@sora_h, @eagletmt)が大人げないスコアで優勝しましたが、新卒チームも初期スコアの4, 5倍のスコアを出し、新卒研修の成果が見てとれました。参加者の感想もおおむね好評で今後も定期的に開催していきたいと考えています。

まとめ

以上、今年のインフラ研修についてご紹介しました。今年は講義形式や社内ISUCONが初回だったこともあり、準備も大変でしたが、今年の経験を来年以降にも繋げられるといいと思っています。こんな新卒研修や社内ISUCONに参加したい、主催したいというあなた、ぜひ一緒にやっていきましょう!

*1:本家ISUCONについてはこちら

開発コストを最小限にして施策を進める

$
0
0

投稿推進部・ディレクターの中山です。
普段ディレクターはエンジニアとペアを組んでサービス開発をすることが多いですが、エンジニアが別の開発に集中したい時は、ディレクターだけで施策を進めることもあります。エンジニアがいないと動くものができない…と言っていては何もできません。
既に多くのディレクターの方が試していることかもしれませんが、実装に入る前に紙レベルでモノを作ってテストしたり、一般に出回っているツールを活用してみたり…と方法は色々とあります。私がここ数ヶ月で実践してきたことを、おさらいも兼ねてご紹介したいと思います。

方法1:紙でイメージを膨らませる

サービスを考える際、いきなり実装に入ることはまず無いと思います。
当たり前の話かもしれませんが、スピーディにイメージを掴むには手書きが便利。何度も書いたり直したりしつつ頭の中のイメージを具体化し、周囲のスタッフに当ててみることがができます。 f:id:akoakon777:20160614182920p:plain

例えば上記の写真の例は、見やすく書きやすいレシピフォーマットについて考えて書いたもの。普通のノートやレシピ用に売られているカードなどもあり、これらに実際に書いてみたり、スマホサイズに切り取ってみたりして使用感を試します。
右は手書きではありませんが、自分のレシピをカード状に置き直して紙に印刷してみたもの。普通のA4の紙に印刷し余白を折って整えただけのものですが、手に取ってみることで「文字はもうちょっと減らしたいな」「写真サイズはこれくらいが良いな」などの実感を得ることができます。

方法2:一般に出回っているツールを活用

プロトタイプツール

コードが書けなくてもFlintoやProttなどのツールを使えば動きのあるプロトタイプを作れる良い時代です。手書きや紙のコンテンツでイメージが出来たらこれらのツールで動くものを作り、スマホの実機でユーザーテストをすることができます。

ここでちょっと気をつけたいのは、プロトタイプツールが便利なだけに、ついつい作り込み過ぎてしまうことです。細かいデザインや動きは実装段階でデザイナーやエンジニアにお任せすれば良いので、ここではあくまでもユーザーが画面を見た際に、どういう意図をもってどんな動きをしようとしているのか を確かめます。
ユーザーのどんな課題をどうやって解決するツールなのか
を明確にしておき、その意図のようにユーザーが動いているか(或いはどこで躓いているのか)を確認する目的で使います。

SNSを活用

自分でプロトタイプを作らずとも、既に人が集まっていて尚且つ投稿機能を備えた場を借りることもできます。
TwitterやInstagramのようなSNSは非常に便利。
例えば「写真+1行のキャプション」のようなひと纏まりのコンテンツが周囲の人の興味をひけるのか、を試したい場合。そのまま自分で「写真+1行のキャプション」を作成し、コンテンツとしてSNSに投下してみると、「いいね!」やコメントの数で手応えを掴むことができます。あくまでも参考程度ではありますが、ざっくりとした反響を掴むことで、プロダクトの方向性が正しいのかを確かめることが可能です。

サービスの作りそのものを参考にできるだけでなく、こうしたちょっとしたテストにも活用できるので、自社以外の人気のサービスを普段から自分で使い込んでおくことは非常に有益だと思います。

方法3:既存の社内ツールをうまく使う

新しい施策を実施する際、目的に合わせて新たな機能を開発したくなってしまいますが、エンジニアのリソースは限られています。新しく作る前に、既にある仕組みを活用できないか考えてみると良いでしょう。
例えば弊社の場合は、ユーザー向けの汎用的なアンケートツールや連絡先の取得ツールが既にありました。社内で専用のものを内製していない場合でも、一般的に使えるアンケートやメールのような仕組みは色々あるのではないでしょうか。 本当に開発が必要なのか、手持ちのツールを組み合わせて解決できる可能性をまずは考えてみると開発の手間を省けるというのはよくあります。

次に、ここまでにご紹介した手法を組み合わせて今年の春に実施した「母の日のフォトブック企画」の事例をご紹介します。

「母の日のフォトブック企画」

考えた企画は、
ユーザーさんが「母の日」というイベントを前に、お母さんの思い出の味をレシピにしてをクックパッドにのせる
→自分はいつでも料理を再現できて便利になり、お母さんには記念のフォトブックとともに感謝の気持ちを届けることができる
  というもの。

手作りの試作品でテスト

まず、本当にこの企画がユーザーの心に響くのか、をテストするため、私自身がクックパッドにのせている母親のレシピを紙に印刷し、手作りのフォトブックを作成。 f:id:akoakon777:20160614183020p:plain

写真は粗いし作りも雑でお恥ずかしいレベルですが、この段階でのクオリティは気にしない。とは言え、これでも手に取ってみるとなかなかの達成感があります。※1

次に、実際にこの手作りの試作品を遠方に住む自分の母親に予告なしで送りつけてみて、電話で感想をきいてみました。
いきなり送りつけられた母親はとても驚いていましたが、電話の向こうで涙ぐむほど喜んでいて、送ったこちら側はガッツポーズできるほどの達成感。同時に、伝わりきらなかった部分のヒアリングもでき、テストとしては十分な手応えを掴むことができました。

本番の企画を実施

上記のテストのフィードバックを踏まえ、企画を本格的に実施すべく動かし始めました。

企画を立てた当初は、ユーザーが自分のレシピを選んでサクッと応募できる仕組みを開発するつもりでいました。
しかし、スケジュール的にも開発リソース的にも無理がある。そこで既存の社内のツールを色々調べてみると、アンケートフォームと住所取得フォーム、確認のメールを直接やり取りできる仕組みなどがありました。
これらを組み合わせれば、なんとかできないこともなさそうです。
もちろん、既存のアンケートフォームの仕様に縛られるので、ユーザーが入力するテキスト量が多くなり、手間をかけさせてしまう部分もあります。
そこで、事故を防ぐために気をつけたのは以下の2点です。

  • ユーザーさんから新たに受け取る情報を最小限にする
  • 仕様をシンプルにしてユーザーさんの考えるコストを減らす

具体的には、レシピのフォーマット内に既に書かれている内容をそのままフォトブックに採用。これで応募時のテキスト入力の手間やミスが減ります。また、フォトブックの仕様は「写真入りレシピ5品」という1パターンのみに統一し、余計な選択するための思考コストをなくしました。
一方で、試作の段階では自分(娘)が母親のために作った世界で1冊のフォトブックである、という部分が伝わりきらなかったので、冒頭の1ページ目にお母さんへのオリジナルメッセージを入れる仕様に。ここで皆さんがお母さんへの思いを伝えられるようにしました。

このように既存のフォームを活用してなんとか応募の裏側の仕組みを整え、表側は1枚の告知ページだけを用意することで、実装のコストを最小限に抑えることができました。

スマートな応募フォームを用意できるに越したことはありませんが、最終的なゴールはユーザーが満足できるフォトブックを作成してお届けすること。今回の企画のように、スケジュール内でその目的を達成するため、多少の使い勝手が下がっても実現可能な方法を選択したほうが良い場合もあります。
結果的に大きな事故もなく、皆様に素敵なフォトブックを作成してお届けすることができました。※2 応募してくださった方からはメールやブログなどでの好意的な反響が通常の6倍ほどもあり、大変ご満足いただけたという印象です。

まとめ

何か施策を進めようとする時、まずはエンジニアのリソース確保…と考えてしまいがちですが、ディレクターだけでできることは色々あります。紙や既存のツール、SNSやリアルな人間関係などを駆使すればある程度のテストも可能。
エンジニアには本当に必要な開発に集中してもらえるよう、今後もこれらの手法を常に意識して取り組んでいきたいです。

※1 社内のプリンターでA4用紙に印刷したものをハサミで切ってビニール製のポケットブックに入れ、マスキングテープを貼って綴じただけのもの。
※2 本企画でユーザーの皆様にお届けしたものは専門業者さんに製本してもらった素敵なものです。ご安心ください。

Core Text と遊んでみましょう

$
0
0

こんにちは、技術部モバイル基盤グループのヴァンサン(@vincentisambart)です。

この間、クックパッドの iOS アプリの開発で Core Text と色々遊んだので、今日は Core Text の話をしましょう。

課題は表示する文字の一部の裏に角丸長方形を表示することです。例えばクックパッド iOS アプリに表示されているリンクを長く押すと表示されている角丸長方形です。以下の画像は「落し蓋」に表示されるタッチフィードバックが見えます。区域を計算したら、その後タップ区域のためにも使えますしね。

f:id:vincentisambart:20160617074525p:plain

以下に説明するやり方はクックパッド iOS アプリのやり方を簡略化したものです。(クックパッド iOS アプリは実装時にまだ Swift を使い始めていなかったので Objective-C ですけども)

Swift Playground (Swift 2.2) で開発しましょう。コードは iOS 用ですが、少しいじれば OS X でも使えるはずです。スターティングポイントは下記の新しい画像に文字列を表示するコードです。

import UIKit
import CoreText

letavailableWidth:CGFloat=200// 文字表示に使える幅lettext="français 日本語 हिन्दी English\n\n言語混ぜるのっておもしろくない?"letfont= UIFont.systemFontOfSize(20)
// kCTFontAttributeNameの代わりにNSFontAttributeNameも使えるけど、CocoaとCore Textのattributesは違いあるので、間違いを防ぐためにCore Textのを使いましょう。letattributes= [
    kCTFontAttributeName as String:font,
    // ユーザーの言語が何であろうと、漢字は日本語フォントを優先に使ってほしいですね。
    kCTLanguageAttributeName as String:"ja",
]
// Core Textの説明書がCFAttributedStringを使うけど、NSAttributedStringとCFAttributedStringがtoll-free bridgedなので、もっと使いやすいNSAttributedStringを使います。letattributedString= NSAttributedString(string:text, attributes:attributes)
// Core Textのframesetterというのは文字列を指定される形の中にレイアウトするツールです。letframesetter= CTFramesetterCreateWithAttributedString(attributedString)
// 表示に必要な高さを計算します。letframeSize= CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(), nil, CGSize(width:availableWidth, height:CGFloat.max), nil)
letbounds= CGRect(origin:CGPoint.zero, size:frameSize)
// 文字列を長方形の中に表示したいだけですね。letpath= CGPathCreateWithRect(bounds, nil)
letframe= CTFramesetterCreateFrame(framesetter, CFRange(), path, nil)

// 新しい画像を描き始めます。
UIGraphicsBeginImageContextWithOptions(frameSize, true, 2.0)
ifletcontext= UIGraphicsGetCurrentContext() {
    // 背景を白で塗ります。
    CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0, 1.0)
    CGContextFillRect(context, bounds)

    // 何もしないとCore Textの表示が逆さまなので正しい方向に表示されるようにします。
    CGContextTranslateCTM(context, 0, frameSize.height)
    CGContextScaleCTM(context, 1.0, -1.0)

    // 文字の表示です。
    CTFrameDraw(frame, context)
}
// 描いた画像を出します。letimage= UIGraphicsGetImageFromCurrentImageContext()
// 描き終えます。
UIGraphicsEndImageContext()

Playground上で画像の表示をインラインに表示できて便利です。コードが実行されたら画像が定義されている場所の右側に画像のサイズが表示されていて、マウスカーソルをその上に乗せたら右の一番端に表示されるボタンをクリックしたら画像がその下に表示されます。

f:id:vincentisambart:20160617074532p:plain

framesetter には色々な設定がありますが、それでも足りない人は CTTypesetterCreateWithAttributedStringCTTypesetterSuggestLineBreakCTTypesetterCreateLineで自分で行の列を作れます。

Core Foundation

開発を続けるには Core Text が返している Core Foundation の型(CFArrayCFRange)を扱わなければいけないけど、Swift だと少し扱いづらいですね。もっと扱いやすくしましょう。

まず、 CFRangeはシンプルな structなので、そんなに扱いづらいわけではないけど、便利関数が少ないですね。 NSRangeはそういう便利機能が色々あるので、簡単に変換できるようにしましょう。

extensionNSRange {
    // CFRangeからNSRangeを作れるようにします。init(_ range:CFRange) {
        self.init(location:range.location, length:range.length)
    }
}

extensionCFRange {
    // NSMaxRangeのCFRange版です。varmax:CFIndex {
        return location + length
    }
}

折角 NSRangeをいじっているなら、あとで使えそうな便利機能を1個追加しておきましょう。

// 渡されるrangeが重なるかどうか。funcrangesOverlap(range1:NSRange, _ range2:NSRange) ->Bool {
    return NSIntersectionRange(range1, range2).length !=0
}

CFRangeは簡単でしたが、 CFArrayを Swift の配列に変換するのが少しややこしいです。一番自然なやり方でやってみましょう。

letlines= CTFrameGetLines(frame) as! [CTLine]

上記のコードを実行するとクラッシュします。実は Swift の配列に変換する前に NSArrayに変換すると動きます。

letlines= (CTFrameGetLines(frame) as NSArray) as! [CTLine]

じゃあそれを関数にしてみましょう。

functoArray<T>(sourceArray:CFArray) ->[T] {
    return (sourceArray as NSArray) as! [T]
}

上記のコードは動きますが、Xcode 7.3では、間違っている警告「Cast from 'NSArray' to unrelated type '[T]' always fails」が出ます。そしてなぜか関数の中だと as NSArrayがなくても動きます(警告まだ出ますけど)。

警告や謎キャストが好きじゃないので。代わりに CFArrayの中身を新しい Swift 配列に入れるようにしましょう。

functoArray<T>(sourceArray:CFArray) ->[T] {
    vardestinationArray= [T]()
    letcount= CFArrayGetCount(sourceArray)
    destinationArray.reserveCapacity(count)
    for index in0..<count {
        letuntypedValue= CFArrayGetValueAtIndex(sourceArray, index)
        letvalue= unsafeBitCast(untypedValue, T.self) // 😅
        destinationArray.append(value)
    }
    return destinationArray
}

結局また乱暴なキャストが必要になりましたね…でも仕方ない気がします。

各文字がどこに表示されるのか

CTFrameには、表示されるグリフの位置が決まっています。ある座標がどの文字に一番近いのか教えてくれる関数(CTLineGetStringIndexForPosition)、または文字列のあるインデックスのy座標を教えてくれる関数(CTLineGetOffsetForStringIndex)があります。けれど、文字列の各文字がとっているスペースを直接教えてくれる関数がありません。英語、日本語、だけを対象にするなら CTLineGetOffsetForStringIndexで簡単にできそうですが、もっと複雑な言語にも対応したければそんなに簡単にいきません。簡単じゃないとはいえ、そこまで複雑でもありません。

詳しい説明は公式Core Text紹介でご覧になれますが、軽く説明しましょう。CTFrameに何が入っているのかといいますと、単に CTLine(行) のリストです。「行」というのは "\n"で句切らているパラグラフではなく、表示の1行1行です。

そして各行が CTRunで作られています。"run"がフォント、フォントサイズ、方向、が同じなグリフのリストです。気をつけるべきなのは、元々指定されたフォントが同じでも、文字がフォントに存在していなければ、代わりに別のフォントが使われるので、別の"run"になるかもしれません。例えば、文字列が"Vincentと申します"とフォントがシステムフォント(San Francisco)の場合、2つのrunになります:

  • 「Vincent」のグリフが入ったrun (フォント:San Francisco)
  • 「と申します」のグリフが入ったrun (フォント:Hiragino Kaku)

求める長方形を探すには、各グリフを見て、そのグリフは選択されている文字から来ているのかを確認しましょう。

しかし、グリフがどの文字から来ているのか教えてくれる CTRunGetStringIndicesという関数が少し使いづらいです。グリフごとに文字列の中の開始インデックスだけを返します。1つのグリフが複数の文字を使うことがあります。例えば合字(リガチャー)や結合文字列(combining character sequence)ですね。なので開始インデックスだけではなく、文字列の区域が必要ですね。

次のグリフの開始インデックスに1を引けば…と思った方いるかもしれませんが、残念ながらそんなにうまく行きません。アラビア語やヘブライ語だと 3, 2, 1, 0 になりますし、次のヒンディー語の文字列「हिन्दी」のCTRunGetStringIndicesの結果が:1, 0, 2, 5。並び順が予測できません。

では、各文字で開始インデックスに一番近いインデックスを探すコードを書きましょう。

funcstringRangesPerGlyph(run:CTRun) ->[NSRange] {
    letrunEndIndex= CTRunGetStringRange(run).max
    letglyphCount= CTRunGetGlyphCount(run)

    varstringIndices= Array<CFIndex>(count:glyphCount, repeatedValue:0)
    CTRunGetStringIndices(run, CFRange(location:0, length:glyphCount), &stringIndices)

    return stringIndices.map { glyphStartIndex in// glyphStartIndexより大きいけど一番近いインデックスを探します。varglyphEndIndex= runEndIndex
        for comparedIndex in stringIndices {
            if comparedIndex > glyphStartIndex && comparedIndex < glyphEndIndex {
                glyphEndIndex = comparedIndex
            }
        }
        // 因みに let glyphEndIndex = stringIndices.filter({ $0 > glyphStartIndex }).sort().first ?? runEndIndex でもいけます。return NSRange(location:glyphStartIndex, length:glyphEndIndex- glyphStartIndex)
    }
}

実装

必要な物が揃ったので実装してみましょう。最初に書いたコードの上に上記のextensionや関数を入れて、 let frame = CTFramesetterCreateFrame(framesetter, CFRange(), path, nil)の下に以下のコードを入れましょう。(全部が入ったコードはこの記事の一番下にも入れておきます。)

// 2つのCGRectが水平にすぐ隣なのかどうか。funcrectAdjacentHorizontally(rect1:CGRect, _ rect2:CGRect) ->Bool {
    if rect1.intersects(rect2) {
        returntrue
    }
    if abs(rect1.maxX - rect2.minX) <=1.0 {
        returntrue
    }
    if abs(rect1.minX - rect2.maxX) <=1.0 {
        returntrue
    }
    returnfalse
}

letroundedRectangleRanges= [
    NSRange(location:10, length:5),
    NSRange(location:31, length:3),
]

letlines:[CTLine]= toArray(CTFrameGetLines(frame))
varlineOrigins= Array<CGPoint>(count:lines.count, repeatedValue:CGPoint.zero)
CTFrameGetLineOrigins(frame, CFRange(location:0, length:lines.count), &lineOrigins)

varrectsFound= [CGRect]()

for (lineIndex, line) in lines.enumerate() {
    letlineRange= NSRange(CTLineGetStringRange(line))
    letruns:[CTRun]= toArray(CTLineGetGlyphRuns(line))

    varascent:CGFloat=0vardescent:CGFloat=0
    CTLineGetTypographicBounds(line, &ascent, &descent, nil)
    letlineOrigin= lineOrigins[lineIndex]
    letlineMinY= lineOrigin.y - descent
    letlineMaxY= lineOrigin.y + ascent

    for roundedRectangleRange in roundedRectangleRanges {
        if!rangesOverlap(roundedRectangleRange, lineRange) {
            continue// この長方形がこの行に入りません。
        }

        varrectsForRangeOnCurrentLine= [CGRect]()

        for run in runs {
            letrunRange= NSRange(CTRunGetStringRange(run))
            if!rangesOverlap(roundedRectangleRange, runRange) {
                continue// この長方形がこのrunに入りません。
            }

            letglyphCount= CTRunGetGlyphCount(run)
            letallGlyphs= CFRange(location:0, length:glyphCount)
            letstringRanges= stringRangesPerGlyph(run)
            varpositions= Array<CGPoint>(count:glyphCount, repeatedValue:CGPoint.zero)
            CTRunGetPositions(run, allGlyphs, &positions)
            varadvances= Array<CGSize>(count:glyphCount, repeatedValue:CGSize.zero)
            CTRunGetAdvances(run, allGlyphs, &advances)

            // 以前に説明したとおり、グリフが並び順通りじゃないから、まずはグリフ1つずつ長方形を作ります。for (glyphIndex, stringRange) in stringRanges.enumerate() {
                if!rangesOverlap(roundedRectangleRange, stringRange) {
                    continue
                }
                letx1= positions[glyphIndex].x
                letx2= positions[glyphIndex].x + advances[glyphIndex].width
                letminX= min(x1, x2)
                letmaxX= max(x1, x2)
                letrect= CGRect(x:minX, y:lineMinY, width:maxX- minX, height:lineMaxY- lineMinY)
                rectsForRangeOnCurrentLine.append(rect)
            }
        }

        // このrangeのすぐ隣の長方形をくっつけます。
        rectsForRangeOnCurrentLine.sortInPlace { $0.minX <$1.minX }
        varmergedRects= [CGRect]()
        for rect in rectsForRangeOnCurrentLine {
            varrectToAppend= rect
            ifletpreviousRect= mergedRects.last {
                if rectAdjacentHorizontally(previousRect, rect) {
                    mergedRects.removeLast()
                    letminX= min(previousRect.minX, rect.minX)
                    letmaxX= max(previousRect.maxX, rect.maxX)
                    rectToAppend.origin.x = minX
                    rectToAppend.size.width = maxX - minX
                }
            }
            mergedRects.append(rectToAppend)
        }
        rectsFound.appendContentsOf(mergedRects)
    }
}

表示は CTFrameDrawの上に以下のコードを入れましょう。

UIColor.cyanColor().setFill()
for zone in zonesFound {
    letroundedRectanglePath= UIBezierPath(roundedRect:zone, cornerRadius:4)
    roundedRectanglePath.fill()
}

f:id:vincentisambart:20160617074536p:plain

最後に

一番重要な処理はやってありますけど、改善の余地はありますね。

まず、制限が1つあります:roundedRectangleRanges が合字や結合文字列の真ん中で始まる/終わる場合、グリフ全体の後ろに角丸長方形を描くことになります。それが問題になるかどうかは使う場所次第ですね。

あと、一番上のスクリーンショットみたいに、複数行に渡る時は行末や行頭では角丸にしない方がいいかもしれません。クックパッド iOS アプリでやっていて、そこまで難しくないけど記事が既に十分長いです(笑)。

全コードまとめ

import UIKit
import CoreText

extensionNSRange {
    // CFRangeからNSRangeを作れるようにします。init(_ range:CFRange) {
        self.init(location:range.location, length:range.length)
    }
}

extensionCFRange {
    // NSMaxRangeのCFRange版です。varmax:CFIndex {
        return location + length
    }
}

// 渡されるrangeが重なるかどうか。funcrangesOverlap(range1:NSRange, _ range2:NSRange) ->Bool {
    return NSIntersectionRange(range1, range2).length !=0
}

functoArray<T>(sourceArray:CFArray) ->[T] {
    vardestinationArray= [T]()
    letcount= CFArrayGetCount(sourceArray)
    destinationArray.reserveCapacity(count)
    for index in0..<count {
        letuntypedValue= CFArrayGetValueAtIndex(sourceArray, index)
        letvalue= unsafeBitCast(untypedValue, T.self) // 😅
        destinationArray.append(value)
    }
    return destinationArray
}

funcstringRangesPerGlyph(run:CTRun) ->[NSRange] {
    letrunEndIndex= CTRunGetStringRange(run).max
    letglyphCount= CTRunGetGlyphCount(run)

    varstringIndices= Array<CFIndex>(count:glyphCount, repeatedValue:0)
    CTRunGetStringIndices(run, CFRange(location:0, length:glyphCount), &stringIndices)

    return stringIndices.map { glyphStartIndex in// glyphStartIndexより大きいけど一番近いインデックスを探します。varglyphEndIndex= runEndIndex
        for comparedIndex in stringIndices {
            if comparedIndex > glyphStartIndex && comparedIndex < glyphEndIndex {
                glyphEndIndex = comparedIndex
            }
        }
        // 因みに let glyphEndIndex = stringIndices.filter({ $0 > glyphStartIndex }).sort().first ?? runEndIndex でもいけます。return NSRange(location:glyphStartIndex, length:glyphEndIndex- glyphStartIndex)
    }
}

// 2つのCGRectが水平にすぐ隣なのかどうか。funcrectAdjacentHorizontally(rect1:CGRect, _ rect2:CGRect) ->Bool {
    if rect1.intersects(rect2) {
        returntrue
    }
    if abs(rect1.maxX - rect2.minX) <=1.0 {
        returntrue
    }
    if abs(rect1.minX - rect2.maxX) <=1.0 {
        returntrue
    }
    returnfalse
}


letavailableWidth:CGFloat=200// 文字表示に使える幅lettext="français 日本語 हिन्दी English\n\n言語混ぜるのっておもしろくない?"letfont= UIFont.systemFontOfSize(20)
// kCTFontAttributeNameの代わりにNSFontAttributeNameも使えるけど、CocoaとCore Textのattributesは違いあるので、間違いを防ぐためにCore Textのを使いましょう。letattributes= [
    kCTFontAttributeName as String:font,
    // ユーザーの言語が何であろうと、漢字は日本語フォントを優先に使ってほしいですね。
    kCTLanguageAttributeName as String:"ja",
]
// Core Textの説明書がCFAttributedStringを使うけど、NSAttributedStringとCFAttributedStringがtoll-free bridgedなので、もっと使いやすいNSAttributedStringを使います。letattributedString= NSAttributedString(string:text, attributes:attributes)
// Core Textのframesetterというのは文字列を指定される形の中にレイアウトするツールです。letframesetter= CTFramesetterCreateWithAttributedString(attributedString)
// 表示に必要な高さを計算します。letframeSize= CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(), nil, CGSize(width:availableWidth, height:CGFloat.max), nil)
letbounds= CGRect(origin:CGPoint.zero, size:frameSize)
// 文字列を長方形の中に表示したいだけですね。letpath= CGPathCreateWithRect(bounds, nil)
letframe= CTFramesetterCreateFrame(framesetter, CFRange(), path, nil)

letroundedRectangleRanges= [
    NSRange(location:10, length:5),
    NSRange(location:31, length:3),
]

letlines:[CTLine]= toArray(CTFrameGetLines(frame))
varlineOrigins= Array<CGPoint>(count:lines.count, repeatedValue:CGPoint.zero)
CTFrameGetLineOrigins(frame, CFRange(location:0, length:lines.count), &lineOrigins)

varrectsFound= [CGRect]()

for (lineIndex, line) in lines.enumerate() {
    letlineRange= NSRange(CTLineGetStringRange(line))
    letruns:[CTRun]= toArray(CTLineGetGlyphRuns(line))

    varascent:CGFloat=0vardescent:CGFloat=0
    CTLineGetTypographicBounds(line, &ascent, &descent, nil)
    letlineOrigin= lineOrigins[lineIndex]
    letlineMinY= lineOrigin.y - descent
    letlineMaxY= lineOrigin.y + ascent

    for roundedRectangleRange in roundedRectangleRanges {
        if!rangesOverlap(roundedRectangleRange, lineRange) {
            continue// この長方形がこの行に入りません。
        }

        varrectsForRangeOnCurrentLine= [CGRect]()

        for run in runs {
            letrunRange= NSRange(CTRunGetStringRange(run))
            if!rangesOverlap(roundedRectangleRange, runRange) {
                continue// この長方形がこのrunに入りません。
            }

            letglyphCount= CTRunGetGlyphCount(run)
            letallGlyphs= CFRange(location:0, length:glyphCount)
            letstringRanges= stringRangesPerGlyph(run)
            varpositions= Array<CGPoint>(count:glyphCount, repeatedValue:CGPoint.zero)
            CTRunGetPositions(run, allGlyphs, &positions)
            varadvances= Array<CGSize>(count:glyphCount, repeatedValue:CGSize.zero)
            CTRunGetAdvances(run, allGlyphs, &advances)

            // 以前に説明したとおり、グリフが並び順通りじゃないから、まずはグリフ1つずつ長方形を作ります。for (glyphIndex, stringRange) in stringRanges.enumerate() {
                if!rangesOverlap(roundedRectangleRange, stringRange) {
                    continue
                }
                letx1= positions[glyphIndex].x
                letx2= positions[glyphIndex].x + advances[glyphIndex].width
                letminX= min(x1, x2)
                letmaxX= max(x1, x2)
                letrect= CGRect(x:minX, y:lineMinY, width:maxX- minX, height:lineMaxY- lineMinY)
                rectsForRangeOnCurrentLine.append(rect)
            }
        }

        // このrangeのすぐ隣の長方形をくっつけます。
        rectsForRangeOnCurrentLine.sortInPlace { $0.minX <$1.minX }
        varmergedRects= [CGRect]()
        for rect in rectsForRangeOnCurrentLine {
            varrectToAppend= rect
            ifletpreviousRect= mergedRects.last {
                if rectAdjacentHorizontally(previousRect, rect) {
                    mergedRects.removeLast()
                    letminX= min(previousRect.minX, rect.minX)
                    letmaxX= max(previousRect.maxX, rect.maxX)
                    rectToAppend.origin.x = minX
                    rectToAppend.size.width = maxX - minX
                }
            }
            mergedRects.append(rectToAppend)
        }
        rectsFound.appendContentsOf(mergedRects)
    }
}


// 新しい画像を描き始めます。
UIGraphicsBeginImageContextWithOptions(frameSize, true, 2.0)
ifletcontext= UIGraphicsGetCurrentContext() {
    // 背景を白で塗ります。
    CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0, 1.0)
    CGContextFillRect(context, bounds)

    // 何もしないとCore Textの表示が逆さまなので正しい方向に表示されるようにします。
    CGContextTranslateCTM(context, 0, frameSize.height)
    CGContextScaleCTM(context, 1.0, -1.0)

    UIColor.cyanColor().setFill()
    for rect in rectsFound {
        letroundedRectanglePath= UIBezierPath(roundedRect:rect, cornerRadius:4)
        roundedRectanglePath.fill()
    }

    // 文字の表示です。
    CTFrameDraw(frame, context)
}
// 描いた画像を出します。letimage= UIGraphicsGetImageFromCurrentImageContext()
// 描き終えます。
UIGraphicsEndImageContext()

より良いデザインにするために大切にしたいと思っていること

$
0
0

こんにちは。デザイナーの遠藤です。 私は今クックパッドiOS/Androidアプリのデザインを担当しています。

みなさんは、既存の機能を別のプラットフォームに追加する際に、あまり考えずにそのまま追加してしまい、後で後悔したことはないでしょうか?今回は、web版にある機能「料理の基本」をクックパッドアプリへ追加した時のことを交えて、より良いデザインにするために大切にしたいと思っていることをご紹介します。

料理の基本について

レシピをみて料理を作っている際に、「あれ、半月切りって何だっけ?」「水にさらすってどれだけやればいいんだろう」のような、基本的なことさえわからず手が止まってしまったことはありませんか?クックパッドでは、そんな方のために料理の基本を提供しています。今回私は、この機能をiOS/Androidアプリのレシピページに追加する際、デザインを担当しました。

この機能の使い方は、レシピ中の手順欄にある言葉から利用できます。 例えば、スマートフォンwebの場合、レシピ中にところどころ薄く下線が引いてあります。 その下線の部分をクリックすると、詳しく内容が見れるようになっています。 (↓下記の例では、下線「みじん切り」をクリックした場合)

デザイン検討のプロセス

上記のスマートフォンwebの機能をアプリに追加する際に、下記のようにデザインを検討しました。

(1)現状把握(ユーザーのシーンを理解、今まで出来ていないことの洗い出し)

PCやスマートフォンweb版では、料理の基本のキーワードをタップした際、レシピページから料理の基本ページへ遷移します。ユーザーは料理中にレシピを見ていることが多いため、料理中に画面の行き来が発生することで、本来の目的から少し脱線し料理の進行を妨げてしまっているのではないか、ということを課題にしていました。 それを踏まえて、アプリではなるべく本来の目的の「料理をする」ということを妨げずに、知りたい情報を簡単に知れることを重視することになりました。

(2)そのシーンでは、「ユーザーはどんな情報が必要で、何をしたいのか?」アイデアを考える

料理中にわからない言葉に直面して困ってしまったというシーンでは、どういった情報が必要で、何ができると嬉しいのか?というアイデアを出しました。以下は、そのアイデアの一部です。

  • 3Dtouchの機能を使えばすぐに内容が知れて嬉しいのではないか?
  • 画面遷移して見に行くのは料理を妨げることにならないか?
  • 「料理の基本」を全文見せる必要はないのでは?

こういったアイデアをプロジェクトの担当者同士で意見し合い、ブラッシュアップしていきます。 また、アイデアだけでは実際に操作感がわからないため、すぐにUIを具体化していきます。(3)

(3)アイデアをプロトタイプしてデザインの方向性を決める

アイデアが浮かんだ段階で、どんどんプロトタイプを作っていきます。 また、今回iOSとAndroid両方での実装が必要だったため、両方でどういったデザインが必要かも都度検討していきます。 以下、プロトタイプの一例です。

プロトタイプを作っていくとたくさんの気づきがあり、方針がとても固めやすいです。 例えば、上記の一番左のプロトタイプでは、モーダルを表示している時に背景を暗くしてモーダルを目立たせていましたが、「暗くすると、本来の料理をすることを妨げることになるのでは?」という意見のもと、背景を暗くするのをやめました。

(4)デザインが決定したら、周りのデザイナーやエンジニアにレビューしてもらう

プロトタイプである程度方針を固めたら、次に細部のデザインをつめ、決定したら周りのデザイナーやエンジニアに触ってもらいレビューをもらいます。 ここでは、

  • 他の機能の挙動との齟齬がないか
  • 情報が適切か、見せ方が適切か
  • 実装が現実的にできるものかどうか

の観点でアドバイスをもらいます。

決定したデザイン

プロトタイプ作成を重ね、周りの人にレビューしてもらった結果、このようになりました。

iOS / Android

リリース後の評判はどうか?

リリース後、一部のブログなどで「嬉しいアップデートだった」というご意見をいただいているのを発見し、とても嬉しく感じています。とはいえ、まだまだ課題もたくさんありますので、引き続き今後も改善していく予定です。

まとめ

今回、下記の3点を行ったことにより、ただの機能追加にとどまらず、より良いデザインは何か?ということを追求することが出来たと思っています。

★現状把握をする

既存の機能の追加であれば、現在できていることと、出来ていないことをきちんと把握することが大事です。もし何も考えずにそのまま追加した場合、ユーザーが現状で感じている疑問や不安を無視してしまう可能性があります。現状把握をして振り返ることで、デザインする際に大事にしたいことが浮き彫りになってくると思います。

★「ユーザーがしたいことに対してどうアプローチするのか」という方針をある程度定める

今回で言えば、「ユーザーが料理中にわからない言葉に出会った時に、その時していることをなるべく妨げずに、知りたい情報を簡単に知れるようにする」という方針があったので、それを軸にデザインがスムーズに出来たと思います。

★アイデアを出した段階でなるべく早くプロトタイプを作る

アイデアを出しているときに、頭のなかで想像するものでは議論が進まず、悩みのポイントもどんどんずれてきてしまいます。実際に触れるものになって気づく点や、想像と違ったものになってしまったときどうするかという方向転換も早い段階でできたのがとても良かったと思います。

これらは、今後もデザインをする際に大切にしたいと思っていることです。 既存の機能を他のプラットフォームへ追加する際にどう見せるべきか迷っている方は、ぜひ試してみてください。


クックパッドでは、より良いユーザー体験を届けていきたい!というデザイナーやエンジニアを募集しています。

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

実践 Pact:マイクロサービス時代のテストツール

$
0
0

技術部の taiki45です。

以前「サービス分割時の複雑性に対処する: テスト戦略の話」という記事で、サービス間のインテグレーションテストにおける問題について紹介しました。現在のクックパッドではこの問題の解決のために Pactというツールを導入して運用しています。この記事では、その運用の知見を紹介できればと思います。

Pact

Pact は Consumer-Driven Contract testing (CDC testing)を実現するためのツールです。"Consumer"、"Provider"という見慣れない単語が出てきますが、この記事ではだいたい「Consumer = Web API クライアント」、「Provider = Web API サーバー」と対応ができます。この記事では具体的な Pact の利用例を通じて CDC testing がどういうものなのかについても紹介します。

必要になった背景

クックパッドでは、今までサービス間連携(他サービスの呼び出し結果を使うこと)の部分のテストには主に WebMockを使って書いていました。しかし、WebMock だと Provider が更新されても Consumer のスタブデータは更新されないので Provider 側の変更に Consumer のテストは追随できません。定期的にスタブデータを更新したり、RackVCRを使って Provider 側が変更されたら Consumer のスタブデータを更新することもできますが、それだとあくまで Consumer でのテストであるため、Provider が意図しない破壊的変更をリリースすることは防げません。それを防ぐには Provider のテストで Consumer に対する破壊的変更を検知できる必要があります。

そこで、Consumer の期待する振る舞いをプログラムから扱えるデータとして表現して、Provider に渡し、Provider 側でその「Consumer が期待する振る舞い」を満たしているか検証する手法で、Provider の変更がサービス間の連携を壊さないことを確かめることができます。この「Consumer が期待する振る舞い」を "Consumer-Driven Contract (CDC)"と呼んでいます。そして、この手法を実装に落とし込んだツールが Pact です。

Pact の仕組み

Pact では CDC は "pact file"と呼ばれるファイルへと JSON フォーマットで出力されて、Consumer の CI 時に生成されます。Consumer は CI 時に毎回 pact_broker と呼ばれるアプリケーションに pact file を "publish"し、pact_broker がバージョンや publish 日時といったメタデータと共に保管します。Provider は CI 時に pact_broker から自身が関連する Consumer が publish した pact file をチェックアウトして、新しい変更が Contract を壊していないか検証します。このテスト自体は pact file から RSpec の example を生成して実行することで実現されています。それぞれの example をパスさせるためには Provider のデータのセットアップが必要になることが多く、それは "Provider State"と呼ばれる機能で実現されています。Provider State は、Consumer が状態に一意な名前を付けて、Provider はその状態を再現するためのコールバックを登録できる仕組みです。

実際のフローは以下のようになります:

  1. Consumer プロジェクトがテスト内で Contract を記述する。Contract はリクエストと、そのリクエストに対応するレスポンスと期待する副作用が含まれる。
  2. Consumer のテスト実行時にモックサーバーを起動させる。Consumer は HTTP クライアントの向き先を起動したモックサーバーに向けてリクエストを発行する。モックサーバーは登録された Contract を元にレスポンスを返す。
  3. テスト成功後に Consumer は定義した Contract を JSON 形式でファイルに書き出し、pact_broker と呼ばれるサーバーにアップロードする。
  4. Provider は CI で pact_broker から関係する Consumer(s) がアップロードした Contract をダウンロードして、その Contract 通りに自身が振る舞うかテストを実行する。

work_flow_of_pact

https://github.com/realestate-com-au/pact/blob/v1.9.2/README.mdより

実際のコードを見たほうがより理解しやすいと思うので、ここで示します。

次のような Consumer の期待が存在するケースを例にします:

「レシピが2つ存在している前提で、Consumer が Provider の /v1/recipesに対して GET リクエストを送信した時に、Provider は [recipe_a, recipe_b]となる構造や値のデータを Content-Type: application/jsonのようなヘッダー値とともにレスポンスする」

このケースでは Consumer 側のテストで以下のように Pact を使って HTTP リクエストをモックするとともに、Contract を記述できます:

RSpec.describe Recipe, pact: truedo
  before do
    allow(described_class.client).to receive(:endpoint).and_return(provider_app.mock_service_base_url)
  end

  describe 'get_all'do
    let(:recipe_a) { { id: Pact.like(1), name: Pact.like('Curry') } }
    let(:recipe_b) { { id: Pact.like(2), name: Pact.like('Salada') } }

    before do
      provider_app.given('there are 2 recipes').
        upon_receiving('a request for recipes').
        with(method: :get, path: '/v1/recipes').
        will_respond_with(
          status: 200,
          headers: {
            'Content-Type' => Pact.term(generate: 'application/json', matcher: %r{application/json}),
          },
          body: [recipe_a, recipe_b]
        )
    end

    it 'returns recipes'do
      recipes = described_class.get_all
      expect(recipes.size).to eq(2)
      expect(recipes.first.name).to eq('Curry')
    endendend

上記コードでは、まず、HTTP クライアントの API call 先をモックサーバーに向けています。その後、Pact のモックサーバーに対して Contract の登録をしています。テストケース内の described_class.get_allが評価されると、実際に HTTP リクエストがモックサーバーに送信され、セットアップしたレスポンスが返却されます。以上の流れでこのテストは成功します。

先ほどのテストを実行すると pact file が生成されます。pact file には Consumer/Provider の情報、Consumer が期待するリクエスト/レスポンスが含まれています:

{"consumer": {"name": "ConsumerApp"
  },
  "provider": {"name": "ProviderApp"
  },
  "interactions": [{"description": "a request for recipes",
      "provider_state": "there are 2 recipes",
      "request": {"method": "get",
        "path": "/v1/recipes",
        "query": "fields=media%5Bcustom%5D&image_size[recipe]=280"
      },
      "response": {"status": 200,
        "headers": {"Content-Type": {"json_class": "Pact::Term",
            "data": {"generate": "application/json",
              "matcher": {"json_class": "Regexp",
                "o": 0,
                "s": "application/json"
              }}}},
        "body": [{"id": {"json_class": "Pact::SomethingLike",
              "contents": 1},
            "name": {"json_class": "Pact::SomethingLike",
              "contents": "Curry"
            }},
          {"id": {"json_class": "Pact::SomethingLike",
              "contents": 2},
            "name": {"json_class": "Pact::SomethingLike",
              "contents": "Salada"
            }}]}}],
  "metadata": {"pactSpecificationVersion": "1.0.0"
  }}

Provider 側では、この pact file を使って自身が振る舞うかテストを実行します。 Pact を使った CDC testing では、単体テストと同じように、Provider の検証テスト実行毎に環境を初期化します。そのため、Pact が "Provider States"という前提条件を記述する仕組みを提供していて、Provider の検証時にはその仕組みを使ってテスト時にデータのセットアップをします。上記の例では、there are 2 recipesという文字列が Consumer から指定され、Provider はその前提条件をセットアップするためのロジックを記述します:

Pact.provider_states_for 'ConsumerApp'do
  provider_state "there are 2 recipes"do
    set_up do%w[Curry Salada].each {|name| Recipe.create!(name: name) }
    endendend

データのセットアップロジックが揃うと、後は Pact が提供する Rake タスクを使って Provider の振る舞いを検証するテストを実行します。テストが成功することを確認して一連のワークフローが完了します。

ここでは一部の Pact を使った CDC testing の一部のコード例を示しましたが、より詳細なコード例は Pact の README にあります。

Consumer のテストで WebMock などを使って HTTP リクエストを単にスタブするのに似ていて、Pact はさらにそこに Consumer が暗黙に期待していた内容をやりとりできるように pact file に落としこむ点が加わっています。

Pact というツールは、コアとなる機能を複数の言語実装から利用できるように、いくつかのコンポーネントに分割されています。ここでは主に Ruby 実装である pact gem を中心に、その周辺のコンポーネントと合わせて紹介します。

pact gem

https://github.com/realestate-com-au/pact

Ruby 用の Pact ライブラリです。各種設定機能や、Consumer のテストで Contract を定義するための DSL が実装されています。また、Contract を読み込んで、それをもとに RSpec のテストを自動で行うことによって Provider を検証する機能が実装されています。

pact file の生成やモックの登録や検証といった主要な機能は後述の pact-mock_service gem で実装されています。

pact-mock_service gem

https://github.com/bethesque/pact-mock_service

上述のように Pact のコアとなるような機能が実装されています。

  • Consumer が期待するリクエスト、レスポンスの組の登録を受け付ける
  • Web サーバーとして動作し、リクエストを受け付けて登録されたレスポンスを返却する
  • 登録されたリクエストが期待するパラメータで呼ばれたどうかを検証する
  • pact file を生成する

pact-mock_service はコマンドライン経由で起動できます。また、Ruby のプロセス内から実行することもできます。

pact-mock_service の役割

典型的なリクエスト/レスポンスを見てみると pact-mock_service の役割と Pact の仕組みについてわかりが得やすいのでここで試します。

まず、モックサーバーを起動します:

$ pact-mock-service service --pact-dir=. --port=3000

とりあえずヘルスチェックをします:

$ curl -H 'X-PACT-MOCK_SERVICE: 1' -v http://localhost:3000
* Rebuilt URL to: http://localhost:3000/
*   Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.43.0
> Accept: */*
> X-PACT-MOCK_SERVICE: 1
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Access-Control-Allow-Origin: *
< X-Pact-Mock-Service-Location: http://localhost:3000
< Server: WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25) OpenSSL/1.0.2e
< Date: Tue, 21 Jun 2016 15:20:02 GMT
< Content-Length: 20
< Connection: Keep-Alive
<
* Connection #0 to host localhost left intact
Mock service running

Pact では Provider/Consumer 間で期待されるリクエスト/レスポンスの組を Interaction と呼びます。Pact の Contract は、Provider や Consumer に関する情報の他に、複数の Interaction から構成されています。

ここでその Interaction を1つ登録します:

$ curl -X POST -H 'X-PACT-MOCK_SERVICE: 1' -H 'Content-Type: application/json' \
  http://localhost:3000/interactions \
  -d '{"description": "a request for recipes", "request": {"method": "get", "path": "/recipes"}, "response": {"status": 200, "headers": {"Content-Type": "application/json"}, "body": "[]"} }'

Set interactions

登録した Interaction が呼び出されたかどうか検証します。ここではまだ呼び出してないのでエラーが返ります:

$ curl -H 'X-PACT-MOCK_SERVICE: 1' -v 'http://localhost:3000/interactions/verification?example_description=a%20request%20for%20recipes'
*   Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> GET /interactions/verification?example_description=a%20request%20for%20recipes HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.43.0
> Accept: */*
> X-PACT-MOCK_SERVICE: 1
>
< HTTP/1.1 500 Internal Server Error
< Content-Type: text/plain
< Access-Control-Allow-Origin: *
< X-Pact-Mock-Service-Location: http://localhost:3000
< Server: WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25) OpenSSL/1.0.2e
< Date: Tue, 21 Jun 2016 15:36:26 GMT
< Content-Length: 144
< Connection: Keep-Alive
<
Actual interactions do not match expected interactions for mock MockService.

Missing requests:
    GET /recipes

* Connection #0 to host localhost left intact
See standard out/err for details.

登録した Interaction を呼び出します:

curl http://localhost:3000/recipes

[]

もう一度 Interaction が呼び出されたか検証します。すでに呼び出しているので今回は成功レスポンスになります:

curl -H 'X-PACT-MOCK_SERVICE: 1' -v 'http://localhost:3000/interactions/verification?example_description=a%20request%20for%20recipes'
*   Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> GET /interactions/verification?example_description=a%20request%20for%20recipes HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.43.0
> Accept: */*
> X-PACT-MOCK_SERVICE: 1
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Access-Control-Allow-Origin: *
< X-Pact-Mock-Service-Location: http://localhost:3000
< Server: WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25) OpenSSL/1.0.2e
< Date: Tue, 21 Jun 2016 15:36:54 GMT
< Content-Length: 20
< Connection: Keep-Alive
<
* Connection #0 to host localhost left intact
Interactions matched

pact file の生成リクエストを送ります:

$ curl -X POST -H 'X-PACT-MOCK_SERVICE: 1' -H 'Content-Type: application/json' http://localhost:3000/pact -d '{"consumer": {"name": "A Consumer"}, "provider": {"name": "A Provider"}}'

{
  "consumer": {
    "name": "A Consumer"
  },
  "provider": {
    "name": "A Provider"
  },
  "interactions": [
    {
      "description": "a request for recipes",
      "request": {
        "method": "get",
        "path": "/recipes"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body": "[]"
      }
    }
  ],
  "metadata": {
    "pactSpecificationVersion": "1.0.0"
  }
* Connection #0 to host localhost left intact
}

pact file が ${consumer_name}-${provider_name}.jsonという命名規則で書き出されています:

$ cat a_consumer-a_provider.json
{
  "consumer": {
    "name": "A Consumer"
  },
  "provider": {
    "name": "A Provider"
  },
  "interactions": [
    {
      "description": "a request for recipes",
      "request": {
        "method": "get",
        "path": "/recipes"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body": "[]"
      }
    }
  ],
  "metadata": {
    "pactSpecificationVersion": "1.0.0"
  }
}

pact gem は Consumer のテスト実行時に DSL で登録された内容を元に、このようなリクエストをモックサーバーに対して実行することで Pact の機能を実現しています。

pact-mock_service gem は pact 以外からも利用できるように設計されているので、Ruby 以外の言語で Pact の Consumer を実装する時に pact-mock_service が利用できます。例として pact-consumer-swiftという Swift 向けの Pact Consumer ライブラリがあります。 クックパッドでも pact-mock_service を利用した Swift 向けの Pact Consumer ライブラリを作成しました。そのうち公開されると思います。

また pact-mock_service 相当の機能を独自に実装しているような Pact ライブラリも存在します。例として pact-jvmがあります。

pact_broker

https://github.com/bethesque/pact_broker

Consumer と Provider が pact file をやりとりするための仲介サーバーです。 Pact を使って CDC testing を実行するのに pact_broker は必須でありません。例えば Consumer と Provider を同じチームが開発している場合、手元での開発中は pact file をローカルのファイルシステムのどこかに置き、pact verification 時にそのファイルパスを指定する、ということもできます。しかし、複数の CI サーバーで各 Consumer/Provider の CI が実行されるような環境では、ストレージは独立していたほうが使い勝手が良く、かつ pact file のバージョンや更新日時や差分などのメタデータも合わせて管理できたほうが便利です。なので pact_broker という仲介サーバーが提供されています。

上述した機能の他に、各 Consumer/Provider 間の関係を可視化する機能が用意されていて、意外と便利です:

network_diagram

https://github.com/bethesque/pact_broker/blob/v1.9.2/README.mdより

pact_broker は Rack アプリケーションとして実装され gem として提供されています。公式に Docker イメージも提供されています。

また Ruby 向けクライアントライブラリとして pact_broker-clientが提供されています。

後述しますが、クックパッドでも pact_broker サーバーを用意して Provider/Consumer 間のやりとりに利用しています。

pact-specification

https://github.com/pact-foundation/pact-specification

Pact における Contract のデータ構造とマッチングの仕様に関するテストケース群が集まっているリポジトリです。 Ruby 実装の pact gem 以外に、pact-jvm など、他言語による実装がありますが、仕様に沿って実装されている限り Consumer と Provider の言語が異なっても互換性があるようになっています。

クックパッドでの Pact の運用

基本的には Pact が想定する使い方やワークフローを採用しています。Consumer の CI で pact file を生成し pact_broker へ publish、Provider の CI で pact verification を実行しています。

手元ではデフォルトで pact file を生成しない

開発者の手元でテストを実行する時には pact file を生成する必要がないので、pact file を生成しないようにしています。CI ジョブでテストを実行する時、もしくは明示的に指定された時のみ生成するようにしています。

pact file を生成しないオプションを追加するパッチを送り、環境変数でそのオプションを制御しています。

pact verification の結果を見やすくする

規模が大きいアプリケーションでは、pact verification が失敗した時に出力が多く、Jenkins のコンソール画面だと内容が把握しにくい問題がありました。そこで JUnit format で検証結果を出力する gemを作り verification 失敗時に素早く内容を把握できるようにしています。

pact file のバージョニング

Pact では pact file を pact_broker にアップロードする際にバージョンをつけることができます。pact_broker では x.y.zのようなよくあるバージョニング以外に、独自にバージョニングロジックを定義することができます。 クックパッドでは頻繁にリリースを行うため、バージョンを表すのに単に git revision hash が良く使われています。git revision hash 単体ではバージョン番号として順序を表すのに不適当なのと、日時を得るまでに1ステップ必要になってしまうので、日時と git revision hash 値を連結した値を pact_broker におけるバージョンとして採用しています。

pact_broker の運用

公式に Docker イメージが公開されているのですが、上記のバージョニングロジックや、ヘルスチェックエンドポイントの追加など、いくつか変更する箇所があったのと、社内にすでに Web アプリケーション用のベースイメージが存在しているので、独自にイメージを作成してそれを利用しています。

pact_broker には内部からのみアクセスできるようにアクセス制御をかけています。データベースには RDS 上の PostgreSQL インスタンスを利用しています。また、CI 環境から publish する本番環境と、チュートリアルやテスト用途に利用できるサンドボックス環境を用意しています。

動的なポート確保

Pact のチュートリアルでは Consumer がモックサービスを設定する際に固定なポート番号を指定しています。ポート番号を手で管理するのは面倒なので、動的なポート確保機能を使うようにしています。

以前の pact-mock_service でも find_a_port gemを使用して動的に空いてるポートを確保する実装が存在していたのですが、社内にある RRRSpecを利用して分散テスト実行しているアプリケーションのテストで webkit-server が起動しないなどの不具合があったため、モックサーバーを起動する時に port 0 に bind する実装に修正した上で pact gem からも使えるようにしました。

Remote Facade パターンと pact-expectations

サービス間通信を利用するようなアプリケーションでは設計のガイドラインを設けていて、Remote Facade パターンを推奨しています。特にテストで Pact を使う際は Contract の数や種類を極力減らしたいので、様々な層のテストで個別に Pact を使って HTTP リクエストをスタブするのではなく、Remote Facade のテストでのみ Pact を使い、その他の層では Remote Facade をモックするようにしています。

Remote Facade パターンを単純に適用するだけだと、Pact を使わずにモックしている箇所でその Expectation が検証されない問題がありました。Pact で記述する Expectation をそのまま Remote Facade をモックする際にレスポンスとして利用することでその問題を解決し、人事部長氏の手によって pact-expectations gemという実装が作られ、利用しています。

集約的ライブラリの作成

上記のようなバージョニングロジックや、Pact を利用するのに必要なヘルパーの初期生成や、Railtie を使った Zero-Configuration 化などをするために、集約的ライブラリを作成しています。主に社内向けの設定を出力するので、このライブラリは公開していません。

まとめ

Consumer-Driven Contract testing を実現する Pact というツールについてと、クックパッドにおける Pact の運用について紹介しました。

今回 Pact を導入するにあたり、遭遇した不具合を修正したり機能を追加しました。例えば、UTF-8 への対応や、Expectation 内の query をネストできるようにすることや、pact verification の際に pact file の取得をリトライするようにするなど。みなさんが Pact を導入する際に、この記事及び Pact への貢献がお役に立てると幸いです。

クックパッドでは自由に OSS に貢献することのできる環境や文化があります。共にワイワイ OSS に貢献していく仲間を募集してます。応募をお待ちしています。

http://recruit.cookpad.com/jobs/career_recruitment


One more thing: 8月の iOS Developers Conference Japan で、トークが採択されれば Pact の話をするようなので、興味のある方は是非。

https://iosdc.jp/2016/c/node/62

そのメールアドレス、現在も使っていますか?

$
0
0

こんにちは。ユーザーファースト推進室ディレクターの大黒です。

ありがたいことにクックパッドは今年で20年目をむかえ、数多くのユーザーに利用されるまでに成長しました。それ故に発生する課題もあり、今回はその中でもユーザー登録に使われているメールアドレスの課題と対策をご紹介したいと思います。

ユーザー登録の仕組み

クックパッドのユーザー登録では、下記の項目が必要となります。

  • メールアドレス
  • パスワード
  • 郵便番号
  • 生年月日

※iOSアプリでは郵便番号と生年月日は任意入力となります

f:id:kotsuru0812:20160628234150p:plain

SNSアカウント認証や認証コードでのアクティベートを採用するサービスが今では主流ですが、20年続くサービスであれば一般的なユーザー登録フローではないでしょうか。しかしながら最近のスマートフォンユーザーの多くはメールを使わないという実態も分かっているため、ユーザー登録にメールアドレスを使い続けるかどうかは、別途議論を進めているところです。

メールアドレスの課題

一度取得したメールアドレスを生涯にわたって使い続けるという人は多くありません。SNSやメッセージアプリが普及した今では、メールでやり取りをすることが少なくなり、ナンバーポータビリティが可能になったことも相まって、キャリアのメールアドレスが変わることへの抵抗感はほぼありません。また、登録した時のメールアドレスとパスワードさえ覚えていれば、実際にそのメールアドレスを使っていなくてもログインできるので、メールアドレスを変更せずに使い続けるという人も出てきます。

例えば私たちが把握しているメールアドレスの課題としては、次のようなものを挙げることができます。

3〜6ヶ月でメールアドレスは再利用される

キャリアやプロバイダーにもよりますが、使われなくなったメールアドレスは3〜6ヶ月程度の保留期間を経て、再利用が可能になります。メールが普及してからの年月を考えると、多くのメールアドレスが再利用されていると予想されます。

登録できない人がでてくる

クックパッドでは、1つのメールアドレスで作れるアカウントの数は1つだけです。もしユーザーAさんが取得したメールアドレスが、過去にクックパッドのユーザー登録に使われていて、誰かがメールアドレスを変更せずに使い続けているとしたら、その人がメールアドレスを変更するか、ユーザーAさんが別のメールアドレスを取得しないとユーザー登録ができないことになります。

以上のことをふまえると、フィーチャーフォン全盛期からサービスを続けているクックパッドとしては、ユーザー登録、もしくはメールアドレス変更から一定期間経過しているユーザーに対して、登録しているメールアドレスを現在も使っているかどうかを確認することが大切だと考えました。

最小の実装で確認施策を行う

ユーザーにメールアドレスが最新であるかどうかを確認するような施策は、他社ではあまり事例がなく、社内でも知見が全くない状態から始まりました。このような場合はなるべく既存の仕組みを使い、対象者をできるだけ絞り込んでコンパクトなサイズでまず試してみることが大切です。

重要なお知らせという仕組み

クックパッドには返金などが必要になったユーザーに対して、案内を通知する仕組みを持っています。これはアプリで使われている仕組みですが、PUSH通知とアプリのトップスクリーンに導線が表示されるため、ユーザーに内容を確認してもらえる確率が非常に高くなります。今回はこの仕組みの一部を使い、何度かテストしてみることにしました。

f:id:kotsuru0812:20160628234542p:plain

対象者を定義する

メールアドレスの課題を考慮すると、キャリアのメールアドレスで登録、またはキャリアのメールアドレスに変更してから一定期間経過しているユーザーの場合、機種変更などによりメールアドレスが変わっている可能性が高いという仮説がたてられます。あとは一定期間をどうするかですが、メールアドレスの保留期間が3〜6ヶ月程度なのを考慮すると、6ヶ月以上経過している人たちを対象とした方がよさそうということになりました。

施策の実施

f:id:kotsuru0812:20160628234637p:plain

施策の評価は下記の数値を元に行いました。

  • 変更率
    メールアドレスをいまは使っていないと判断し、新しいメールアドレスの登録が完了したユーザーの割合

  • 変更手続き中
    メールアドレスをいまは使っていないと判断し、新しいメールアドレスの登録が完了していないユーザーの割合

  • アクション率
    上記の変更率と変更手続き中に加え、「使っているメールアドレスです」と明示的に回答してくれたユーザーの割合

第1回テスト

PUSH通知を大量に配信すると、サイトに思わぬ負荷がかかる場合があるので、インフラ面でも問題がないことを確認する必要があります。なので最初のテストでは、無事にPUSH通知が配信できるか、ユーザーがPUSH通知を開いてくれるかという点を中心に検証しました。

配信数 変更率 変更手続き中 アクション率
300 1% 1% 35%

結果を見てみると、多くのユーザーがPUSH通知に反応してくれたことが分かります。インフラ面でも問題がないことが分かったため、今度はメールアドレスを変更したい人たちが無事に変更することができるかを検証することにしました。

第2回テスト

このテストではある程度配信数を増やし、メールアドレスを変更するユーザーが多くなるよう抽出条件を変更し、メールアドレスの更新が古いユーザーを対象としました。

配信数 変更率 変更手続き中 アクション率
1,000 5.1% 1.8% 40.3%

結果としては変更手続き中で止まっているユーザーが、前回と同じくらいの割合でいることが分かります。このことからメールアドレス変更フローを見直してみたところ、ユーザーが躓くポイントがあることが判明したため、既存のメールアドレス変更フローを改善することにしました。

メールアドレスを変更するには、パスワードの入力が必要になります。登録しているメールアドレスを現在使っておらず、パスワードを忘れてしまうと、パスワードの再設定を行うことができないため、メールアドレスの変更ができずフローの途中で止まってしまいます。

改善前f:id:kotsuru0812:20160628234726p:plain

別件でメールアドレス変更フローの改善を行った際に、パスワードを忘れてしまった場合の考慮が漏れていました。この場合、サポートにお問い合わせをして本人確認を行うことで、パスワードの再設定をすることができるので、パスワード入力画面にお問い合わせへのリンクを設置することにしました。

改善後f:id:kotsuru0812:20160628234814p:plain

この改善を行った結果、この導線から1日平均約3件のお問い合わせがありました。言い換えると、メールアドレスを変更しようと思ったがパスワードが分からず、どうしていいか困っていた人が1日平均約3人ほどいたということになります。

第2回のテスト結果としてはアクション率は前回と同じくらい高く、PUSH通知に対してのネガティブなご意見もなかったため、メールアドレス変更フローの改善を行い、施策としてはこのまま進めるという判断になりました。

まとめ

実際に登録しているメールアドレスを現在も使っているかどうか確認してみると、「使ってるメールアドレスです」というアクションをしてくれたユーザーもたくさんいて、ユーザーにとっては非常に関心が高いことだと分かりました。提供するサービスにもよりますが、サービス側から定期的に「登録してるメールアドレス使ってる?」と聞いてあげることは、とても大事なことです。しかしながら冒頭にも書いた通り、いつまでユーザー登録にメールアドレスを必須にするかについては引き続き議論を重ね、今後の改善に繋げていきたいと思っています。

ActiveRecordを使ってRedshiftから大量のデータを効率的に読み出す

$
0
0

こんにちは、トレンド調査ラボの井上寛之(@inohiro)です。 普段は、クックパッドの検索ログを基にした法人向けデータサービス「たべみる」の開発や、 広告事業周辺のデータ分析などを担当しています。

Amazon Redshiftなどのデータベースに蓄積されたログなどの大量のデータに対して、 日次や週次などの単位でバッチ処理を行っている方は多くいらっしゃると思います。 ログなどを扱うバッチ処理では、処理対象が膨大であるとアプリケーションが使うメモリが増大し、 枯渇してしまう恐れもあるため、データの扱いに気をつける必要があります。 データベース内で完結するバッチ処理ならばそこまで気にする必要は無いかもしれませんが、 外部のプログラムからデータを読み出して処理する場合は特に注意が必要です。

そこで考えられる一つの工夫として、処理対象を分割して、繰り返して処理を行う方法が挙げられます。 一般的なRDBMSが備えるカーソルと呼ばれる機能を利用することで、処理対象を分割して少しずつ処理することができます。

本稿では、特に Rails(ActiveRecord)を使って書かれたAmazon Redshiftを利用するようなバッチ処理において、 カーソル機能を簡単に利用できる "redshift_cursor" gem を紹介します。

まずカーソルについて、もう少し詳しく説明したいと思います。

そもそもカーソルって?

カーソルはデータベースからデータを得るする際に、一度にすべてのデータを読み出すのではなく、 ある程度の単位(行数)に分けて読み出すための仕組みです。イテレータのように動作することで、 アプリケーション側のメモリの枯渇を防ぐことができます。

PostgreSQLやMySQLなどの、一般的なRDBMSにはカーソル機能が備わっていて、すぐに使うことができます。 もちろんRedshiftにもあります。それぞれのRDBMSのカーソルについては以下を参照してください。

以下は、PostgreSQLで、カーソルを使って大きな結果から10行ずつ読む例です。

begin ; -- カーソルはトランザクションの中で使う
declare sample_cursor cursorfor-- カーソルを宣言select title
    from recipes
    where title like'%トマト%' ;

fetch 10from sample_cursor ; -- 最初の10件を得る
fetch 10from sample_cursor ; -- 次の10件を得る-- 必要なだけ繰り返す

close sample_cursor ; -- カーソルを閉じるcommit ;

Railsにおけるカーソル的な処理

大量のクエリ結果を少しずつ取り出して処理を行う場合、 Railsだと ActiveRecord::Batches.find_each.find_in_batchesを利用する方も多いのではないでしょうか。 .find_eachおよび .find_in_batchesの詳しい説明は割愛しますが、 これらのメソッドを使う際は、以下の点で注意が必要です。

  • ソートカラムが指定できない(プライマリキー(大抵 idカラム)でソートされる)
    • 特にログ系のテーブルだと idカラムが付いてなかったり、そもそもプライマリーキーが設定されていないこともある
    • また、日付カラムがソートキーとなっている可能性が高く、意図しないキーで大量の行をソートしてしまうおそれがある
  • プライマリキーが必ず数値型である必要がある
  • チャンク毎にクエリが何度も再実行される
    • カーソルはクエリを一度だけ実行し、結果をチャンクに分けて返す

ソートカラムが明示的に指定されていない状態で、チャンク毎にクエリが再実行されると、 得られた結果が正しくない可能性も考えられます。 またチャンク数分、同じようなクエリが発行されるので非効率とも言えます。 以上のことから、カーソルを利用する方がパフォーマンスや信頼性の面で良いと言えます。

redshift_cursor

さて、カーソルについて簡単に説明しましたが、ここからが本題です。 今回紹介するredshift_cursor gemは、 Rails(ActiveRecord)でRedshiftに接続して大量の行を得るような場合に、 カーソルの構文を覚えなくても、カーソルを透過的に利用できるようにするgemです。 redshift_cursorは実際にクックパッドの一部のバッチジョブで、ログの集計やユーザーの抽出に利用されています。

以下、この gem の使い方を簡単に説明します。

まず Gemfile に記述して bundle installします。

# Gemfile
gem 'redshift_cursor'

すると、各モデルで.each_row, .each_instance, .each_row_by_sql, .each_instance_by_sqlなどのメソッドが使えるようになります。 .each_row, .each_row_by_sqlは結果をハッシュの配列で、.each_instance, .each_instance_by_sqlは結果をレシーバークラスのインスタンスの配列で返します。

Recipe.where(id: 3199605).each_row.fitst
=> {"id"=>"3199605", "title"=> "簡単 生地なし!キヌアキッシュ", ... }

Recipe.where(id: 3199605).each_instance.first
=> #<Recipe:0x007fe5260eeaa8 id: 3199605, title: "簡単 生地なし!キヌアキッシュ", ...>

これらのメソッドは Enumerable を返すので、結果に対して .map.eachなど使うこともできます。 以下、利用例です。

# タイトルが「ほうれん草」にマッチするようなレシピRecipe.where('title like ?', '%ほうれん草%').each_insntace.map {|recipe| recipe.title }

# 必要なカラムがタイトルだけならRecipe.where('title like ?', '%トマト%').select(:title).each_row.map {|recipe| recipe['title'] ... }

# 条件や順序をSQLで書くRecipe.each_instance_by_sql('select * from recipe where ... order ...').map {|recipe| recipe.created_at }

# ヒアドキュメントでSQLを書くSearchLog.each_row_by_sql(<<~SQL
    select
        title
        , count(*) as pv
    from
        search_logs
    where
        keyword like '%ズッキーニ%'and log_time between ...
    group by
        title
SQL
).each {|log| log['pv'] ... }

.each_row_by_sql, .each_instance_by_sqlでは Array Conditionが使えないことに注意)

上記のコードは、それぞれカーソルを使ったクエリに書き換えられ、 利用者はカーソルの構文や仕組みを新たに覚えなくても、大量のデータを効率的に扱うことができます。

実装

実はredshift_cursorの大部分は、postgresql_cursor gemを利用しています。 redshift_cursorは、activeRecord4-redshift-adapterを使ってRailsからRedshiftに接続している時に、postgresql_cursorが正しく使えるように互換性を追加しています。

まとめ

本稿では、バッチ処理等で大量のデータを読み込む際にアプリケーション側の負荷やパフォーマンスを改善する カーソルについて説明しました。

またRails(ActiveRecord)を使って書かれたAmazon Redshiftを利用するようなバッチ処理において、 カーソル機能を簡単に利用できる "redshift_cursor" gem を紹介しました。 「postgresql_cursor」というよく出来たPostgreSQL向けのgemをRedshiftでも使えるようにしたgemです。

Gemfileに追加して、ActiveRecordのメソッドとよく似たメソッドで、 カーソルを使ったクエリを簡単に発行することができます。 ぜひログなどの大量のデータをバッチ処理するときにご活用ください。

追記(2016/07/12)

(ブコメでもご指摘いただいておりますが、)10万行を超えるような結果を読み出すならば、 カーソルではなく UNLOADコマンドを使いましょう。

社内でもこのポリシーで運用しています。


ディレクターがSQLを使えてよかった話

$
0
0

こんにちは。ディレクターの川原田です。 クックパッドでお気に入りレシピを保存する「MYフォルダ」のサービス開発や、保存・記録に関する新規サービスの検討・開発を担当しています。

ディレクターの仕事は様々ありますが、今回は私が身につけたことで仕事領域が広がった!と感じているSQLについてお話ししたいと思います。

いきなりですが、SQLが使えてよかった点をまとめると以下です。

よかったこと

  • 数値抽出から分析まで自己完結
  • エンジニアとのコミュニケーションがスムーズに
  • 仕事が増えていそうで実は効率アップ
  • 周囲の知的好奇心を刺激

それぞれ具体例を交えてお話します。

数値抽出から分析まで自己完結

事例1:ログ構造を理解でき後の仕事がスムーズに

昨年、アプリのサービス開発を担当した際、エンジニアの設定したログが、実際に送信されるかどうかを事前チェックをしました*1

アプリのリリースはタイミングが決められており、ウェブのようにリリース後の修正がすぐには反映できません。そのため、仕様や設定にミスがあると修正が反映されるまでの期間のログが欠損してしまいます。

ログの事前確認により、欠損を防げただけでなく、新規に設定したログデータの構造も理解できていたので、リリース後すぐにデータベースに蓄積したログをSQLで抽出し、利用状況の把握を自ら行うことができました。今まではエンジニアに抽出してもらうことが多かったのですが、自己完結できたことでエンジニアの負荷を軽減できました。

また、この時はログデータ構造を理解した際に、出したい数値を抽出するためのSQLを書いておいたのですが、この虎の巻がリリース後の自分やチームを助けました。

事例2:エンジニアのタスクを削減

SQLを使え、ログ抽出を自ら行うとデータ構造の理解も進むため、必然と会社のサービス全体のログ構造の理解も進みます。

今年初めに、Web版のクックパッドに機能追加を行ったことがありました。その際、既存のログ構造を把握できていたため、今回の機能においては新規のログ設定は不要、とディレクター側で判断がつきました。そのため、この時の開発においては、ログの設計や設置というエンジニアタスクが削減されました。

SQLがわからずともデータ構造を理解していれば同じことができそうですが、実装時にテスト環境で検証する際、テスト環境のログもSQLで抽出ができたので、今回に関してはログの新規設置が不要だ!と自ら確信が持てたことはよかったです。

エンジニアとのコミュニケーションがスムーズに

事例3:データ設計の検討時に、エンジニアと会話がちゃんと出来る

事例2の時、見たい数値が既存ログで対応できそうという予測がつけられ、実際に取得ができたことをエンジニアに共有することで、エンジニアも新たにログの設定が不要であることが理解できました。

今まではどのようにログが設定されるのか理解できていない状態で依頼をしていたので、設定後取得できないものがあったり、より深く調査したいと思っても不可能なことがありました。

SQLを身につけたことで、ログ設計の検討をディレクターだけで出来たり、エンジニアが行ったログ設計の内容を聞いてKPIの数値が確実に取れそうかの判断がついたりと、設計検討時に、より具体的な話ができるようになりました。私からのオーダーがエンジニアに伝わりやすくなりました。

事例4:エンジニアが書いた長いSQLをダブルチェックでき、書いた人も依頼した私もお互い安心(逆もしかり)

この頃、社内ではre:dashというツールを各部署で使っています。re:dashでは、SQLで抽出したデータをそのままグラフにでき、任意の間隔で自動更新ができます。また、複数のグラフやデータを組み合わせてダッシュボードとしてまとめられます。今までは抽出データをスプレッドシートに貼り付け、グラフ化し、そのシートを共有していました。

re:dashダッシュボードのサンプル f:id:erikwrd:20160704110904p:plain

このre:dashの浸透により、様々なデータの可視化が便利な一方、可視化させたいことが多くあり、グラフ化するために複雑で長いSQLを書く機会が増えました。例えば「あるサービスに3ヶ月継続して訪問している人をアクティブ、それ以外を非アクティブして、その2グループの有料会員の継続率(1年間月ごと)を出す」などです。

少し時間はかかりますが、私のチームでは、あまりにも複雑そうなものはエンジニアと私で独自でSQLを書いてみて、結果を見比べ、その後にSQLを互いにレビューするという手法を取っています。

細かな点の漏れ(アプリのバージョン指定が間違っている、サービス閲覧の定義が足りていないなど)が見つかり、お互いのミスをカバーしあえます。

今では、エンジニアに複雑な抽出を任せっきりにすることは、お互いに不安だと感じるようになりました。

仕事が増えていそうで実は効率アップ

SQLを扱えると仕事が増えるのでは?と、思われるかもしれないですが、事例4に書いたre:dashというツールの採用もあり、以前よりも新規プロダクトの定期的な数値分析の効率はアップしています。

新規のプロダクトだと新たなログ集計ツールの用意自体が工数増になるので省略することが多く、日々SQLで抽出してスプレッドシートに貼って、、、ということが多々ありました。今ではすべてre:dashにお任せしています。

また、プロダクトや目的ごとにダッシュボードを作ることで、改めて分析し直す時や新たなプロダクトの分析をする際も、過去の様々なSQLから参考になりそうなものをさっと取り出せるので、ゼロから考えることが減りました。

さらに私自身のSQLの理解が進んだことで、見たい数値が取れてない!ということもなくなり、エンジニアとの認識齟齬もなくなっているので、結果としてチームの効率もアップしています。

周囲の知的好奇心を刺激

事例5:ペアプロならぬペアSQL

ありがたくも、周囲から質問だけでなくSQLを一緒に考えて欲しいと頼まれることも出てきました。

あるときは、ちょうど自分が最近身につけたウィンドウ関数を使った累積和や、事例4の時に身につけたwith句が使えそうだったので、説明しながら出し方を共に考え、実際に書くところまでを一緒に行いました。

人に教えることで自分の理解も深まり、普段自分が出そうとしている内容とは全く違うSQLを考えることで、分析の仕方に新たな発見もありました。

事例6:初心者向け勉強会を開催

上半期(1〜6月)まで所属していた部署は人数が多く、ディレクターも多くいました。私含め部署内にSQLが書けるディレクターがいる一方、SQLを書けないけれど興味を持っている初心者もいました。勉強会を開催することになり、私が講師役を務めました。

勉強会は、SQLとは何かをただ理解するだけではなく、「SQLを書く!」に徹した内容を伝え、実際に書けるようになることを目指した内容にしました。実践に集中できるように、SQLとは?という基本理解は、弊社エンジニア青木峰郎の著書「10年戦えるデータ分析入門」の第1〜3章にお任せし、各自で事前に読んできてもらいました。

10年戦えるデータ分析入門 SQLを武器にデータ活用時代を生き抜く (Informatics &IDEA)

10年戦えるデータ分析入門 SQLを武器にデータ活用時代を生き抜く (Informatics &IDEA)

また、勉強会当日は最初にお題を出し、以下のように「お題=出したい内容(日本語)からSQLを作る、その作る過程で基本のSQL構文を理解してもらう」方法で伝えました。 お題を分解し、どこから(from)、どんな条件の(where)、何を(select)出すのか?を考えた上でSQLを書いてもらいました。

スライド抜粋 f:id:erikwrd:20160704113640p:plain

また、私が普段実践している「出したいことがあっても出し方がわからない(何のカラムを見たら良いか、カラムに何が入るのか不明な)時は、実際に自分で操作して自分のログを抽出する」という自己解決法を伝えました。誰でも今からやれる方法ですし、エンジニアに聞く前に少し調べるだけで、解決したり、聞き方が明確になったりして有益だと思っています。

勉強会の実際のスライド(一部抜粋)は以下です。

勉強会で人に教えることで自分自身の理解が深まったり、理解しやすい伝え方を考えたりと、とてもよい機会でした。また、参加してくれたディレクターのみんながその後チーム内で継続して、SQLを習得するために1週1題を実践しています。 自分の知見を共有することで、周囲にもよい影響を与えることができ、とてもよかったです。

最後に

SQLが書けると色々な数字が出せること自体が面白くなり、データ構造がわかるとあれこれ調べたくなります。それはそれでデータ分析の入り口に立てた!という意味では良い気がしますが、どんな仕事も同じで、その仕事の目的を明確に持ち、また常にその仕事の目的が何かを意識すること、単に作業すること自体を目的にしないように心がけることが、当たり前ですが大事だと思います。

この会社で、初めてディレクターという職種で2年半働き、様々なことを周囲から学んだ中で、特にこのSQLのスキルに関しては身につけて心底よかった!と思っています。この記事が、もし今後SQLを学んでみようという方の役に少しでも立てたら幸いです。

事例6で紹介した本はとてもわかりやすいので、最初の1冊にされることをオススメします。

*1:この時はAndroidStudioを使いました

デザイナー横断組織の変遷

$
0
0

こんにちは。デザイナーの池田(id:tikeda)です。6月末までユーザーファースト推進室というデザイナーを中心としたユーザー体験について横断的に責任をもっている室の室長を勤めていました。7月からこのユーザーファースト推進室をなくし役割を各部室に分散させる体制変更を行いました。 ユーザーファースト推進室については、過去のインタビューブログのエントリーをご覧ください。

はじめに

サービス開発では、デザイナー・エンジニアといった職種毎に部を構成し各プロジェクトに派遣するようなスタイル(A)、ディレクター・デザイナー・エンジニアといった役割の異なる職種で部を構成するスタイル(B)、またこの2つを組み合わせたハイブリットのような組織が存在しており、弊社だけでなく開発の現場ではよりよい開発が行われるよう試行錯誤が行われていると感じています。

f:id:tikeda:20160715091652p:plain

クックパッドではここ数年、アプリケーションエンジニアは各事業に所属していましたが、デザイナーのほとんどはユーザーファースト推進室に所属しプロジェクトやサービス毎にジョインするスタイルをとっていました。 本エントリーは私がこのユーザーファースト推進室を通して得た主にデザイナー組織についての知見を書きたいと思います。

2013年 : デザイナーをひとつの組織に

属人性をなくし、みんなでサービスを作れるように

まず、ユーザーファースト推進室は、社内で新規事業が立ち上がっていく過程で、デザインやユーザー体験がバラバラにならず統一感をもつこと、デザイナーがいない事業でも早いスピードでサービスを立ち上げられることなどを目的として、まずデザイン部という形で立ち上がり、のちにユーザーファースト推進室となりました。

それまで、デザイナーは各部門に所属していましたが、人数不足により、デザイナーが関わっていないプロジェクトや、あるプロジェクトのデザイナーが他のプロジェクトに頼まれてデザインしているといった事が自由に起こっていて俗人化していました。これを束ねて担当をつけることにより、リソースの分配や優先順位付けていき開発に取り組めるようになったと感じています。

ひとつの組織に集めてよかったこと

1 : デザイナー人数以上の力を発揮

1つに束ねたことで、デザインのガイドライン、フレームワーク化が促進され仕組みが整い効率化されました。そして、個々のデザイナーにとっても視野が広がることでこれまで幅広い仕事の領域にトライでき成長にもつながったと思っています。コーディングが得意な人、イラストが得意な人、UI が得意な人、ディレクションできる人がコミュニケーションを高め、教えあう環境を自然に作り個々の経験を高めるということです。結果、苦手なところを補いあってトータルの完成度も高められたのではないかと感じています。

そんな過程で、それぞれがお互いの仕事に意見を言い合える、360度でレビューの仕組みなどが生まれました。全体の見通しをよくするとともに、自信がない場合でも得意な人に聞け角度を高める文化となっています。

360度レビューの事例
f:id:tikeda:20160715091815p:plain:w400

2 : デザイナー以外スタッフのデザイン意識の高まり

横断的組織になることで、開発にとどまらず会社全体での接点も増えたと感じます。その結果、デザイナー以外のスタッフがデザインへの感度が高まったとも感じています。 コーポレイトロゴ、年賀状、広告のクリエイティブ確認など範囲は広がりましたが、コンペ形式をとり社内の参加を促すこともこの感度を高める一つの事例です。

コンペの告知事例
f:id:tikeda:20160715092023p:plain:w400

そのために相談しやすいインターフェイスを作るというのも意識していたひとつです。チャットでもメールでも、廊下ですれ違ったときでも相談方法は柔軟にしつつ、それをチームに持ち帰り整理し優先順位をつけていくことに労力を割くことが社内に浸透していく秘訣とも言えます。相談しにくい環境を作り品質管理を怠るよりも、小さいものでも受け入れ、クオリティをあげるチャンスをつかむ方が大切です。もちろん、それを継続させていくためには、それに答えるクオリティが外せないためデザイナーにもプレッシャーになります。

3 : 特定事業だけなく、全体を意識しデザインできるように

各プロジェクトにユーザーファースト推進室のメンバーが加わっていることで、事業部長などの縦串のプロジェクト責任者だけでなく、ユーザーファースト推進室としての横串の責任が生まれてきます。デザイナーは他のチームメンバーと共に事業成功に貢献するのが大切でありますが、エンジニアがエンジニアリング面での仕様や設計の責任を持つのと同様に、デザインに関してもデザイナーが責任を持つべきだと思っています。そしてそこには、事業サイドだけでなくトータルでのユーザー体験やブランディングの意識が欠かせません。そのため、例えば「この色は以前別のプロジェクトでやっていたここと合わせましょう」「このデザインをこのまま進めるとあっちのユーザー体験を毀損することになりそうです」といったことが主体的に行われるようになったと思っています。

全体を意識した取り組みの事例
f:id:tikeda:20160715092114p:plain:w400

2016年 : デザイナーを各組織に分割

目的を絞り、責任範囲を明確に

横断機能が社内でうまく作用していると感じている側面についていくつか書いてきました。しかし、もちろんうまくいうことばかりではありません。前述した通り、デザイナーは様々なプロジェクトに参加できる一方で、1つのプロジェクトを継続していく力や、1つのユーザー体験に集中して考えることに対して弱さを感じるようになりました。また、プロジェクトにジョインはするもののタイミングにずれがでることでコミュニケーションコストが嵩んだり、「依頼」的な観点が抜けない、依頼者側もユーザー体験についてお任せで頼りがちになるという問題もあります。 今回、ユーザーファースト推進室をなくした理由はこれらの課題の解消。そして、デザイナー同士の距離はこのままに、エンジニアやディレクターとの距離をもっと縮めて同じ責任領域でゴールを目指すことに意識に振り切ることで新しい組織力がつくように思っています。

最後に

以上、ユーザーファースト推進室での経験を元に組織を作る側の目線とデザイナーとしての目線を織り交ぜながら書いてきました。冒頭でも書いたように今回は各事業部に責任を強め、ディレクタ・エンジニアそしてデザイナーが1つの部で仕事をするようにしました。 かといって、これまでやってきた文化やカルチャを失わせる事を目的でやっていることではありません。フェーズにあわせて横と縦のバランスをうまく組み替えていくことでさらによいサービス作りが行えるとおもっています。

開発体制は会社が何を成し遂げたいのかによって左右し、それは規模や事業内容によっても変わると思います。今回のエントリーはクックパッドが会社として変わっていく過程でのデザイン組織の変遷事例として参考にしていただければと思います。

なお、クックパッドではエンジニア・デザイナーを絶賛募集しています。一緒にサービスを作ってくれる人のご応募お待ちしています。

webpackを使った Rails上でのReact開発

$
0
0

はじめに

こんにちは、投稿開発部エンジニアの芳賀です。

既存のRailsプロジェクトの中でReact.jsを利用する機会があったので、その時にやったことについてまとめてみます。

私自身は普段RailsのサーバサイドとCoffeeScriptが中心で、最近のJavaScript開発環境についてあまりキャッチアップできていなかったのですが、それらの状況を把握しつつ試行錯誤で開発していった経験から、できるだけ「React採用してみたいけどJavaScript界隈よくわからない目線」で書いてみようと思います。

RailsでReact.jsを使ういくつかの方法

2016年時点で、RailsでReact.jsを使う方法はいくつかあって、どれを採用するかで悩みました。

  1. vendor/assets/javascripts にreact.jsを置いて利用する
  2. react-rails gem を利用する
  3. browserify-rails で npm管理して利用する
  4. railsプロジェクト内に、JavaScript開発用のディレクトリを用意して webpack + babel-loader で利用する

調査したところ、だいたいこんなパターンがあると思っていて、下に行くほどRailsよりもJavaScript開発の知識が必要になってくるイメージでした。

最終的にはwebpackを選択したのですが、それぞれ軽く振れておくと

vendor/assets にライブラリを置く

Railsで外部JSファイルを利用する場合、vendor/assetsにダウンロードしたファイルを置いて Sprocketsのマニフェストファイルで読み込んで利用するのが1番手軽だと思います。

手軽だとは思いますが、Reactを使いはじめると他のnpmモジュールもどんどん使いたくなってきて、それらを全部 vendor/assetsに入れて sprockets で読み込み順を考えながら開発していくのは、ごく簡単なReactアプリケーションでもすぐ辛くなる印象でした。もっと良い環境を作った方が最終的に楽になると思います。

react-rails

https://github.com/reactjs/react-rails

その名の通り、RailsでReactを利用するためのGemです。 Bundlerで react-railsをインストールするだけで、すぐ利用できるようなお膳立てをしてくれます。

React.jsのファイルが同梱されているのはもちろん、Rubyで設定を書けたり、Reactコンポーネントのレンダリングヘルパーが用意されていたり、coffeescriptでもes2015でもJSXでも書けるなど、JavaScriptよりもRailsやRubyに慣れている場合、かなりとっつきやすいです。

React以外のnpmライブラリは自分でなんとかする必要があります。

browserify-rails

https://github.com/browserify-rails/browserify-rails

React用というわけではありませんが、browserifyというJSビルドツールをRailsのSprocketsで利用できる browserify-rails は、JSモジュールをnpmで管理できて、baberifyを通せば、es2015やJSXの変換もできます。

react-railsのヘルパー関連が必要なければ、browserify-rails のnpmで管理したreactを利用するのも手だと思います。

ただ、開発時のビルドが結構遅くて辛くなってきたのと、既存プロジェクト固有のコードと相性がよくなかったため採用は見合わせました。

browserify-railsに関しては、弊社外村の http://techlife.cookpad.com/entry/2015/12/14/130041の記事が詳しいです。

webpack

http://webpack.github.io

webpackは依存関係のある分割されたJSやクライアントサイドのアセット群を、いい感じにまとめてくれるビルドツールです。

webpackにはLoaderという仕組みがあり、ソースコードに適用する処理を柔軟に設定できるのですが、babel-loaderを使うことでes2015やJSXで記述したJSファイルを変換することができます。

Reactに関わるモジュールバンドリング(複数ファイルの結合)、ソースコードの変換、ビルドしたコードの配置まではwebpackで行い、ファイルへのフィンガープリント付与などはこれまで通りSprocketsに任せます。

このやり方の場合、Rails開発者がある程度webpack環境について理解する必要があり、若干コストが高いような気もするのですが、今回JavaScriptのコードを触る人間が限られていたのと、JSを触らない開発者はある程度気にしないでもRailsの開発はできるような状態にしておきました。

また、私自身がReact、es2015などをはじめて使ったこともあり、問題があった時に切り分けが簡単であることが重要だったのと、開発中のビルドが速いということが決め手となり採用しました。

webpack利用時の構成

ディレクトリの構成は、以下のような感じで プロジェクトルートの clientが webpack環境となっています。

├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
├── bin
├── client
├── config
├── config.ru
├── db
├── lib
├── log
├── public
├── test
├── tmp
└── vendor

webpack環境のディレクトリは

client
├── src
├── node_modules
├── package.json
└── webpack.config.js

このような構成になります。

clientの 構成は一般的なwebpackによるreact開発とほとんど変わらないのですが、 ビルド時にファイルを配置する場所に ../app/assets/javascripts/webpackを指定しています。

client/node_modules../app/assets/javascripts/webpackはバージョン管理対象外としたいので .gitignoreに追加しておきます。

# .gitignore
/client/node_modules
/app/assets/javascripts/webpack

webpack + babelの設定

webpack環境の準備をします。 必要な作業は以下の様な感じです。

  1. package.json を作り、npmでライブラリをインストールする
  2. webpack.config.js でビルド設定を書く
  3. foreman で webpack buildプロセスを起動する

1. package.json を作り、npmでライブラリをインストールする

clientディレクトリで

$ npm init -y

を実行して、package.json を生成します。公開することはないので「private」にしておきます。

{"private": true,
  "scripts": {"test": "echo \"Error: no test specified\"&& exit 1"
  }}

次に webpack と babel関連のライブラリをインストールします。 npm install -Dは 開発時にのみ必要なライブラリをインストールしつつ、packpage.jsonに依存関係を記述してくれるオプションです。

$ npm install -D webpack babel-loader babel-preset-es2015 babel-preset-react

「babel-xxxxxx」が多くて混乱するのですが、 babel-loader はwebpackからbabelを使ってトランスパイルするためのパッケージ。 babel-preset-xxxxx は、es2015やJSXを変換するためのプリセットがbabel本体と分離しているので 個別にインストールする必要があります。

なんとなくBabelをインストールすればいい感じに全部やってくれるんでしょ?と思っていたのですがそうではありませんでした。

そして、いよいよ react をインストールします。 -Sオプションは、アプリケーションに必要なライブラリを、packpage.jsonに追加しつつインストールをします。

$ npm install -S react react-dom

2. webpack.config.js でビルド設定を書く

次に webpackのビルド設定を書いていきます。 ここでは最小限やりたいことの

  • ソースコードのエントリファイルを指定する
  • 出力先のルールを設定する
  • 出力する際に、Babelによるトランスパイルを設定する

を、記述していきます

// webpack.config.js
module.exports = {
  entry: {
    app: './src/index.js',
  },

  output: {
    path: '../app/assets/javascripts/webpack',
    filename: '[name].js',
  },

  module: {
    loaders: [
      { test: /\.(js|jsx)$/,
        loader: "babel",
        exclude: /node_modules/,
        query: {
          presets: ["es2015", "react"],
        }
      },
    ]
  },
}

これで client/src/index.jsがある状態で

$ ./node_modules/.bin/webpack -w

を実行すれば、ファイルの変更を監視して Railsの app/assets/javascripts/webpack/app.jsにビルド結果が配置されるようになります。

さらに packpage.jsonに npm scripts に開発ビルドと本番ビルドのコマンドを用意しておくと、foremanやcapistranoから実行するときに便利です。

{"private": true,
  "scripts": {"webpack-watch": "webpack -w",
    "webpack-build": "webpack -p"
  },
  "devDependencies": {"babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-react": "^6.11.1",
    "webpack": "^1.13.1"
  },
  "dependencies": {"react": "^15.2.1",
    "react-dom": "^15.2.1"
  }}

3. foreman で webpack buildプロセスを起動する

このままでは、Rails開発者もわざわざJSビルド用の別プロセスを起動しておかなければならないので foreman startで railsとwebpackのプロセスを起動するようにします。

# Procfile
rails: bundle exec rails server
webpack: npm --prefix client run webpack-watch

その他

今回は最低限の設定のみふれましたが、webpackの設定で アプリケーションコードと react などのベンダーコードを分けて あまり変更のないベンダーライブラリをキャッシュしやすくしたり

複数のアプリケーションコードに分割しておくことができます。

https://webpack.github.io/docs/code-splitting.html#split-app-and-vendor-codehttps://webpack.github.io/docs/code-splitting.html#multiple-entry-chunks

最後に

今回は RailsでReactを利用する際に、react-railsやbrowserify-railsを利用しないアプローチについて書いてみました。 誤解のないようにお伝えしておくと、弊社のすべてのプロジェクトでこのアプローチを採用しているわけではなく、 react-railsやbrowserify-railsを使っているプロジェクトもあります。

各々のチームにあった方法を検討する参考になれば幸いです。

クックパッドでは エンジニアを積極採用中です。 https://recruit.cookpad.com/

今回のサンプルをこちらにおいておきます https://github.com/func09/react-on-rails-sample

ユーザーの気持ちを考えてデザインをするために大切なこと

$
0
0

こんにちは。クックパッド ダイエットのデザイナーの新妻です。 クックパッド ダイエットでは、「正しく食べてやせる」をコンセプトに銀座と代々木にある店舗でのダイエット指導と、ダイエット情報を毎日配信するメディアサイトの運営を管理栄養士と一緒に行っており、私はデザインやコーディングを担当しています。

今回は新規コンテンツを作る中で、ユーザーの声をどのようにデザインに反映していくかというお話をさせていただきます。

どんな気持ちでダイエットラボを利用している?

クックパッド ダイエットは、ユーザーの約85%が女性で、年齢でいうとメディアの方は20代〜40代と幅広く、店舗の方は40代の方を中心にご利用いただいています。

f:id:lica19:20160728102519p:plain

サイトや店舗ユーザーの目的の多くは“ダイエット”ですが、店舗にいる管理栄養士を通して「歳を重ねるごとに痩せにくくなって困っている」「体調が良くないので改善したい」という、より具体的な悩みがきっかけでお店に訪れる方が一定数おり、これらの悩みは更年期世代に多く「やせたい」という理由だけでなく「体調不良」に悩んで不安な気持ちでダイエットラボを訪れている方がいることがわかりました。

ユーザーの不安を解決できるコンテンツってなんだろう?

「やせたい」という思いから、自己流の食べない・偏ったダイエットを行ってしまう方が多くいらっしゃいます。その結果、一時的に体重が減少しますがリバウンドの可能性も高く、何より体のバランスが崩れ更年期症状が悪化してしまうことも・・・しかし専門家の指導のもと正しい食生活を送ることで心身の不調が改善されることもあります。

そこで、私達の提案する食事改善サービスを知ってもらうきっかけとして【更年期診断】というコンテンツを作る事になりました。更年期症状に悩む方に、いくつかの質問に答えていただき、自分の現状を把握してもらったうえで、改善案の一つとして、ダイエットラボでの食事改善を提案するというコンテンツです。 クックパッド ダイエットにはダイエット診断という100万人以上が実施した大人気コンテンツがありますので、そのシステムを活用し質問内容と診断結果に関しては専門家の指導のもと医学的根拠に基づき作成しました。

各質問に入れるイラストのテイストをユーザーインタビューを元に決めることになり、「最近更年期症状を感じる」という数名のユーザーさんに直接店舗にお越しいただき、3つの質問に答えていただきました。

1.イラストのテイストについて

まず更年期診断の挿絵として良いと思うイラストと、良くないと思うイラストを理由と共に答えてもらいました。最初に仮設を立てた段階では、ターゲットとなる40代50代の女性は、人間味があり感情移入しやすいAやEのようなテイストが有力だと思っていました。逆にBやCは症状に悩んでいる人にとっては少し軽すぎるタッチでふざけたように感じられないかが懸念点でした。

f:id:lica19:20160728102543p:plain

しかし結果はBやCのようなデフォルメされたイラストが人気で「LINEのスタンプのようなイラストで見慣れているので良い」「おしゃれでかわいい」という意見があり、逆にAのようなイラストには否定的な意見が多く、「人間っぽすぎて嫌」ということでした。

仮説の段階では、感情移入のしやすさがポイントだと思っていましたが、実際に話を聞いて見ると更年期症状というつらい経験に対しての質問に答えていく中でイラストのリアルさが逆に気持ちをネガティブな方へ導いてしまうということが分かりました。

2.キャラクターの年齢について

次にイラストを描くにあたり、年齢の表現を探りました。 この4種類のイラストの中で、「自分に近い年齢のイラストはどれですか?」という質問に対し、Bに答えが集中しました。

f:id:lica19:20160728102541p:plain

理由と聞くと「本当はCかもしれないけど、加齢を認めたくない。」「Cがあなたです。と言われたらショック」「現実はDかもしれないけど、気持ち的にはBでいたい!」という実年齢よりも少し若く見えるイラストのほうが良いという意見が多かったです。

これは以前、50代男性向けの健康サービス開発時にも同じようなことがあったことを思い出しました。ストックフォトサイトで「50代、男性」で検索して出てきた画像をユーザーイメージとして提案したら、ちょうどユーザーと同じ年齢層の担当者から「この写真の人は、ちょっと老け過ぎじゃないか?もう少し若い人がいいな。」と言われたことがありました。イラストでも写真でも、実際よりすこし若く見えるほうが感情移入しやすい傾向があるようです。

3.キャラクターの表情について

最後に表情について質問しました。更年期診断は、性質上しかたないのですが体の不調に関してなどマイナスな質問が多くあります。それらの質問の挿絵として、このイラストを見てどう感じるか答えてもらいました。

f:id:lica19:20160728102532p:plain

結果は「この人は本当につらそうな表情で、私も気分が落ち込んでしまいそう」「表情が暗すぎるのでは?」という意見で、私自身は「ちょっと疲れたなぁ」というイメージでこのイラストを選んでいましたので、実際のユーザーが予想以上にマイナスイメージを受けていることに驚きました。 みなさんはどう感じるでしょうか?

ユーザーの声をデザインに反映する

今回ユーザーインタビューをしてみて、私達が思っている以上に更年期症状というのはつらく、不安な状態であるということが伝わってきました。その心理状況はイラストの見え方にも影響しており、表情やリアルさによっては、この診断を利用することでさらに気分が落ち込んでしまう可能性もあることがわかりました。

それらを踏まえ、更年期診断のイラストはなるべくユーザーが暗い気持ちにならないように、以下の様なデフォルメされ年齢がわかりにくく、色味が綺麗なものに決まりました。

f:id:lica19:20160728102534p:plain

診断全体のデザインも、クリーム、ピンク、グリーンなどを使い、フレームの角は丸みを持たせるなど優しい雰囲気に仕上げました。

f:id:lica19:20160728102530p:plain

思い込みをすてて、よりユーザーの気持ちに近づくために

サービスを作る際はユーザーの年齢や利用する環境などから、文字のサイズやボタンの位置など最適なUIを考えるのはもちろんですが、どのような心理状態で使っているのかというのも重要なポイントだと思います。

今回は「ユーザーの気持ちが落ち込まないように」という点に特に注意して制作を進めました。しかし、いくら開発者の中で想定で話し合いをしても、実際のユーザーの声を聞くまで気づけないこともあります。

まずは、小規模に身近な人へのインタビューから始めても良いかもしれません。

今回紹介した更年期診断はこちら

デザイナー募集中です!

アプリのアップデートに依存せずにアプリの画面を改善し続ける仕組み

$
0
0

検索事業部の日高(@kaa)です。
検索事業部では作りたいレシピが見つかることをひとつの目標に、レシピを探す行動を助けることに挑戦しています。 その中で、レシピ検索した際の結果画面でのコンテンツを改善していくための仕組みについて紹介します。

作りたいレシピが見つかるためへの色々なアイデア

レシピ検索結果画面でレシピを一覧で見せるだけでなく、作りたいレシピがより見つかるために考えられることはたくさんあります。

例えば

  • もし、入力ミスと思われる検索ワードだったら
    正しいと思われる検索ワードを提案、または正しいと思われるワードで検索し、元のワードも表示(Google検索のように)

  • あいまいな検索ワードだったら
    より具体的に検索できるよう検索ワードを提案。「さっぱり」だったら「冷しゃぶ」「さっぱり 麺」など。 (具体的なワードで検索するとレシピが決まりやすい傾向があります)

  • さらに小分類をもつワードなら
    「パスタ」だったら「ナポリタン」「カルボナーラ」「冷たいパスタ」といった具体的なワードを提案する。

  • 見つからない様子だったら
    もし検索結果をある程度の件数見て、まだ迷っているようなら、この食材でやってみるのはどう?といった方向性を変える提案

などなど、 それぞれのアイデアごとに、どのような検索ワードの時に、検索結果の一覧のどのあたりに出すべきかということを検討していく必要があります。 もちろん全てのアイデアに価値があるわけではありません。素早く効果を確かめ、もし駄目な場合も他の開発に影響なくクローズを行えるのが理想です。

アプリでの新機能・導線改善のよくある課題

  • アップデートの浸透具合により、コンテンツの効果判断できる時期が伸びがち(改善サイクルが遅い、段階リリースを利用するとさらに伸びる)
  • A/BテストのコンテンツのためのAPIなど、アプリ・サーバーに限らず一時的な実装が増えてしまう。
  • コンテンツ変更にアップデートが必要になってしまうので、スマホサイトで価値検証後、アプリに導入になってしまう。
  • Webで効果あった施策がアプリでも効果があるとは限らない。再度確かめる必要がなる

よくあるアプリでのA/Bテストとその課題

アプリで機能の価値評価を行なう際、A/Bテストを行なうことがあります。
基本的な流れとしては起動時などに設定用APIからフラグなどのデータを取得し、機能のONOFF、UIの切替を行ない数値検証をしていきます。 Google公式のFirebaseでもRemote configという機能が提供されるのでこれからさらに行われるようになると思います。

この場合も弱点として、フラグを処理する実装をしたバージョンからしか有効でない、機能自体はアプリに実装されていないといけないのでテストできるバリエーションが限られる、A/Bテストの結果を見てさらにテストを行ないたい場合、また次回アップデート待ちになりがち。
AパターンとBパターン、さらに手を加えたA'パターンまでアプリに実装されていないことにはWebと同じスピードで改善を続けていくことはできません。

また、クックパッドのandroidアプリでは新バージョンでの新たなクラッシュの検出のため段階的にリリースされていくようにしています。段階リリース中にクラッシュが検出された場合、その修正対応などによりアプリの全体公開の時期がずれこむこともあります。
段階リリース自体は素晴らしいのですがアップデートによる価値検証という目的ではサイクルが遅くなる要因となります。

アプリでの価値検証のスピードを上げるために

1.表示するコンテンツをAPIで指定できるようにする

検証したい価値ごとにアプリを実装していたのでは、アプリのアップデートに縛られてしまいます。 これには事前に考えられるコンテンツのデザインフォーマットを事前に用意しておくことで対応します。

コンテンツの表示方法がその機能固有になると、その実装したバージョンからしか検証ができません。
それではアイデアをすぐに試せないため今回はアプリのデザインルールに沿ったコンテンツのフォーマットを10パターン程度想定しました。
横スクロールのカルーセル型か、バナーか、一覧形式か、またはタイトル・サブタイトルになる要素、「もっと見る」的要素など各フォーマット共通に使われそうなものを整理。 画像があるのかないのか、あるなら画像サイズの自由度は、キャプション、バッジ的要素はあるのかないのか。
各コンテンツフォーマットのデザインに対してどんな利用シーンが起こりえるか想像力を働かせます。既存のデザインを見返し、どの要素が必須で、必須ではないcaptionなどの要素が空の場合でもデザインが破綻しないよう注意します。

今回は以下のようなテンプレートを用意しました。
上部には共通でlabel要素、アイコン、more(もっと見る等)の要素とその下に各フォーマットの情報。
これで全てではありませんが一例として。 f:id:futura24pt:20160729124517p:plain

各要素の名前も出来る限り汎用的に考えます、label,lead,caption,thumbなど。ここでもしrecipe_titleといった意味を持った名前を採用してしまうとそのフォーマットを別の用途で利用しようとした際に混乱を生んでしまいます。
ただ汎用性を高めすぎると使いにくかったり複雑な構成になりやすいため、今回はレシピを表示するrecipe_list,recipe_singleは別に用意しました。

価値評価後、最終的にはさらに適していると考えられるそのコンテンツの専用デザインを作成する可能性はありますが、評価段階では汎用的なもので進めます。
これらがアプリに実装された状態が出来てしまえば、APIの変更のみでコンテンツを入れ替え価値検証していくことが可能になります。

2.APIを検証したい機能ごとに用意しない

APIの種類が増えると表示速度の低下、通信エラーリスクの増加、コードの複雑化といった問題が起きますし、APIを叩く実装をしたバージョンでの動作に限定されます。 それでは価値検証したいコンテンツの評価に次バージョンを待たなくてはいけません。
また後々使わない可能性がある、しかし特定のバージョンでのみ利用しているAPIといったものができてしまうとサーバーサイドの実装もいつまでも残す必要ができてしまい、負債になってします。そのバージョンを利用するユーザーはアプリアップデート後も残ることになりますので。

運用していくために

仕様をドキュメント化する

あるデザインフォーマットのコンテンツを追加したい場合、コンテンツの各要素の指定方法がわからないといけません。 各要素の必須、任意なのかもまとめておきます。
この仕様さえ共有できてしまえば、価値検証時にアプリエンジニアの稼働がゼロで、アプリリリース時期に依存せず新たなコンテンツを提供していくことが可能です。もちろんバッティングしないよういまどういったコンテンツをレシピ検索結果に表示しているかの共有は大事ですが。
サーバーサイドとしてはどのようなJSONを出力すると望んだ表示ができるのか把握できることが望ましいので、サンプルのJSONと画面キャプチャも用意しておきます。

各デザインフォーマットをプレビューできるようにする

全デザインフォーマットが実運用で常に使われるわけではありません。 どのような表示が可能なのか、確認できるようになっていないと困ります。
クックパッドでは開発版ビルドでのみ各デザインフォーマットの確認ができるダミー画面を実装し、どのような表示が可能なのか見当できる画面を用意してあります。

各デザインフォーマットの対応バージョンを明記する

いくらデザインフォーマットを色んな想定したとしても、追加したいデザインはでてきます。
また、特定の機能のためのデザインを追加したくなることもあります。 もちろんそのデザインが実装されたバージョン以降からしか利用できませんので、どのフォーマットがどのバージョンから利用可能なのかまとめておきます。 これにより新しいコンテンツを配信した際、どの程度の数のユーザーがそのコンテンツを利用できることになるのか判断できます。
APIを一本化したことにより、もし古いバージョンのアプリを利用していて、最新版のアプリでしか対応していないコンテンツを受け取ってしまうとどうするか、という問題がありますがアプリが知らないコンテンツフォーマットのデータに関しては無視する、表示しないといった方針にしています。

まとめ

このようにアプリでの表示とAPIのルールを定義することで、以前はスマホサイトで価値検証後にそれぞれAPI開発し、アプリに実装し次回アップデート後に反映、といった進め方から最初からアプリで価値を検証し、改善していく進め方ができるようになりました。

クックパッドでは職種問わずサービス改善に取り組み続けることに興味のある方を募集しています。もしすぐに採用でなくても、アプリでのサービス改善について興味ある方がいらっしゃいましたら@kaaまでお気軽にどうぞ。

Viewing all 726 articles
Browse latest View live