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

こんにちは。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

過去の開催情報

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

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

本日は開発基盤グループが社内の各サービスに提供している共通基盤サービスの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つの方法に縛られることなく柔軟に対応できる点
  • 完璧を目指すのではなくエラーが起きる前提で、通常のハンドリングから漏れてどうしても整合性を維持できなかったものが発生した場合は、改善のフィードバックをすぐに行うようにすることで外的要因の変化に対して柔軟に対応できる点

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

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

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

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

クックパッドでは昨今の多くの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) # 時間になると現れるコンテンツ

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

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

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

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

def enabled?(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) do
    TimePeriodPolicy.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')) }
  end
end

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

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

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

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

というアプローチです。

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

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

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

class ApplicationController < ActionController::Base
  include Trice::ControllerMethods
end
class SpecialEventsController < Applicationcontroller
  def show
    ...
    if @event.policy.enabled?(requested_at)
      do_something
    else
      head :not_found
    end
  end
end

モデルの処理でこの時刻を使うには、コントローラからその時刻を渡してあげます。 なぜなら、現在を知るのは「外部からの入力を適切にモデルに渡す」というコントローラの責務の範囲であり、モデル側はその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も示しますので、よかったらこちらも合わせてどうぞ。