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

iOSアプリデザインリニューアルの舞台裏の舞台裏

$
0
0

技術部の松尾(@Kazu_cocoa)です。

iOSアプリデザインリニューアルの舞台裏でも書かれていた、"修正期間中は毎日夜間にアプリケーションの全画面のスクリーンショットを記録するスクリプトを実行し、画面崩れが起きてないか、新デザイン未反映の画面はないか、進捗状況の確認に利用していました。"の舞台裏を少し書いてみようと思います。

はじめに

モバイルアプリケーションのテスト環境はまだまだ成長中で、様々なツールが飛び交っていることかと思います。ここでは、E2Eテストに対しての話題に絞り、使っているツール、シナリオの書き方、クックパッドでは、という話しをします。この記事におけるE2Eテストは、UIからの操作によりユーザの操作を模倣して実施するテスト、という意味合いです。

ツール

E2Eテストを自動化する為のツールの選定には以下を気にしていました。

  • OSの更新に追従できそうなもの
  • 特別なテスト用ビルドが必須ではない
  • UI越しの操作をシナリオとして記述、実施できる
  • (できれば)Ruby, Groovy, Pythonなどで記述したい

それらの点から、いくつかツールを検討し、結果、Appiumを選択しました。私がAppiumの状況を追い始めてもうそろそろ1年が経過しようとしている時期ですが、バージョンも1.0を超え、現在も活発に開発が続いているOSSです。

このブログを書いているときは、Appiumは1.2.2が最新です。

Appiumとは

Appiumに関する踏み込んだ話しは本家を参考にしてください。以下では、今回説明に使うAppiumのiOS向けな技術的概要を記載します。

全体のアーキテクチャは以下の図の通りになります。

http://www.3pillarglobal.com/insights/appium-a-cross-browser-mobile-automation-toolより

要素ツール

  • WebDriver
  • instrumentsUI Automation
    • Appleが提供する解析ツールなどのツール群の総称です。
    • UI AutomationはInstrumentsの一部で、特に外部からJavaSctiptで記述されたスクリプトによりUIの操作を可能とする機構です。
    • Appiumは、UIに対する操作はiOSが提供するUI Automationの機構を利用しています。

Appiumのシナリオを記述するために、既にいくつか公式にライブラリが提供されています。RubyやPython、Javaなんかのライブラリも提供されています。

Appiumを使ってアプリを起動する

簡単に、実際にAppiumを使いiOSアプリをシミュレータを用いてインストール・起動する所までの流れを記載します。Appiumを使うまでに、実際にInstrumentsを使ってみることもしてみます。

テストする成果物について

iOSのアプリは、.appのアプリとして生成されたものをアーカイブして.ipaファイルを作ります。iOSシミュレータでは、この.appのファイルをインストールし、起動、操作することになります。xcodebuildコマンドをインストールしていると、以下の通りのコマンドを使うことで、OUTPUT_PATHで指定したパスに.appのアプリを出力することができます。xcodebuildに関しては、こちらのURLを参照ください。

$ xcodebuild \
  -workspace "WORKSPACE" \
  -scheme "SCHEME" \
  -sdk "iphonesimulator8.0" \
  -destination "platform=iOS Simulator,name=iPhone 5s,OS=8.0" \
  -configuration "CONFIGURATION" \
  SYMROOT="$OUTPUT_PATH" \
  DSTROOT="$OUTPUT_PATH" \
  clean build

ここで、OUTPUT_PATH以下に出力された.appファイルを cookpad.appとして、以下の話しを進めていきます。

Instrumentsを使う

Appiumでアプリを起動するまえに、基本としてinstrumentsを使いシミュレータへcookpad.appをインストールするところを模倣してみます。Xcodeのコマンドラインツールがインストールされていれば、通常はinstrumentsコマンドを利用可能です。以下のようにコマンドを直接入力することで、シミュレータを起動してcookpad.appをインストールすることが可能です。

$ instruments -t "/Applications/Xcode.app/Contents/Applications/Instruments.app/Contents/PlugIns/AutomationInstrument.bundle/Contents/Resources/Automation.tracetemplate" cookpad.app

OSにバンドルされたinstumentsを使うと上記のように操作可能なのですが、実際はAppiumはカスタムしたinstrumentsを経由してアプリのインストール、UI Automationへ命令を出してアプリを操作します。

Appiumを経由してアプリを起動する

Appiumのインストールはnpmを使います。npmの実行環境が入っていれば、以下の手順でAppiumをインストール、実行することが可能です。

$ npm install -g appium
$ appium

デスクトップ版も提供されているようなので、そちらを使うのも良いと思います。その場合、npmコマンドをインストールするなどは不要なので、軽く使ってみる、という話しでは十分かもしれません。

Appiumサーバを起動したら、以下のようにcapabilityを指定し、アプリを起動します。ここでは、サンプルとしてruby gemのappium_libを使った場合を記載します。Appiumサーバも以下のスクリプトも、同一の端末内にて実行する環境を想定しています。

require'appium_lib'IOS_CAPABILITIES = {
  automationName: 'appium',
  platformName: 'iOS',
  platformVersion: '7.0',
  deviceName: 'iPhone Retina (4-inch)',
  app: "cookpad.appへの絶対パス"
}.freeze

client = Selenium::WebDriver::Remote::Http::Default.new
client.timeout = 120

driver ||= Selenium::WebDriver.for(:remote,
                                   http_client: client,
                                   desired_capabilities: IOS_CAPABILITIES,
                                   url:'http://localhost:4723/wd/hub')
driver.manage.timeouts.implicit_wait = 30# seconds
driver

※Appium 1.2.2時点では、Xcode6向けのinstrumentsには対応していないので、Xcode5をxcode-selectで指定しておく必要があります。 ※メソッドの使い方は、appium_libを完全に使っているわけでなありません。

ここまででアプリが起動するので、以降ではdriverに対して様々なメソッドをよび、シナリオを実施していきます。Appium自身が提供するチュートリアルも充実してきていますので、使い方の詳しくはそちらをご覧ください。

シナリオを書く

皆さんはシナリオを何で書きますか? 私は、今はTurnipを使いシナリオを記述しています。Cucumberの派生と捉えて頂ければ想像しやすいでしょうか。

例えば、シナリオを記述する.featureファイルには、以下のような自然言語でシナリオを記述します。

# encoding: utf-8# language: ja機能: 0000. ユーザはログインすることができる
  シナリオアウトライン: 0000.1ユーザはそれぞれ異なるアカウントでログインすることができる
    前提 <device> で試験を行う
    * <user_status> ユーザでログインする
    * 画面に <expected> が表示されている
    * スクリーンショットを <screenshot> という名前で撮る

  例:
    | device   | user_status |  expected    | screenshot |
    | 'iphone' | 'ゲスト'     | '期待する文字' |    '1'     |
    | 'iphone' | '無料'       | '期待する文字' |    '2'     |
    | 'iphone' | '有料'       | '期待する文字' |    '3'     |

それぞれのステップに対応した処理は別ファイルで実際の処理と関連づけます。この関連づけるためのファイルに、これらの操作をAppiumを経由して動作させるためのfind_elementsなどのメソッドを使ったスクリプトを記述します。ここでは特にTurnipの記述に関しては言及しません。

クックパッドにおける使い方

上記までのAppiumの使い方とシナリオの書き方を増やしていき、実際にどのようにしてそれらを使っているのかを記載します。

テストも開発プロセスの一部として実施する

言わずもがなですが、テストも開発プロセスの一部です。なので、何か修正を加えたら組み込まれたテストを常に実施し、健全な状態に保てるようにしたいです。一方、今回記載しているE2Eテストは、ユニットテストに比べて遥かに時間がかかり修正の度に実施、ということは難しいです。

現在のクックパッドのリリース間隔では、この自動テストを毎晩回すという進め方で十分なので、今は帰宅するときに実行して帰ります。リリース前にしか実行しないのではなく、頻繁に実行し、なるべく開発プロセスの一部になるように向けています。

機械が実行可能なタスクは機械に任せる

このようなテストを自動化する目的は、機械的に実施可能な箇所は機械に任せ、人はもっと振る舞いを観察しながらテストするところに力を注ぐことができるようにすることにあるかと思います。少ない人数で開発を回さないといけない開発現場は多いと思います。ならば、機械的に実施できる領域であれば機械に任せ、人がもっと別のことに時間を使えるようにする、というのは自然な流れですね。

人が忘れがちなシナリオは機械に任せる

例えば、検索結果がゼロになるときや、いくつかの状態がくみ合わさったときのみ再現する境界値のような状態など、人が実施すると忘れがちな箇所は機械に任せます。こうすることで、人の忘れによるテストケースの漏れを防ぐこともできますし、新規機能追加時のレイアウト崩れで不具合に気づくきっかけを得ることができます。

メンテナンス性

このようなテストケースはメンテナンスコストが膨れると言われます。私は、開発の活発さに合わせて適度にシナリオの統合やAppiumの変更にもある程度対応できるように薄いラッパー層をもうけるなどして最小限のメンテナンスで済むように調整しています。ここ数ヶ月の間、Appiumの更新にも、シナリオの調整にも片手間で追従できているので、今のところメンテナンスがかかりすぎるから使えない、という判断まではいっていないように思えます。

現在の実施状況

現在、iOSのクックパッド本体アプリに対してCheckingの文脈で定期的にこのE2Eテストを実施しています。IDを持つ無料ユーザ/有料ユーザ/IDを持たないユーザなどの組み合せによるシナリオの実施を異なるテストケースとした場合、テストケースは合計で100近くあります。これらをiPhone 4-inch/ iPhone 3.5-inch/ iPad向けに実施するので、パターン数では300を超える程度のシナリオセットを定期的に回していることになります。

iOSアプリデザインリニューアルの舞台裏で言及されていたスクリーンショットの話しでは、400枚近いスクリーンショットを毎晩取得し、翌朝、出社してから業務に入る慣らしとしてざっとスクリーンショットを確認していました。UIを変更する、という特性から、スクリーンショットを使い確認する、というシナリオ自体は非常に相性の良い流れでした。

今のところ、実施時間はすべてで2時間くらいです。夜間に回すにはまだ大丈夫そうです。特定のシナリオセットを実施する、ということも可能なので、これ以上が今は頑張っていません。

自動化テストのピラミッドを参考にするとほめられた構成ではないかもしれないですが、model以外のユニットテストが難しいモバイルアプリの現在のテスト自動化の環境からすると、現実的な姿ではないかと思います。

ROI?

私たちのリリース周期はだいたい約2週間に1回です。その期間の中では、1つ以上の何らかの不具合への気づきを与えてくれています。また、先で述べたシナリオすべてを人が実施すると、順調にいったと見積もっても1人日はかかると思います。そのように考えると、費用対効果、費用対便益という点では十分な価値を出していると思います。なお、テストスクリプトの作成自体、すべて合わせても恐らく1日かかってません。

また、ROI以上に、単純作業を機械に任せることで得られた時間やCheckingの時間短縮は非常に良いものでした。

最後に

テスト自動化に限った話しではないですが、様々な取り組みは自分たちの開発をよりスムーズにし、自分たちがやりたいことを実現するための手段だと思います。開発のスケールに対して検証のスケールはやはり現段階では難しいことが多いと思います。一方、自分たちに見合ったツールを組み合わせたりすると思いのほか現実的な形でスケールすることも可能かと思います。

いずれにせよ、自分たちに見合った開発/検証体制・プロセスを成長させ、やりたいことができる環境に一歩でも近づけると良いですね。

Android...

今回はiOSに関して記載しました。私たちはAndroidに関してもいくつかテスト自動化を実施しはじめています。 Appiumもuiautomator越しに特定のAndroidバージョンからサポートしていますが、その安定性はまだまだなので、adbコマンドを組み合わせるなどをしていくつか機械的に検出可能な障害性の高い不具合の検出をできるようにしています。そこらへんの取り組みの話しもどこかのタイミングでできれば幸いです。


Lunch.cookpad vol.2のお知らせ

$
0
0

こんにちは。クックパッド・イベント事務局です。 前回多くの方からご応募頂いたエンジニア同士がオフラインでカジュアルにお話するLunch.cookpadですが、早速第2回を10/14(火) に開催いたします!
「クックパッドのインフラに興味があるけど、話を聞く機会がない・・」という声を学生の方から多くいただくため、今回はクックパッドを支えるインフラストラクチャー部の部長に登場してもらいます! ※今回は学生の方限定です

Lunch.cookpadとは?
エンジニアとして活躍する&これから活躍していきたいと思う皆さんにとって有意義な情報交換の場となる Lunch.cookpadは、毎回1時間で、開催日は都度決定します。クックパッドから参加するエンジニアは1名で、毎回2〜3名の方をお招きしてご希望のテーマに沿った話をする予定です。

【第2回 Lunch.cookpad /開催概要】
日時:10/14 (火) 12:00- 場所:クックパッドオフィス(恵比寿ガーデンプレイスタワー12階)
参加するエンジニア:成田一生(@mirakui)愛知県出身。2010年にクックパッドに入社し、画像配信システムの開発やRailsアプリケーションのパフォーマンス改善担当を経て、現在はインフラストラクチャー部の部長を務める。好きな動物はパンダ。

f:id:cookpadtech:20140916183956p:plain

応募方法:エントリーはこちら
応募資格:学生の方限定
応募締め切り:10/8 (水) 18:00 応募者多数の場合はこちらで選ばせていただいた上、当落を10日 (金)までにメールで連絡いたします。
【成田から一言】
エンジニアとして働きたい学生のみなさん、なんでも質問にお答えしますので、ぜひお話しましょう!

クックパッドAndroidアプリにおける最近のDB運用事情

$
0
0

モバイルファースト室の @rejasupotaroです。

Androidフレームワークには端末内にSQLiteでデータを保存するしくみがありますが、みなさんはどのようにしてますか? クックパッドのAndroidアプリでは、ActiveAndroidを使ってDBにデータを保存しています。

ActiveAndroidとは

ActiveAndroidとは、Active Recordパターンを採用したAndroidのORMです。

テーブルのCREATEを行うときに、SQLiteOpenHeleperを継承したクラスでonCreateをOverrideしてdb.execQueryでCREATEクエリを実行…としなくても、ActiveAndroidを使えば、

publicclass MyApplication extends Application {
    @Overridepublicvoid onCreate() {
        super.onCreate();
        ActiveAndroid.initialize(this);
    }
}

このようにアプリの起動時に一行、initializeメソッドを呼ぶだけで定義されているモデルの構造を読み取ってテーブルが作成されます。

モデルの定義は、Modelクラスを継承してアノテーションで設定をしていきます。

@Table(name = "recipes")
publicclass Recipe extends Model {
    @Column(name = "recipe_id")
    privateint recipeId;

    @Column(name = "category")
    private Category category;

        public Recipe(){
                super();
        }
        public Recipe(int recipeId, Category category){
                super();
                this.recipeId = recipeId;
                this.category = category;
        }
}

NOT NULL制約やUNIQUE制約に違反したときの振る舞いもアノテーションで設定することもできます。

モデルのDBへの保存と削除は Model.saveModel.deleteで行うことができて、問い合わせはクエリビルダーを使うか Model.queryで行うことができます。

マイグレーションをするときは assets/migrationsディレクトリの中に 3.sqlのようにDBのバージョンを記入したファイルにクエリを書いておくと、バージョンアップ時に差分のクエリを実行することができます。

ActiveAndroidを使うことでDBの管理を楽にしたり、抽象化された表現でSQLiteにアクセスできるので、アプリの開発効率が上がりました。一方で、しばらく運用してみて気を付けるべきことや課題も見えてきました。

モデルをParcelableにすることができない

ActiveAndroidで扱うモデルは、Modelクラスを継承する必要がありますが、Modelクラスはidをprivateなフィールドに持っているのでParcelableにすることができません。 そのため、画面間でモデルをやりとりするにはidを渡してDBに問い合わせるか、別の方法でシリアライズして渡す必要があります。

マイグレーションに気を付ける

ロールバックについて

クックパッドアプリではリリースをする前に、深刻な障害が起こったときすぐに安定したバージョンに差し替えられるように、ロールバック用のapkを用意しています。ロールバック用のapkといっても、安定したバージョンからバージョンコードを上げたものですが、最新のapkでDBのバージョンを上げていた場合は、ロールバック用のapkのDBのバージョンをその一つ上にしておかないとクラッシュしてしまいます。

テーブルのスキーマ変更について

SQLiteの制限で、テーブル作成後はカラムの追加はできますが、カラムの変更や削除はできません。そのため、始めからできないものとしてカラムの変更や削除は避けるべきですが、どうしても行う必要がある場合もあります。

SQLiteOpenHelperを直接使っている場合は、onVersionChangedが呼ばれたときにデータを一度メモリに退避させて、テーブルをDROP AND CREATEして戻すということもできます。

しかし、ActiveAndroidを使っていた場合はマイグレーションファイルに書かれているSQLの実行しかできません。データを削除しないでカラムの変更をしたいというときには、ActiveAndroidの初期化の前にSQLiteOpenHelperでDBに接続してデータを取得して、ActiveAndroidの初期化を実行して(ここでマイグレーションが走る)、そのあとにデータを保存し直すなどの方法が考えられます。 このようにアドホックな解決方法になりがちなので、やはりカラムの変更はできないものとして設計するのが良いです。

マイグレーションファイルについて

マイグレーションは、デフォルトだと一行でコメントなしの単純なクエリしか実行することができませんでした。 あるバージョンからはSqlParserが実装されて、コメントや複数行に渡るクエリが解釈できるようになりましたが、デフォルトで使用されるSqlParserは SQL_PARSER_LEGACYになっているため、明示的にConfigurationに SQL_PARSER_DELIMITEDを指定しないといけないので、注意が必要です。

ActiveAndroidは初期化に時間がかかる

クラスのスキャンは遅い

機能追加をするたびに Application.onCreateが肥大化して、初期化のパフォーマンスが低下していくということはよくあります。 クックパッドアプリで初期化処理を見直していたところ、DBの初期化に時間がかかっているということがわかり、ActiveAndroidの初期化のボトルネックがどこにあるか調べるためにTraceViewで見てみました。

f:id:rejasupotaro:20140917110704p:plain

この結果から ModelInfo.scanForModelというメソッドが全体の80%の時間を使っているということが分かります。 ソースを読んでみると、テーブルを作るためにDexFileをフルスキャンしてModelクラスのサブクラスを探索していました。

ドキュメントにはActiveAndroidの初期化はContextを渡すと書いてありますが、

publicclass MyApplication extends Application {
    @Overridepublicvoid onCreate() {
        super.onCreate();
        ActiveAndroid.initialize(this);
    }
}

Configurationを渡すことも可能です。明示的に設定値を渡すことで、フルスキャンしたりマニフェストにメタデータを読みにいくような重い処理をスキップできます。 また、SqlParserの設定もここで渡します。

publicclass DbConfiguration {
    ...publicstaticvoid initialize(Context context) {
        Configuration configuration = new Configuration.Builder(context)
                .setDatabaseName(DATABASE_NAME)
                .setDatabaseVersion(DATABASE_VERSION)
                .setTypeSerializers(getTypeSerializersAsArray())
                .setModelClasses(getModelsAsArray())
                .setSqlParser(Configuration.SQL_PARSER_DELIMITED)
                .create();
        ActiveAndroid.initialize(configuration);

パッケージ内の探索はアプリの規模が大きくなればなるほど時間がかかります。 クックパッドアプリはクラス数が多いため探索に時間がかかっていましたが、モデルを明示的に指定することで1000ミリ秒以上速くなりました。

クラスの定義漏れを検出する

ActiveAndroidの初期化時のパラメータを明示的に指定することで初期化時間を短くすることができましたが、人間がモデル定義を指定すると新しくモデルを追加したときに漏れが発生して、定義されていないテーブルからデータを取得しようとしてクラッシュする、ということが考えられます。 モデルを追加するときは注意してレビューする、のような運用でカバーする方法もありますが、開発するときに気を付けないといけないことを増やすのはあまり良くありません。

定義漏れをなくすために、ビルド時にモデルの定義クラスを生成する方法と、テストでモデルの定義漏れを検出する方法を検討しましたが、ビルド時間に影響を与えないために、テストでモデルの定義漏れを検出できるようにしました。

publicclass DbConfiguration {
    ...publicstatic List<Class<? extends Model>> MODELS = new ArrayList<Class<? extends Model>>() {{
        add(Recipe);
        add(SearchHistory.class);
        add(VisitedHistory.class);
        ...
    }};

このように、Modelのサブクラスをリストで定義して、テストでtargetContextのDexFileをフルスキャンして、クラスの定義を比較して、定義に漏れがあった場合はテストがfailするようにしました。

今後の課題

クエリのインタフェースを改善したい、ネットワークの先にあるデータとローカルのデータを透過的に扱えるようにしたい、などの要求も出てきましたが、ActiveAndroidの開発は現在あまり活発でないので、新しいアプリを作るときにはどうしようかなと思っているところです。

Androidアプリのデータをうまく扱えているという皆様、情報をお待ちしております。

iOSシミュレータをカスタマイズして、オリジナルの機能を追加しよう

$
0
0

モバイルファースト室でiOSアプリケーションの開発を行っている@yuseinishiyamaです。

クックパッドでは日々の業務を効率よく行うためのツールを作り、公開するということが積極的に行われています。 社内のリポジトリや掲示板を探せば、便利なツールをたくさん見つけることができるような環境です。

こうした文化のお陰で、作業時間の短縮、自動化が容易となり、結果として「ユーザーの方々に価値を届ける」という本質的な作業に費やす時間を増やすことができます。

私も先日、iOSシミュレータをカスタマイズして作業効率を上げる機能を実装してみたので、その方法を紹介いたします。


動作環境

以下の環境で動作確認済みです。他の多くの環境でも動くと思われますが、保証できません。

  • OSX 10.9.4 + Xcode5
  • OSX 10.9.4 + Xcode6

Loadable Bundleについて

iOSシミュレータを拡張するために、Loadable Bundleを作成します。Loadable Bundleは通常のアプリケーションと同じ構造を持っています。つまり、実行可能なファイルやリソースファイルを含むディレクトリです。しかし、通常のアプリケーションとは異なり、main関数を持っていません。Loadable Bundleのロードは、それを利用するアプリケーション側の責任となっています。このLoadable Bundleには以下のような用途があります。

  • コンポーネントの遅延ロードを行う。使用されていないコードがメモリ上に展開されないので、メモリの節約になります。

  • 一部のソースコードを個別にコンパイル可能なコードに分ける。

  • アプリケーションにプラグイン機能を設ける。

詳しくは下記のリンクを参照してください。

EasySIMBLのインストール

EasySIMBLを利用すれば、既存のアプリケーションの実行時に、Loadable Bundleをロードすることができます。 自前のクラスがロードされれば、+ (void)loadなどをエントリーポイントとして、既存のアプリケーションを拡張することができます。 インストール方法は

https://github.com/norio-nomura/EasySIMBL

How to installの項を参考にしてください。

バンドルファイルの作成準備

  • XcodeのメニューからFile->New->Projectを選択し、OSXのカテゴリにあるFramework & Library内のBundleを選択します。

20140910214630

  • 任意の情報を入力してください。Bundle Extensionを変更する必要はありません。

20140910214631

  • プロジェクトが作成されたら、ターゲットのInfo.plistを編集します。

20140910214632

  • Info.plistSIMBLTargetApplicationsというキーを追加します。それぞれ、拡張したいアプリケーションのバンドルID、拡張が実行可能なバージョンの最大値、最小値を記載します。

20140910214633

もし、特定のバージョンでしか拡張したくないという場合は、適宜バージョンの値を修正してください。

事前準備は以上で終了です。ただし、この状態のバンドルを組み込んでも何も起こらないので試しにログを出力できるようにしてみましょう。

ログを出力する

  • 自前のクラスを作成し、ロード時にログを出力するように実装します。

EXSSimpleLog.h

#import <Foundation/Foundation.h>

@interface EXSSimpleLog : NSObject

@end

EXSSimpleLog.m

#import "EXSSimpleLog.h"

@implementation EXSSimpleLog

+ (void)load
{
    NSLog(@">>>>> Injection Succeed!");
}

@end

  • ビルドし、SIMBLのプラグインディレクトリに配置します。

ビルドすると、

~/Library/Developer/Xcode/DerivedData/(アプリ名+ハッシュ)/Build/Products/Debug

にビルドされたバンドルファイルが格納されるので、それを

~/Library/Application Support/SIMBL/Plugins

内に移動させます。

  • iOSシミュレータを起動し、ログを確認します(起動中の場合は再起動してください)。

20140910214634

これで、自前のクラスがロードされたことが確認できました。しかし、これだけではつまらないですね。

【注意】不具合が出た場合

拡張内容に不備があったりすると、当然アプリケーションがクラッシュしてしまいます。その場合は、EasySIMBLのアプリケーションを開いて作成したアプリケーション名、もしくは、Use SIMBLのところにあるチェックボックスをオフにしてください。

20140910214635

メニューへのアイテム追加

先ほど作成したクラスのロード時にメニューバーのアイテムを追加するコードを実行します。 <Cocoa/Cocoa.h>をインポートするのを忘れないようにしてください。

EXSSimpleLog.m

#import "EXSSimpleLog.h"#import <Cocoa/Cocoa.h>@implementation EXSSimpleLog

+ (void)load
{
    static EXSSimpleLog *esl;
    esl = [[EXSSimpleLog alloc] init];
    [esl installMenuItem];
}

- (void)installMenuItem
{
    NSMenu *appMenu = [[[[NSApplication sharedApplication] mainMenu] itemAtIndex:1] submenu];
    NSMenuItem *appMenuItem = [[NSMenuItem alloc] init];
    appMenuItem.title = @"My Own Extension";
    appMenuItem.target = self;
    appMenuItem.action = @selector(action:);
    [appMenuItem setKeyEquivalentModifierMask: NSShiftKeyMask | NSCommandKeyMask];
    appMenuItem.keyEquivalent = @"X";
    [appMenu addItem:appMenuItem];
}

- (IBAction)action:(id)sender
{
    NSLog(@">>>>> Injection Succeed!");
}

@end

この状態で、再度ビルドしたバンドルをプラグインのディレクトリに追加し、シミュレータを再起動してみましょう。すると、下記のようになります。

20140910214636

メニューが追加されていることが確認できます。この状態で、メニューを選択したり、ショートカットコマンドを入力したりするとログが出力されるはずです。

さらなる拡張、しかし...

さて、ここまでできれば、拡張したクラス内でシェルコマンドを実行したりするなど色々な活用方法が見えてきます。しかし、これだけでは不十分です。 場合によっては、iOSシミュレータ既存のコードを呼び出したり、iOSシミュレータのコードを書き換えたりする必要がでてくるかもしれません。 詳細は割愛しますが、Objective-Cでは実行時に既存のメソッドを別のものに置き換えたりすることは容易に実現できます。しかし、どのメソッドを置き換えれば良いのでしょうか? 置き換える対象のメソッドシグネチャが分からなければ、どうすることもできません...

class-dumpのインストール

そこで、バイナリファイルからiOSシミュレータのクラス情報をダンプしてみましょう。class-dumpを使用します。 class-dumpMach-O形式のバイナリファイル内に格納されている実行時情報を出力するためのツールです。標準で提供されているotoolでも同様のことが可能ですが、class-dumpではObjective-Cスタイルの宣言方法に置き換えて出力してくれるため、簡単に読むことができます。 class-dumpのインストール方法は色々ありますが、下記のものが簡単です。

ダウンロードできたら、下記のコマンドを実行してください。

class-dump  /Applications/Xcode.app/Contents/Developer/Applications/iOS\ Simulator.app/Contents/MacOS/iOS\ Simulator

大量の変数名やメソッドプロトタイプが出力されたはずです。そこから、拡張に利用できそうな変数やメソッドを推測することができます。iOSシミュレータ既存の機能でさえも、自分の拡張から呼び出したり、挙動を変更したりすることが可能になります。


終わりに

いかがでしたでしょうか?アイデアさえあれば比較的簡単に既存のアプリケーションを拡張できることがお分かりいただけたかと思います。この記事を参考にして、皆様もご自身で便利な機能を追加して開発効率をあげていっていただければ幸いです。

勝手自作OSSライブラリを会社のアプリに組み込んだ話

$
0
0

こんにちは、レシピ投稿推進室の小室(id:hogelog)です。

今回は私が実装したAndroid向けグラフ描画ライブラリline-chart-viewの紹介、ではなくついカッとなってOSSとしてこのライブラリを作りクックパッド (Androidアプリ)に組み込んだ話について書きます。

line-chart-view

最低限の紹介をするとline-chart-viewというのは

        List<LineChartView.Point> points = new ArrayList<>();
        points.add(new LineChartView.Point(date("2014/07/01"), 100));
        points.add(new LineChartView.Point(date("2014/07/02"), 200));
        points.add(new LineChartView.Point(date("2014/07/03"), 400));
        points.add(new LineChartView.Point(date("2014/07/05"), 1100));
        points.add(new LineChartView.Point(date("2014/07/06"), 700));
        points.add(new LineChartView.Point(date("2014/07/08"), 1700));
        points.add(new LineChartView.Point(date("2014/07/09"), 2700));
        points.add(new LineChartView.Point(date("2014/07/10"), 100));
        points.add(new LineChartView.Point(date("2014/07/11"), 1200));
        points.add(new LineChartView.Point(date("2014/07/12"), 1100));

        DateLineChartView chartView = new DateLineChartView(this, points);
        chartView.setManualXGridUnit(2 * 24 * 60 * 60 * 1000); // 2 days

        chartContainer.addView(chartView);

こんな感じのコードで

f:id:hogelog:20140919165221p:plain

こんな感じのグラフが描画される、ごく普通のAndroid向け線グラフ描画ライブラリです。

クックパッドアプリ内ではline-chart-viewを自分が投稿したレシピのPV数を閲覧する画面などで利用しています。

f:id:hogelog:20140916181711p:plain

この機能、レシピを投稿してみると 「あー、このレシピはあんまり見られてないなー」 「おっ、このレシピ結構見られてるんだ意外だなー」 などと一喜一憂できて結構楽しい機能です。

経緯

そんなline-chart-viewですが最初からそういうライブラリを作ってクックパッドアプリに組み込もうと計画していたわけではありません。

レシピPV数を線グラフで描画する機能をAndroidアプリで実装し始めた時は既存のOSSライブラリを用いていました。ですがどうも実現したいデザイン通りの描画をするのが難しかったり不可能だったりする部分が出てきました。

自然と「一から自前で書きたい」という欲求が湧いてきます。が、一から自前で書いた場合のコストの見積りが難しかったため、会社では「既存ライブラリの仕様により、一部ほんとうに実現したかったデザインではない線グラフ」のスケジュールで仕事を進めました。

というわけでこの頃から業務時間外にline-chart-viewを実装し始めました。完全に趣味プログラミングなので上長や会社の許可も必要ありませんし、アプリのリリーススケジュールに間に合わなくても業務にダメージはありません。

そして実戦投入

会社で色々な機能を開発する一方、趣味の時間で地道にline-chart-viewの開発を進め

f:id:hogelog:20140919165334p:plain

  • 「横軸が日時、縦軸が整数値」という線グラフに元から対応している
  • X軸、Y軸のラベルを手動で設定できる(Y軸の0のラベルを省略してます)
  • グラフの枠を上下左右別々に設定できる(実は左と下の枠だけちょっと太い)
  • その他いろいろ(クックパッドアプリで必要な)デザイン調整機能

といった利点がでてきたのでMaven Centralにデプロイしたり、READMEとかちゃんと書いたり、ライブラリとしての体裁を整えた段階で

「hogelogさんって人が開発しているline-chart-viewというオープンソースのライブラリの方が良さそうなのでそっち組み込みます」

と言ってクックパッドアプリに組み込みました。

個人のクレジットもライセンス画面に組み込みました :)

f:id:hogelog:20140916181643p:plain

勝手OSSの利点

弊社では業務時間内にプログラミングした成果物をOSSも多数ありますし、OSSを積極的に公開することは推奨されています。が、line-chart-viewは(少なくともいまのところ)業務時間を利用して開発したことはありません。

# って思ってコミットログ読み返したらREADMEの更新とか細かいことちょっとやってました:P

業務上ではリスクを取りたくないけれども技術的な挑戦をしたい個人にとって、会社プロダクトに用いるための勝手OSSライブラリ開発は便利な手法だと思っています。

仕事ではアプリを作り、趣味でライブラリを作る、そんな 余暇の過ごし方(ここ重要) のススメでした。 プログラミングが趣味だけれども特に作りたいものとか存在しないような人にオススメです。

追記 2014/09/19 18:41

「勝手開発ライブラリを組み込んだ話」なんだけど「勝手開発ライブラリを勝手に組み込んだ話」と誤読する人もいるかなーと思って補足。組み込む際には普通にレビューしたり妥当かチームで検討したりとかはしてます。はい。

「関連する○○」機能を手軽に実現できる。そう、Elasticsearch ならね。

$
0
0

セコン (id:secondlife, @hotchpotch) です。ウェブサービスにはよく「このエントリーに関連するブログ記事」や「このレシピに関連するレシピ」という機能が実現されてますよね。さて、この機能はどのように実現すれば良いでしょうか。例えば tf-idf で単語の類似度を求め…といった実装が必要になり、いささか面倒です。

しかしながら Elasticsearch や Solr *1を使うと手軽に実現できます。例えば、クックパッドニュースの記事では Solr を使い「この記事を読んだ人におすすめ」の機能に、最近クックパッドにジョインしたインドネシアの会社の DapurMasakでは Elasticsearch を使い「Resep serupa(関連レシピ)」の機能で利用しています。

クックパッドニュースでのこの記事を読んだ人におすすめ

DapurMasak での関連レシピ

使い方は非常に簡単で、Elasticsearch にドキュメントを登録して検索できるような状態にしておけば、あとは Elasticsearch の more like this の APIを叩くだけで、関連する○○を実現できます。

はてなブログの関連記事を表示する

先日、はてなブログに記事の export 機能が実装されたので、この export されたファイルを Elasticsearch に読み込んで、似ている記事を表示してみます。Elasticsearch は http の RESTful な API が用意されてますが、今回は抽象化された ruby のライブラリ、elasticsearch-model *2を使います。

# entry.rbrequire'sqlite3'require'active_record'ActiveRecord::Base.establish_connection(
  adapter: 'sqlite3',
  database: 'hatena-blog-entry.sqlite'
)

unlessActiveRecord::Base.connection.table_exists? 'entries'ActiveRecord::Schema.define(version: 1) {
    create_table(:entries) {|t|
      t.string :title
      t.text :body
    }
  }
endrequire'elasticsearch/model'classEntry< ActiveRecord::BaseincludeElasticsearch::ModelincludeElasticsearch::Model::Callbacks

  mapping do
    indexes :id, type: 'string', index: 'not_analyzed'
    indexes :title, type: 'string', analyzer: 'kuromoji'
    indexes :body, type: 'string', analyzer: 'kuromoji'enddefmore_like_this(mlt_fields: 'title,body', min_doc_freq: 0, min_term_freq: 0, min_word_len: 0, search_size: 10, body: {})
    target_id = self.id
    es = __elasticsearch__
    searcher = Class.new do
      define_method(:execute!) do
        es.client.mlt(
          search_size: search_size,
          index: es.index_name,
          type: es.document_type,
          body: body,
          id: target_id,
          mlt_fields: mlt_fields,
          min_doc_freq: min_doc_freq,
          min_term_freq: min_term_freq,
          min_word_len: min_word_len
        )
      endend.new
    Elasticsearch::Model::Response::Response.new(self.class, searcher)
  endend

このように、ActiveRecord のモデルに Elasticsearch::Model を include し、more_like_this メソッドを生やします。また日本語の形態素解析のため、アナライザには kuromojiを指定してます。

続いて、はてなブログの export した記事を読み込み、Elasticsearch に保存します。export されたファイルが MovableType 形式ェ…。

# hatena-blog-importer.rbrequire'./entry'require'time'require'nokogiri'

source = ARGV[0]
abort"usage: bundle exec ruby #{$0} hatenablog.export.txt"unless source

entries = File.read(source).scan(/              TITLE:\s(.*?)\n.*?              STATUS:\s(.*?)\n.*?              DATE:\s(.*?)\n.*?              BODY:\n(.*?)              ----\n/mx)

entries.each do |entry|
  title, status, id, body = *entry
  if status == "Publish"Entry.where(id: id.gsub(/\D/, '')).first_or_create(title: title, body: Nokogiri::HTML(body).text)
  endend

export してきたデータを流し込みます。

$ bundle exec ruby hatena-blog-importer.rb techlife.cookpad.com.export.txt

これで Elasticsearch にドキュメントが登録された状態になります。Elasticsearch を使った検索を試してみましょう。

$ bundle exec pry
> require'./entry'
=> true> Entry.search('ActiveRecord').records.map(&:title)
=> ["クックパッドにおける最近のActiveRecord運用事情",
      "Rails アプリケーションのパフォーマンスについて RubyKaigi 2013 で発表しました",
      "クックパッドとマイクロサービス"]

うまく動いてますね。続いて「類似する記事」を more_like_this メソッドを叩いて表示してみましょう。

> entry = Entry.where(title: '5分でわかるBatteryHistorianによるAndroidアプリの解析方法').first
> entry.more_like_this(search_size: 3).records.map(&:title)
=> ["Android Publisherによるストア管理の自動化",
      "Amazon Cognitoについて - AWSが提案するモバイル時代のアカウント管理",
      "ドイツでIoTについて考えた"]

ちゃんと5分でわかるBatteryHistorianによるAndroidアプリの解析方法に近しい記事がマッチしてますね。スコアも入ってます。

> entry.more_like_this(search_size: 3).map {|r| [r.title, r._score]}
=> [["Android Publisherによるストア管理の自動化", 0.7496942],
      ["Amazon Cognitoについて - AWSが提案するモバイル時代のアカウント管理", 0.58310145],
      ["ドイツでIoTについて考えた", 0.45574456]]

なお、これらのサンプルコードは、以下に push してあります。

まとめ

サイトに何らかの検索機能を付与する場合、今は Elasticsearch や Solr を検索エンジンとして使うことが多いと思います。そんなときは、すでにドキュメントを検索エンジンに登録してるでしょうから、あっという間に「関連する○○」機能を実現できるでしょう。

他にも「もしかして」を実現する phrase suggester APIや、autocomplete を実現する completion suggester APIなど、Elasticsearch や Solr *3には便利な API がそろってたり*4します。

実際にユーザに対してベストな解決策*5では無い場合もありますが、プロトタイピングで使ったり、実際に「関連する○○」を用意した場合、どれぐらい便利に使われるのか確かめるMVPとしては非常に便利ですね。

*1:タイトルでは触れてませんでしたが、Solr でも簡単です

*2:elasticsearch-model は elasticsearch 社のオフィシャルライブラリとして提供されてますが、オフィシャルになる以前は tire という名前で、オフィシャルになった後 tire の方は retire という名前に変更されました…リタイヤ…

*3:両方とも検索のエンジンのコアには Lucene を使ってるので、Lucene に入った機能はそのうち利用できるようになる可能性が高いです

*4:なおそれっぽい記事を書いてますが、私自身弊社検索野郎の @PENGUINANA_ に教えて貰うまで、more like this API を知りませんでした…。どんなことが出来るのか、しっかりと知ることも重要ですね。

*5:例えば「もしかして」機能は文字の編集距離を求めて算出するより、検索ログを追いかけてどうユーザが正解の単語を入力したかから学習したほうが基本的には精度が高い、など

OSSライセンス表記の自動生成機能をCocoaPods Pluginで改善した話

$
0
0

モバイルファースト室の@y_310です。

iOSアプリでオープンソースなライブラリを使う場合、サーバサイドアプリケーション以上にソフトウェアライセンスを意識する必要があります。 多くのライブラリはMITライセンスや修正BSDライセンスで提供されていますが、それらのライブラリを使う場合、再配布時に元のライセンス条文を配布物のどこかに含めることが要求されています。

とは言え、アプリケーションに含めたライブラリのライセンスを確認して確実に配布物に含めていく作業というのはどうしても漏れがちで手間なものです。

そこでiOSで標準的に使われているパッケージ管理ツールであるCocoaPodsでは、ライブラリのインストール時に自動的に各ライブラリのライセンス表記を集約し1つのplistファイルにまとめてくれる機能を持っています。 あとはこのファイルをSettings.bundleの中に移動すれば設定アプリの中にライセンスが表示されるようになります。

この辺りのやり方についてはCocoaPodsのWikiに掲載されています。

https://github.com/CocoaPods/CocoaPods/wiki/Acknowledgements

GitHubなどで公に公開されているライブラリのみを使用する場合はこれで終わりです。 しかし社内用のライブラリなど外部に公開していないものがPodfileに含まれている場合、それも自動的にこのファイルに含まれてしまうためあまり具合が良くありません。

ここでようやく本題ですが、この問題を解決するためにcocoapods-ack_filterというCocoaPods Pluginを作りました。

このプラグインを使うと、plistファイルを生成する際に、ライセンス本文が指定した正規表現にマッチするライブラリを除外することができます。

CocoaPods Pluginとは

CocoaPods PluginはCocoaPods 0.28から導入されたプラグイン機構で、podコマンドに機能追加ができるようになります。 作り方の詳細はこちらのオフィシャルブログに書かれています。 http://blog.cocoapods.org/CocoaPods-0.28/

PluginのルールにそってRubyのgemを作るだけなので、Rubyに慣れている人であれば簡単に作ることができます。

インストール

gem install cocoapods-ack_filter

設定

まず、除外したいライブラリのpodspecを変更します。このtextに指定した文字列にマッチさせてライセンス表記を除外させます。

Pod::Spec.new do |s|
  ...
  s.license = { type: 'Private Library', text: 'Copyright 2014 MyCompany Inc. All Rights Reserved.' }
  ...
end

Podfileに以下のpost_installブロックを追加し、正規表現と出力先のパスを適切に変更します。

# Podfile

pod 'AFNetworking'
pod 'MyCompanyLib'

post_install doPod::Command::AckFilter.filter(
    pattern: /MyCompany/,
    output: 'OurApp/Settings.bundle/Acknowledgements.plist'
  )
end

実行

いつものようにpod installするだけです。

pod install

これで、outputに指定したパスにライセンスの記載が不要なライブラリを除外したplistファイルが出力されます。

ちなみにCocoaPods Pluginとして実装しているのでコマンドラインから実行することもできます。

pod ack-filter MyCompany --output=OurApp/Settings.bundle/Acknowledgements.plist

生成されたAcknowledgements.plistは変更管理できるようにリポジトリに含めておきましょう。

まとめ

cocoapods-ack_filterを使うことでライセンス表示ファイルの生成を日常のワークフローの中で完全に自動化することが出来ました。 セットアップも簡単なので新しいアプリに導入する際も少ない工数ですぐに導入できます。

iOSアプリでレシピ投稿機能を統合したねらい

$
0
0

レシピ投稿推進室の大前です。

クックパッドでは、公式アプリ(以下、本体アプリ)から独立したアプリケーション(「クックパッド のせる」)として提供していたレシピ投稿の機能を、2014年9月に本体アプリに完全統合しました。

※iOSプラットフォームのみ。Androidは2012年に統合済み

今回は、もともと別アプリケーションとして分離していた投稿機能を統合したねらいと、統合において課題となった点などについて簡単にご紹介したいと思います。

統合の目的・ねらい

レシピ投稿機能を統合するねらいは大きく2つありました。

1点目は、レシピ投稿機能を統合することで、「今日つくる料理を見つける」ことを主要な目的として本体アプリを利用されている方にも、より身近にレシピ投稿の機会を感じてもらえるようにするという点。

2点目は、クックパッドというレシピサービスがユーザーのレシピ投稿によるエコシステムによって成り立っているということを、レシピを投稿するユーザー、さがすユーザー双方に実感してもらえるようにしたい、という点でした。

クックパッドを利用していただくユーザーさんが増えるにつれ、モバイルアプリケーションのみでクックパッドを利用し始めるという方もおり、そういった方々に上記のような点を感じてもらいながら、それと同時にモバイルアプリケーションからのレシピ投稿者を増やすことを実現することを目的に統合プロジェクトをスタートさせました。

課題

例えば、インスタグラムのように、同じユーザー投稿型のサービスでも、利用者のほとんどが投稿も閲覧もするユーザーであるようなユーザー構成のサービスとは違い、クックパッドの利用者の大半はレシピを検索、閲覧するユーザーで、レシピ投稿者の割合はごくわずかなユーザー構成のサービスです。

統合における一番の課題は、当初、分離という選択肢をとっていた理由そのものでもあるのですが、そのような「今日つくる料理を見つける」と「レシピを投稿する」というまったく異なる目的・異なるターゲットユーザーを持った価値を一つのアプリケーションで実現する必要がある点でした。

ウェブページとモバイルアプリケーションの違い

とは言え、もともとクックパッドはウェブサービスとしてスタートしたサービスで、現行のウェブ版のサービスでもすべての機能が分離されず(別サービスとして展開されているものを除き)ひとつのウェブサービスとして提供されています。

ウェブサイトでのレシピページ

以下はPCサイトでわたし自身が投稿したレシピページを表示したものです。 WYSIWYGの思想を元に他人がこのレシピを見たのとほぼ同じような表示のまま編集できるような作りになっています。 また、右カラムにはレシピがどれくらい見られたかというような自分だけが見ることのできる情報を表示するエリアがあります。

f:id:yuki-omae:20140925094614p:plain

おおまかに分類するとこのページでは以下の3点の目的を実現するための機能が混在しています。

  • レシピを閲覧するコンテキストでの機能(
  • レシピを編集するコンテキストでの機能(
  • レシピに対するフィードバックを閲覧するコンテキストでの機能(

f:id:yuki-omae:20140925094618p:plain

これをモバイルアプリケーションのレシピ画面で実現することを検討してみるとどうでしょうか。 以下がモバイルアプリケーションでのレシピの画面です。

f:id:yuki-omae:20140925094621p:plain

PCでの表示より圧倒的に表現できる情報量が少ないことがわかると思います。 この面積の中に、先ほどのPCページが満たしていた機能を全て混在させるとどうでしょうか。

例えば、レシピを編集する観点で見た場合、「レシピを送る」という機能は余計だし、このレシピを見て料理をつくろうとした場合「フィードバック」の要素は邪魔にならないでしょうか。

異なる利用コンテキストへの対応を、モバイルアプリケーションの小さな画面サイズで実現しようとした場合、PCなどの大画面をベースに設計されたものを、単純には移植するだけではだめだということがわかっていただけると思います。

モードを分ける

今回の統合では、このような異なるコンテキストでの利用が想定されるような場面では、基本的にモードを分離する、という方法でひとつのアプリケーション内で異なる利用コンテキストを考慮して機能を共存させることを徹底しました。

先ほどのレシピページは以下の様な構成で利用できるようにしました。

f:id:yuki-omae:20140925094624p:plain

自分のレシピの一覧から「閲覧」「編集」「フィードバックの閲覧」という異なるコンテキストを導線として分岐させ、利用しようとしているコンテキストに応じて、最適化された画面を表示して利用することができるようにしています。

レシピページは一例ですが、このように異なる利用コンテキストやユーザーが混在することを考慮して、レシピを投稿するための機能や導線を既存の「今日つくる料理を見つける」ことを実現するためにデザインされたアプリケーションに統合していきました。

まとめ

上記のような対応を経て、2014年9月に機能統合をはたした結果、現在、iOSプラットフォームは、全プラットフォームの中でも1番レシピ投稿者の多いプラットフォームになっています。

投稿者数の面だけでなく、クックパッドというサービスがレシピの投稿者あってのサービスだという認識の向上にも貢献できているのではないかと思います。

今回は、レシピページという一部の実例だけの紹介にとどまりましたが、今回紹介させていただいたような観点でぜひ実際のアプリ内でレシピ投稿をお試しいただければと思います。


『健康レシピ』スマートフォンサイトのデザインプロセスについて

$
0
0

ユーザーファースト推進部の坂本です。 8/4(月)に自分がデザインを担当している『健康レシピ』のスマートフォンサイトを公開しました。 今回のスマートフォンサイトのデザインプロセスに合わせてクックパッドでよく用いているツールを紹介します。デザイナーやエンジニアの方が今後スマートフォンサイトを作る際に参考にして頂くためまとめてみました。

■健康レシピとは…

クックパッドで公開されているレシピを管理栄養士監修のもとに選定し、簡単に栄養計算ができるようにしたサービスです。主に糖尿病や高血圧、脂質異常症やメタボの方向けに、健康的でおいしい食事を紹介しています。

f:id:kanako-sakamoto:20140930112934p:plain

スマートフォンサイトのデザインプロセス

スマートフォンサイトのデザインプロセスは、以下の流れで行いました。

  1. 画面構成を作成し、全体の遷移を確認する
  2. 各ページのデザインを作成する
  3. Flintoで遷移に違和感や問題点がないかを確認する
  4. コーディング後、各端末での表示と操作の検証と、目的達成の確認をする

各項目について詳しく説明していきます。

1. 画面構成を作成し、全体の遷移を確認する

まずは私たちが理想とするユーザーの体験フローを想定し、各画面の構成を作成します。健康レシピでは以下のような体験を想定しました。

レシピを探す⇒レシピを献立に追加する⇒栄養価を確認⇒材料を買う&レシピの手順をみながら料理をつくる

この流れを滞りなく行える構成や遷移になっているかを確認するために、Cacooというワイヤーフレームの作成ツールと、ペーパープロトタイプを使用しました。

Cacooでは実際のデザインに近いイメージで構成を作成できるので、デザインを起こした時のズレを少なくすることができます。また、ペーパープロトタイプでは短時間で検証と評価を繰り返すことができ、それによって全体の品質を高めることができます。

今回の健康レシピのスマートフォンサイト作成では、ペーパープロトタイプで全体的な構成と遷移の確認をしたあと、Cacooで各画面のより詳細な構成を作成し、品質を高めるとともに先の工程での手戻りを最小限に抑えるようにしました。

f:id:kanako-sakamoto:20140930113110p:plain

Cacoo(無料・有料)
Web上で簡単に遷移図やワイヤーフレームなどが作成でき、メンバーに共有できるサイトです。

2. 各ページのデザインを作成する

次に各ページのデザインを作成します。ここでは情報の強弱などの機能面でのデザインと、楽しさ・親しみやすさなどを表現する装飾面でのデザインとなります。

健康レシピは、レシピ検索&栄養計算サイトというツール的な面もあり、ユーザーが毎日使っても飽きない、負担にならないようなデザインを目指しました。とはいえ、簡素になりすぎても面白みがなく、使ってみたい、という気も起こりにくくなります。なので下記の点で工夫をして、機能面と装飾面のバランスの良いところを探っていきました。

  • 要素の大小や色などでメリハリをつけて情報の強弱をわかりやすくする
  • 色などで「らしさ」を出し、クックパッド本体から移動してきた人に違うサイトに遷移したことを伝える
  • 上記と少し矛盾するが、クックパッドの一サービスなので同じ風が流れているようにする
  • 対象ユーザーが親しみやすく楽しいと思えるような雰囲気を作り、使ってみたいという興味を引き出す


ページごとのデザインは、LiveViewというツールを使ってiPhoneで確認をします。LiveViewはデザインカンプをiPhone、iPadでリアルタイムに確認できるツールで、デザインの微調整を行うときに大変便利です。スマートフォンサイトやアプリのデザインをしたことがある方は覚えがあると思うのですが、実際の端末でデザインを見ると、Photoshop上でデザインした時と印象が違っていた(思っていたより色が薄かったり、要素が小さかったりなど)ということが良くあります。

そのような時に細かく調整をして確認する、という作業が必要になり、このツールによって時間と手間の短縮ができます。

LiveView(無料)
PCの画面をiPhoneやiPadなどでリアルタイムに確認できるツールです。PCで修正したデザインがすぐiPhoneなどで確認でき、大変便利です。

3. Flintoで遷移に違和感や問題点がないかを確認する

各ページのデザインをFlintoにアップし、iPhoneなどの端末で遷移の確認をします。本物に近いモックアップを実際の端末で見て操作をすることで、構成やデザイン作成でつめきれていなかった部分の発見と改善ができます。 これらの問題点は構成に戻って修正⇒デザイン修正⇒Flintoで再確認します。この流れを何度か繰り返すことで、プロダクトのブラッシュアップができます。

また、Flintoでつくったモックアップはメンバーにも共有できるので、意見の交換や議論がしやすくなります。これもプロダクトのブラッシュアップにつながります。

f:id:kanako-sakamoto:20140930113126p:plain

Flinto(有料)
スマートフォンサイトやアプリのプロトタイプがつくれるツールです。Photoshopなどで作成したデザインをアップし、リンク先を設定することで本物に近いモックアップを作ることができ、また社内外で共有できます。

4. コーディング後、各端末での表示と操作の検証と、目的達成の確認をする

各端末での表示と操作の検証

検証は以下のようなシートを用意し、表示と操作について細かく項目を設けて検証していきます。こうすることで各端末や担当者での見落としがなくなり、ユーザーに迷惑をかけることがぐっと少なくなります。

f:id:kanako-sakamoto:20140930113141p:plain

▲のついた項目は備考欄にバグ内容を細かく書きます。

Android、iPhoneの各端末でチェックする際には、ユーザーが2%以上利用しているかをクックパッドの基準として設けて、それに従って検証をしています。例えば、Android 2.2系の利用率は2%未満なので検証をしない、などとしています。これらの利用率は月に一度確認し、2%を切ったものは検証から外していきます。

目的達成の確認

検証後、全体を通して使ってみて、問題なくユーザーが目的達成ができるかどうかの確認をします。動きなどを含めた全体の確認をすることで、つまずいた部分やとまどった部分の洗い出しをしやすくなります。

見つかった問題点は、健康レシピの場合では直接コードを修正⇒再確認、というのが多いです。この方法だとその場でメンバーや対象ユーザーに再度テストしてもらうことができ、改善のスピードを早めることでより完成に近づけていくことができました。

以上です。 サービスのデザインプロセスには、それぞれのフェーズに合ったツールの選択を正しく行う事が大切です。そのため、色々な方法を試行錯誤することも欠かせません。また、サービスは作って終わりではなく、作ってからが始まりなので、もっと便利に使っていただけるよう改善のための試行錯誤もどんどんしていく予定です。

Rails 4 へのアップグレード時に遭遇した問題

$
0
0

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

クックパッドでは8月に本体アプリケーションや API サーバ等で使われている Rails のバージョンを 3.2 から 4.1 へ順次アップグレードを行いました。 アップグレードは主に松田さん (@amatsuda) と私で進めました。 この記事ではアップレードの際に遭遇した問題の一部を紹介します。

MySQL strict mode の有効化

MySQL を使っている場合、Rails 4.0 からデフォルトで @@SESSION.sql_mode = 'STRICT_ALL_TABLES'が最初に実行されるようになりました (Ruby on Rails 4.0 Release Notes) 。 これを無効化するために database.yml で strict: falseという設定が用意されています。 しかし、同じく Rails 4.0 で導入された partial insertと組み合わさると、Rails 3.2 とは挙動が変わるケースがありました。

たとえば NOT NULL 制約がついているカラムに MySQL のデフォルト値が挿入されるようになりました。 以下のような recipes テーブルがあったとします。

mysql> SHOW CREATE TABLE recipes;
+---------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table   | Create Table                                                                                                                                                                              |
+---------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| recipes | CREATE TABLE `recipes` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `title` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+---------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

このとき Rails 3.2 では、user_id や title を指定しないと、新しいレコードが NOT NULL 制約により作られませんでした。

[1] pry(main)> ActiveRecord::VERSION::STRING
=> "3.2.18"
[2] pry(main)> Recipe.create
   (0.2ms)  BEGIN
  SQL (0.4ms)  INSERT INTO `recipes` (`title`, `user_id`) VALUES (NULL, NULL)
   (0.1ms)  ROLLBACK
ActiveRecord::StatementInvalid: Mysql2::Error: Column 'title' cannot be null: INSERT INTO `recipes` (`title`, `user_id`) VALUES (NULL, NULL)

しかし Rails 4.0 以降で strict: falseにすると、partial insert と MySQL のデフォルトの sql_mode の組み合わせにより INSERT に成功してしまいます。

[1] pry(main)> ActiveRecord::VERSION::STRING
=> "4.1.4"
[2] pry(main)> Recipe.create
   (0.2ms)  BEGIN
  SQL (0.3ms)  INSERT INTO `recipes` VALUES ()
   (0.3ms)  COMMIT
=> #<Recipe id: 1, user_id: nil, title: nil>
[3] pry(main)> Recipe.last
  Recipe Load (0.3ms)  SELECT  `recipes`.* FROM `recipes`   ORDER BY `recipes`.`id` DESC LIMIT 1
=> #<Recipe id: 1, user_id: 0, title: "">

これでは壊れたデータが保存されてしまうため、Rails のバグとして Rails 側を修正しようとしました。 しかし partial insert を維持しつつこの挙動をうまく抑えることは難しいと判断し、クックパッドでは strict mode を有効化することで対応しました。

幸い sql_mode はセッション毎に設定できるので、徐々に strict mode を有効化しながら進めることができました。 最初にテストを strict mode で通る状態にし、テスト時と開発環境で常に strict mode を有効化しました。 その後、本番の app サーバで徐々に strict mode を有効化していき、問題が無いか確認しながら最終的に全 app サーバで有効化しました。

acts_as_readonlyable からの脱却

こちらの記事にあったように、クックパッドでは acts_as_readonlyableを使い続けてきました。 acts_as_readonlyable は Rails において R/W splitting を行うための gem です。 本家の更新は2007年で止まっていますが、Rails のバージョンを上げる度に改修しながら使い続けてきました。

今回のアップグレードも同じように acts_as_readonlyable を改修して乗り切ろうとしましたが、大きく実装を変えなければ対応が難しかったため、switch_pointを作りこちらへ移行することにしました。 Rails のバージョンに極力依存しないように書いたので、Rails のアップグレード前に置き換えることに成功しました。

なお、switch_point については RubyKaigi 2014 の LTで発表しています。 詳細については、こちらの資料をご覧ください。 https://speakerdeck.com/eagletmt/w-splitting-in-rails

database_cleaner + 独自パッチからの脱却

クックパッドの本体アプリケーションには大量のモデルがあるため、テストケース毎に全てのテーブルへ DELETE 文を発行すると、非常に時間がかかってしまいます。 この問題に対処するため、以前は database_cleanerに独自パッチをあてて、INSERT が行われたテーブルのみレコードを削除していました。 しかし、今回のアップグレード時にこの独自パッチ部分が問題になりました。 そこで同等のことを行える database_rewinderを松田さんが作り、こちらへ移行しました。

implicit join references

Rails 3.2 までは includes で指定した関連先が where で使われている場合、Rails が JOIN を補ってくれていました。

classUser< ActiveRecord::Base
  has_many :recipesendclassRecipe< ActiveRecord::Base
  belongs_to :userend

このような関連があるとき、Rails 3.2 では Recipe.includes(:user).where('users.id = 1')とすると、where の引数を見て users テーブルを LEFT OUTER JOIN することでまとめて取得していました。 このため、明示的に joins を指定せずに where にこのような条件式を渡しても正常に動作していました。

しかし Rails 4.0 でこの挙動が deprecated になり、4.1 で取り除かれました。 そのため、Rails 4.1 では SELECT recipes.* FROM recipes WHERE (users.id = 1) ORDER BY recipes.id ASC LIMIT 1というような不正なクエリが生成されてしまいます。 クックパッドのコードベースには Rails 3.2 までの includes と where の挙動に依存したコードが多くあり、テストの失敗を見ながら1つずつ joins や references を補っていきました。

Time の JSON 表現にマイクロ秒

Ruby 1.9 から Time がマイクロ秒を持つようになりました。 Rails 4.0 からは Ruby 1.9 以上のみをサポートするようになったため、JSON 表現にもデフォルトで小数点以下3桁まで含むようになりました。 ところが MySQL に保存する際に時刻のマイクロ秒が落とされてしまうので、従来のまま小数点以下の値を切り捨てることにしました。 これは config.active_support.time_precision = 0とすることで設定できます。 http://guides.rubyonrails.org/configuring.html#configuring-active-support

セッション flash の非互換性

クッキーへのセッションの保存方式が Rails 4.0 で変わりました。 Rails 3.2 方式の設定である config.secret_tokenのみ設定していれば、従来の保存方式が使われるため、ロールバックする可能性があるアップグレード時には従来の方式のみ使われるようにしました。 また Rails 4.1 ではデフォルトのシリアライズ形式が marshal から json へと変わったため、config.action_dispatch.cookies_serializer = :marshalとして従来の marshal のみを使うように設定しました。 http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#cookies-serializer

これでアップグレードした場合もロールバックした場合もセッションを維持することができるようになりました。 しかし、flash で使うために保存されているオブジェクトに非互換性がありました。 Rails 3.2 では ActionDispatch::Flash::FlashHash というクラスのインスタンスであったのに対し、Rails 4.1 では単純な Hash になりました。 このため、ロールバックによって Rails 4.1 でセットされたセッション flash を持って Rails 3.2 にアクセスした場合、エラーが発生してしまいます。 具体的にはこの行でエラーになっていました https://github.com/rails/rails/blob/3-2-stable/actionpack/lib/action_dispatch/middleware/flash.rb#L239

この非互換性に対処するため、rails_4_session_flash_backportという gem を利用しました。 この gem を Gemfile に書くだけでモンキーパッチがあたり、Rails 4.0 方式のセッション flash を Rails 3.2 でも読めるようになります。

ActiveRecord が生成するクエリの変化によるスロークエリ

自動テストでは気付きにくい点ですが、ActiveRecord が生成するクエリの変化によりスロークエリになっている箇所がありました。

first を使ったときの ORDER BY id ASC

User.firstと書いたときに、Rails 3.2 では SELECT users.* FROM users LIMIT 1というクエリが生成されていましたが、Rails 4.1 では SELECT users.* FROM users ORDER BY users.id ASC LIMIT 1になりました。 この ORDER BY がつくようになった影響でスロークエリになっていたものが何箇所かありました。

一般に ORDER BY をつけなかった場合に LIMIT 1 でどのレコードが取得されるかは不定です。 しかし、対象が一意に決まるような条件式を与えたときには ORDER BY は不要です。 このような場合には take を使います。User.takeならば SELECT users.* FROM users LIMIT 1というクエリになります。

scope 内での current_scope の変化

例えば以下のような Entry モデルがあったとします。

classEntry< ActiveRecord::BaseCATEGORY_ALPHA = 1

  scope :recent, lambda { where('id >= ?', Entry.maximum(:some_id)) }
  scope :with_category, lambda { |n| where(category: n) }
end

このとき、Entry.with_category(CATEGORY_ALPHA).recentとすると、Rails 3.2 では

/* Rails 3.2: Entry.with_category(CATEGORY_ALPHA).recent */SELECT MAX(some_id) AS max_id FROM `entries`;
SELECT `entries`.* FROM `entries` WHERE `entries`.`category` = 1AND (id >= 42);

のようなクエリが生成されていました。 一方 Rails 4.1 では

/* Rails 4.1: Entry.with_category(CATEGORY_ALPHA).recent */SELECT MAX(`entries`.`some_id`) AS max_id FROM `entries` WHERE `entries`.`category` = 1;
SELECT `entries`.* FROM `entries` WHERE `entries`.`category` = 1AND (id >= 42);

のようなクエリが生成されます。このように、scope の中でクエリを実行したときにそれまでの scope が引き継がれるようになりました。 したがって、Entry.recent.with_category(CATEGORY_ALPHA)のように scope の適用順序を変えるとクエリも変わります。

/* Rails 4.1: Entry.recent.with_category(CATEGORY_ALPHA) */SELECT MAX(`entries`.`some_id`) AS max_id FROM `entries`;
SELECT `entries`.* FROM `entries` WHERE (id >= 42) AND `entries`.`category` = 1;

scope の中でこのようなことをしたい場合、明示的に unscoped を使うことで回避しました。

classEntry< ActiveRecord::BaseCATEGORY_ALPHA = 1

  scope :recent, lambda { where('id >= ?', Entry.unscoped.maximum(:some_id)) }
  scope :with_category, lambda { |n| where(category: n) }
end

これならば、Entry.with_category(CATEGORY_ALPHA).recentでも Rails 3.2 と同じクエリが生成されます。

actionpack-xml_parser の切り出し

Rails 3.2 までは JSON だけではなく XML を入力した場合でもリクエストパラメータとして取得することができました。 しかし、この機能は Rails 4.0 から Rails 本体から切り出されて actionpack-xml_parserという gem になりました。 比較的新しい API はすべて JSON フォーマットでやりとりしている一方、古い API では XML フォーマットも使っているため actionpack-xml_parser が必要でした。 コントローラのテストでは params をハッシュで渡しているのでこの変化に気付かず、実際のアプリの接続先をアップグレード後のサーバに向けるとなぜかパラメータが空になる……という現象が発生しました。

まとめ

ここで紹介した修正以外にも、ActiveRecord の記法の変化のような単純な構文上の修正や、Rails の内部実装に強く依存した実装やモンキーパッチの修正などを行いました。 それにより、最終的に Rails のアップグレードに成功しました。 レスポンスタイムの悪化が懸念されていましたが、API サーバで若干悪化したものの、想定の範囲内に収まっています。

この記事がこれから Rails 4.x へのアップグレードをする方の参考になったら幸いです。

2014年も開発コンテスト24を開催します!

$
0
0

開発コンテスト24とは

開発コンテスト24とは、午前9時にテーマが発表されてから24時間以内という制限のもと

  • コンセプトを決め
  • デザインを定め
  • 実装を完了する

までをこなす、開発のトライアスロンとでも言うべきイベントです!

2010年から毎年開催されてきた開発コンテスト24ですが、今年は10月18日(土)の開催となります。 詳細はこちら (「第5回 開発コンテスト24」開催のお知らせ) をご覧ください。 今年のテーマはリンク先のページで10月18日 (土) の午前9時に発表されます。

作業スペースとしてクックパッドのラウンジを開放します!

10月18日 (土) 8時30分〜17時30分の間、クックパッド本社 (渋谷区恵比寿) のラウンジを作業スペースとして開放します。 地図

f:id:rejasupotaro:20141002140114p:plain

当日はクックパッド社員が手作りランチでおもてなしします。ぜひともご利用ください!

なおラウンジ利用の申し込みの期限は10/10 (金) 11時までで、キャパシティの都合で先着20名とさせていただきますので、お早めにどうぞ!

オフィスラウンジ利用登録フォーム

今までのテーマ

第1回

  • テーマ: 朝起きてから学校や会社に行くまでの時間をより便利にするためには
  • 最優秀作品: MoCo Time for iPhone

MoCo Timeはポジティブな気持ちで早起きができるようになるiPhoneアプリです。 「どうやったら自分は本当に起きることができるんだろう」という問題に対して、前向きな解を見つけ、デバイスで実現したことが評価されました。

第2回

  • テーマ: (普段の生活で)半径3m以内にいる人が困っていることを解決する
  • 最優秀作品: はなことば

「はなことば」は、困っていることを伝えられなくて困っている人を助けるデバイスです。 新人が困ってる場面などの具体的な利用シーンを想定でき、実際に役に立つものになっています。花が咲く、しおれる、シャキーンと花がまた咲くという動きがコミカルで、周りの人を楽しませる点も評価されました。

第3回

  • テーマ: 一日の終わりを楽しくするもの
  • 最優秀作品: チリヤマ

一日の終わりに、その日の節約っぷりを実感してニヤニヤするAndroidアプリです。 節約という行為がもっと楽しいものになるはずだ、という問題意識を持ち、その解決をシンプルな入力インタフェースと、節約の結果買えるものを提案することで実現している点が評価されました。

第4回

  • テーマ: 年をとった自分が使うサービス
  • 最優秀作品: DECOPOCHI

DECOPOCHI(デコポチ)は、ぽち袋を手軽にデザイン出来るサービスです。 作ったぽち袋はその場で印刷しすぐに使う事が出来ます。作品の完成度がとても高く、素直に使いたいと思える作品でした。


今年は最優秀賞が1本と優秀賞が5本の総額50万円で、それぞれハックが楽しくなるような賞品をご用意しています! みなさまのご応募を楽しみにしております!

開発環境のデータをできるだけ本番に近づける

$
0
0

こんにちは。技術部の吉川です。 今回はクックパッドの開発環境構成、特に開発用データベースの構成についてご紹介します。

開発環境の構成

クックパッドのシステム環境は以下のようなフェイズに分かれています。

f:id:adorechic:20141002194317p:plain

※ これはcookpad.comの構成で、サブシステムや個別のサービスはその規模や特性に応じて構成が異なります。

development

開発者が実際に開発を行う環境です。クックパッドでは仮想環境は用いず、手元のマシンでRailsアプリケーションを動かして開発を行っています。 データベースはローカルではなく、開発者全体で共通の開発用データベースに接続しています。

test

手元でテストを実行する場合は、ローカルマシンのデータベースを利用します。CI(rrrspec)などの場合も同様で、テスト実行サーバーのデータベースが利用されます。

staging

stagingといえば準本番環境として、本番前の最終確認用の環境として位置づけられるのが一般的かと思います。 クックパッドでは後述するproduction-test環境がそれに近い位置づけで、stagingは少し独特な使い方をしています。

開発者は任意のブランチを独立した専用のインスタンスに対して任意のホスト名でデプロイすることができます。

一般的な利用方法としては、開発者のフォークリポジトリの特定のブランチをデプロイして利用します。 指定したホスト名が社内からアクセスできるURLになるため、あるチーム専用のクックパッド環境ができあがります。

通常の開発フローでは、stagingへのデプロイは必須ではありません。基本的にはローカルマシンで動作確認し、またchankoを利用してスタッフベータ公開とすることでproductionで動作確認を行います。

大きめの機能をチームで開発している場合などに、マージ前に非開発者を含めたチーム全員で動作確認したい場合や、 あるいは連携するシステムからのAPIコールを受けたい場合に、常時利用できる環境としてstaging環境が利用されています。

なおこのstaging環境もdevelopment環境と同じ共通の開発用DBに接続されています。

production-test

準本番環境・・・というかデータベースはproductionのものなので実アクセスから切り離された本番環境、といった方が正しいかもしれません。 CIを通ったリビジョンは自動的にこのproduction-testにデプロイされ、常に最新のリビジョンがデプロイされた状態です。 productionへのデプロイ前に開発者はここで最終確認を行います。

stating-dbとproduction-dbの同期

説明した通り、クックパッドでは手元で開発する場合も共通の開発用データベースを利用しています。 しかし一般的には手元の開発環境でのデータベースはローカルのデータベースに接続する構成が多いのではないでしょうか。

クックパッドの開発用データベースは、productionのデータベースと同期されています。 手元で動かせば、常に本番の最新のレシピが表示されるようになっているのです。

本番と同等のデータで開発することで、どういったメリットがあるのでしょうか?

ユーザーと同等の体験をして開発する

手元でテストする場合には”test” のような適当な入力でテストしたり、それらしいデータを自動生成して用意しておく場合が多いのではないでしょうか。 しかしそれはユーザーが実際に体験するクックパッドではないのです。

"testtesttest..."のようなレシピが並ぶのと、実際にユーザーが投稿してくれたレシピが並ぶのでは、見た目の印象や感じ方だけではなく、操作感も変わってきます。 なるべく本番と同じ状態のクックパッドで開発することで、ユーザーと同等の体験のもとに開発ができるのです。

予期せぬデータによるバグに気づきやすい

前述したようなテストデータはあくまで開発者が予期したデータにすぎません。実際のデータはもっと複雑で、想定していないデータが存在してバグの原因になったりします。 開発の時点で本番データで開発を行っていれば、そういった環境間の差異による問題に気づきやすくなります。

重いクエリに気づきやすい

indexなどに気を使っていても、本番と開発環境でcardinalityが異なっていて思った通りにindexが働かなかった、ということはないでしょうか。 事前に予期できていれば検証環境を用意するといったこともできると思いますが、副次的に発生したクエリだったりすると本番にデプロイして初めて気づくということもあるのではないでしょうか。

これも初めから本番データで開発していれば、本番で重いクエリは手元で動かしても重いため、すぐに気がつけます。 また開発者全体でデータベースを共有しているため、たまに誰かが気付かず重いクエリを発行していると他の誰かが気づいたりします。

本番同期の実現方法

本番と同期するためにMySQLのレプリケーションを利用しています。 staging-dbはproduction-dbからレプリケーションしており、その状態でstaging-dbへの開発用書き込み・参照を行っています。

とはいえ、実際にはこれを実現するためには様々な問題があります。

更新時のコンフリクト

slaveに書き込んだら速攻でレプリが止まるのでは・・・と思いきや、クエリベースのレプリケーションを行っていると案外止まりません。 主キーがAUTO_INCREMENTなテーブルに対してそれぞれでINSERTが走ってもコンフリクトしないのです。 しかし実際にはまともに動作しません。INSERTに成功していても、コンフリクトしたレコードの主キーはproduction-dbとstaging-dbで異なってしまいます。 あるいはstaging-dbでINSERTすると、同じidに対するUPDATE文がレプリされてくるため、何も触ってないのに勝手にデータが変更されていきます。 production-dbとstaging-dbで対象データに齟齬が生じてしまっているわけです。

そこでMySQLの行ベースレプリを利用します。行レプリであればレコードが正確に複製されるため、コンフリクトしたINSERTは失敗します。 失敗されては困るので、あわせてstaging-dbのテーブルはAUTO_INCREMENTのオフセットをidが衝突しない程度に進めておきます。

production-dbではidが1, 2, 3…と進み、同じidを持つレコードがstaging-dbに複製されていきます。 オフセットを仮に600000000にしていた場合、開発環境からINSERTされたレコードのidは600000001, 600000002, 600000003… という風に進みます。 これでコンフリクトせず、本番データと開発データが共存できます。

ちなみに社内でのスキーマ管理はRidgepoleを利用していますが、 これを用いてテーブル作成するとこのAUTO_INCREMENTのオフセット設定は自動でやってくれるようになっています。

なお通常のmaster-slave間でのレプリケーションにはステートメントベースレプリケーションを利用しているため、そのままでは行レプリできません。 そこでproduction-db と staging-dbの間に1台コンバーターを挟んで、そこで行レプリ用のバイナリログを出力するようにしています。

f:id:adorechic:20141002194340p:plain

テーブル定義のコンフリクト

行レプリを使って、レコードの更新がコンフリクトしないようにすることができました。 一方でカラムが削除されて開発環境にはなかったり、カラム名が変更されたりして本番と開発環境でテーブル定義が異なっていると行レプリといえどもさすがにレプリケーションできません。

しかし本番と開発環境でテーブル定義が異なる状態は通常一時的なものです。もし長期間テーブル定義が異なったままの状態であるとしたらそれは健全な状態ではありません。 そこでその一時については目をつぶって、コンフリクトした場合はその更新をスキップするようにしています。

具体的にはstaging-db監視デーモンがレプリケーション状態を常に監視しており、もしレプリケーションが停止したらそのクエリをスキップして再開させるようになっています。

MySQLならslave_skip_errorsでよいのでは?と思った方もいらっしゃるでしょう。実はslave_skip_errorsではスキップできないケースがあるのです。行レプリを使っている場合に発生する1677エラーなどがそれです。というか監視デーモンの目的はほぼこの1677エラーをスキップするのが目的です。

セキュリティ

開発環境のデータベースはテーブル追加など開発者が自由に行えるようになっています。ということは強めの権限が開放されているということです。 ローカルだけで閉じていれば問題ないのですが、本番のデータが入ってくるとなると気になるのはセキュアな情報です。

クリティカルな個人情報はそれを扱う専用のバックエンドシステムがあるのですが、そこまでではないがセキュアなデータに関しては社内で命名規則があり、 それに該当するデータは自動でフィルタリングされstaging-dbにはレプリケーションされないようになっています。

なお余談ですが、社内ではこの命名規則に従っていればログ出力などが自動でフィルターされるgemが利用されています。

まとめ

クックパッドにおける開発環境構成と、開発用データベースを本番データベースと同期する方法について紹介しました。 本番環境にできるだけ近づけることで、よりユーザーに近い目線で開発することができます。

本番構成や開発フローについてはよく紹介にあがりますが、こういった開発環境の構成は意外と情報が少なかったりするので、参考となれば幸いです。

クックパッドでのユーザ調査

$
0
0

こんにちは、ユーザファースト推進部デザイングループの長野です。

今回は、クックパッドで定期的に行っているユーザ調査について、下記の流れでご紹介してみたいと思います。

  1. なぜ調査するのか
  2. どのような調査をしているか
  3. 調査結果の記録と共有の方法
  4. 実際のサービスに活かされた事例

1. なぜ調査するのか

クックパッドでのものづくりはすべて、「誰のどんな課題を解決するのか」を明確に定義することから始まります。そのためには、対象となる「人」への理解が不可欠であり、ユーザ調査はその手段です。

現在クックパッドでは、レシピ検索だけでなく生活全般へと事業領域が広がってきており、提供するサービスが対象とする「人」の生活や利用シーンの幅も、ますます多様化しています。それにともなって、様々なタイプの人の生活を理解することが必要とされてきており、ユーザ調査を活用する意味も、より強まってきていると感じています。

2. どのような調査をしているか

ユーザ調査の手法には様々なものがあると思いますが、クックパッドで最近よく行っているのはコンテキストインタビューという手法です。

コンテキストインタビューでは、相手に意見や要望を聞くことはせず、その人の普段の生活やそこで行う実際の作業の様子をありのままに教えてもらいます。「教えてもらう」というのが大切で、相手の話を先回りしたり、間違いを指摘したりしないように気をつけながら話を聞きます。相手の普段の生活を具体的にイメージできるくらいまで掘り下げるために、インタビューの進め方でもいくつか工夫をしています。

1日の流れを時系列に聞いていく

相手は私たちがクックパッドを作っている人だというのを知っているので、普段の生活全般の話を聞かせてほしいと言っても、どうしても料理に関する部分だけを取り出して語られてしまうことが多いです。 でも実際には、朝のランニングの時間や通勤電車の中でしていることに課題があるかもしれません。 そのため、最近のインタビューでは「朝起きてからの生活を順を追って聞かせてください」という風に、時系列で話を聞いていく形式にしています。そうすると、自然と今日や昨日の生活を思い出しながら話をしてくれるようになるので、生活の全体像がより掴みやすくなります。

実際の作業をやって見せてもらう

「クックパッドを使ってます」とひとことに言っても、私たちが予想していない使い方をされていることが多々あります。本人は毎日当たり前のようにやっているので、質問しただけでは出てこないような工夫や使い方が、実際の作業を観察すると発見できます。 たとえば、スマートフォンにクックパッドアプリを入れているのに、まずはブラウザから検索して、良さそうなレシピを見つけたらIDをメモして、今度はアプリを起ち上げて、IDで再検索をして...など。予想しない使い方にも必ず理由があり、それこそがサービスの改善や新サービスのアイデアにつながることも多いです。

3. 調査結果の記録と共有の方法

クックパッドではこのようなユーザ調査を、ほぼ毎月10名くらいを対象に実施しています。 調査結果は、インタビューに参加していなかった人もサービスづくりの参考にできるよう記録・共有するようにしています。

生の声をそのまま書き起こす

まずは一次情報として、できるだけ会話の内容を要約をせず、生の声をそのまま文章にします。これは、記録した人の主観で要約されることによって、他の人の視点からみたときにヒントになり得る情報が抜け落ちないようにするためです。出来るだけリアルな言葉で書き起こされていれば、あとから読み返すことでインタビューを追体験することができます。

ユーザタイプを分類する

集まったインタビュー記録は、傾向の似通ったユーザをまとめて、いくつかのユーザタイプに分類します。 分類の指標として、サービスの利用形態に影響がありそうな6つの項目を設定しており、各項目についてインタビュー結果を5段階で重み付けします。それをレーダーチャートに起こし、出来たグラフの傾向から分類を決めます。

  • インタビュー結果の重み付けをした例

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

各タイプの人物像を描く

ユーザ像はよりリアルにイメージしやすく描くことが社内のメンバーと共有する上で大切です。ユーザタイプに特徴的な事柄をインタビューから抜き出し、その人達の生活をイメージできる情報として、まとめるようにしています。

  • 人物像の例

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

4. 実際のサービスに活かされた事例

上記のような調査で得られた知見が実際のプロジェクトに活かされた事例をいくつか紹介したいと思います。

スマートフォンサイトの検索結果リニューアル

つい先日、クックパッドではスマートフォンサイトのレシピ検索結果をリニューアルしました。 採用されたUIデザインには、ユーザ調査から得られた気づきのいくつかが実際に反映されています。

写真が引き立つレイアウト
  • リニューアル前のUI(左)とリニューアル後のUI(右)

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

実際にレシピ検索をしてもらうと、多くの人が、まず出てきた大量のレシピをざーっと眺め、そこから気になるものをピックアップしてより詳しく情報を見ていくという行動をします。この「ざーっと眺める」という部分を観察していると、明らかに文章を読むようなスピードではなく、写真と気になるワードのいくつかをざっと見てすぐに判断していることがわかります。 そうした視点でみると、以前のレイアウトは写真が正方形で小さく、どちらかというと文字情報の方が目に入ってきやすいデザインになっていました。そこで、リニューアル後は無駄な余白を削って写真を大きく扱い、ぱっと見でも気になるレシピを的確に選ぶことができるレイアウトを採用しました。

材料表示の重要性

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

リニューアル前も後も、クックパッドの検索結果ではレシピの材料を表示しています。 この背景には、「家にあるもので作れるかどうか」がレシピ選択の判断基準になるという仮説があるのですが、今回のリニューアルではその仮説の検証も含め、材料表示の必要性を再検討しました。

インタビューをしてみると、やはり多くのユーザがその日の料理のためだけに買い物に行くことは少なく、家にあるものを使って料理をしたいと思う状況はとても多いということは確信できました。さらに、材料表示を見て「料理の味を想像できる」ということが、レシピ決定に重要な役割を果たしていることも発見できました。「家にあるもので作れるかどうか」だけでなく「気分や好みに合うかどうか」を判断するためにも、材料表示が機能することを再認識しました。

MYフォルダ機能の表示

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

気に入ったレシピを保存するためのMYフォルダ機能への導線が、今回のリニューアルから検索結果にも表示されるようになりました。その理由として、ユーザ調査から得られた2つの気づきがあります。

一つ目は、機能の認知不足。インタビューをしていると、頻繁にクックパッドを使っているのに、MYフォルダを知らないという人が予想より多いことが感じられました。気に入ったレシピを保存したいと思っても、機能に気付かず、印刷をしたり、ブックマークをしたり、自分のメールに送ったりしているのです。「気に入ったレシピを保存したい」人たちに、機能を適切に届けられていないという課題がみつかったので、頻繁に利用される検索結果にMYフォルダのUIを表示することにしました。

二つ目は、保存したけど忘れられてしまうレシピの存在。機能を知らない人がいる一方で、MYフォルダのヘビーユーザは数千件のレシピを保存していることもあります。その人たちの課題として、レシピを保存したこと忘れてしまい、毎回新たに検索をしてしまうということがありました。リニューアル後のUIでは、保存しているレシピのフォルダアイコンにはっきりと色が付くので、レシピを見逃さずに済み、より効率の良いレシピ決定をすることができます。

新機能・新サービス企画時の方向修正

ユーザは料理中にレシピを見ていなかった

以前、レシピページを調理中にもっと見やすくする機能の企画があがったことがありました。その際、最初は企画のターゲットとして、料理頻度が高い主婦のようなユーザ像を置いていました。しかし、実際にそのようなユーザの話を聞いてみると、実は調理中にレシピをほとんど見ていないことがわかりました。ある程度の料理スキルがある彼女たちは、大まかな作り方と味付けのイメージが出来れば、料理が作れてしまうのです。このような事実から、調理中にレシピを見やすくするという機能は、もっと他のユーザ層(例えば料理初心者)だったり、他の利用シーン(例えばお菓子作りなど)にフォーカスして作ったほうが良さそうだということがわかってきました。企画の方向性を考える上でユーザー調査が役立ったわかりやすい事例でした。

健康意識の高まりと年齢は必ずしも相関しない

現在クックパッドでは、「健康レシピ」や「ダイエット」など健康を意識した食生活を応援するサービスをいくつか展開しています。 特に「健康レシピ」では、高血圧や糖尿病など、高齢になるほどリスクが高まる病気を予防する食事をテーマに扱っているため、漠然とターゲット層はシニア層であるというイメージがありました。しかし、実際にシニア層のユーザにインタビューをしてみると、特に自分の健康に問題がない状態の場合、年齢を重ねたからといって健康意識が高まるとは必ずしも言い難いという現実がありました。むしろ、元気なうちに好きなことをやって好きなものを食べたいという気持ちが高まる傾向もみられ、単純にシニア層とひとくくりにしてしまうことも危険だと感じました。逆に、若くても身近に病気をもった家族がいたりすれば意識はぐっと高まるので、ユーザを取り巻く環境などもきちんと見据えた上でターゲットユーザを理解することが大事だと感じされられた事例でした。

まとめ

以上、最近のクックパッドで行っているユーザ調査の概要と、それがどのようにサービスに活かされているかについて、ご紹介しました。

ユーザ調査は実践するまでは、ちょっと面倒で億劫に感じることもあるかもしれません。でも、実際にやってみると何度やっても必ず発見があり、本当に面白いと思います。

クックパッドでは、デザイナーでもエンジニアでも、サービスをつくる人は誰でもユーザ調査に参加します。このエントリを通して、ユーザ理解から始まるクックパッドのものづくりの様子が少しでも伝われば幸いです。

アプリ開発の品質底上げ施策をWebhooksでBotが支援する世界

$
0
0

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

主にモバイルアプリ開発において、数ヶ月前よりGitHubのWebhooksを使ったとある取り組みを始めました。HipChatやSlackなどをはじめとした様々なサービスとの連携サービスを提供しているGitHubですが、Webhooksの機能を使うことで、より自分たちの開発を支援する未来を創造できればと思います。以降では、実際の使用例、その実装よりな話しへと話しをすすめます。

クックパッドにおけるWebhooksの使用例

チェックリストによるセルフチェックをPR時に実施する

モバイルアプリ開発はWebアプリ開発と異なる点が多々あります。例えば、開発対象の面では端末の多様性、端末システム側設定・通信状態の多様さなど、リリースの面ではデプロイに対する制限や更新がユーザ依存であることなど、です。そのため、当たり前品質の底上げのために不具合の作り込みを減らす仕組みが必要でした。

当たり前品質の底上げのために、基本的な振る舞いの考慮漏れによる不具合の作り込みや、クラッシュに繋がる不具合の作り込みを防ぐことに焦点を絞り対策を考えました。対策の1つとして、確認する観点を絞ったチェックリストを作成し、変更をmasterにマージする前のPull Requestの段階で開発者がチェックできるようにしました。このチェックリストによるセルフチェックと、他開発者のレビューにより当たり前品質の底上げを図っています。

Pull Requestへのチェックリスト張りつけは、当初は利用者の反応を得て改善するために人手で行っていました。一方、開発がスケールすると人手による対応が追いつかなくなることは明らかでした。

Webhooksを使って機械的にチェックリストを応答する

開発のスケールに対応するため、GitHubのWebhooksを使うことでこのチェックリストの応答を機械に任せることにしました。具体的には、以下の順序で処理が行われます。

  1. 開発中のmasterブランチに対して開発者がPull Requestを出す
  2. Pull Requestを契機としたGitHubのWebhooksを受信したサーバが、そのPull Requestへのコメントとしてチェックリストを返す
  3. 他開発者のレビューと、チェックリストによるチェックを終えたPull Requestを開発者が自分でmasterにマージする

チェックリストもテンプレ化できる粒度まで落とし込みましたが、現段階のクライアントアプリの不具合状態を見る限りではある程度効果をだしているようです。蛇足ですが、このチェックリストにはもう一つ狙いがあります。それはリリース前検証の段階で、テストエンジニアが探索的に障害度の高い不具合を見つけていく為のきっかけを作ることです。リリースまでに多くの時間を検証に割ける訳ではないので、検証する側としてもあてをつけて探索的に検証を進めることは重要な要素ですね。

GitHubによるチェックリストの応答例

f:id:kazucocoa:20141007134018p:plain

チェックを終え、マージを無事行ったらそれに合わせてイイネもしてくれます。

f:id:kazucocoa:20141007134021p:plain

GitHub Webhooksの使い方

GitHub Webhooksとは

Webhooksを設定したリポジトリに対する操作を契機に、設定したURLに対してHTTPリクエストを送信するGitHubのサービスです。図1や図2のようにGUIからWebhooksを有効にすることで利用可能になります。Webhooksに関する詳細な説明は公式ドキュメントを参照ください。WebhooksはGitHub/GitHub Enteroriseのいずれにおいても利用可能です。ただし、古いバージョンのGitHub Enterpriseでは、GUIではなくWeb API経由によるWebhooksの設定が必要になるかもしれません。

f:id:kazucocoa:20141002161746p:plain

図1:Webhooksの設定画面

f:id:kazucocoa:20141002161758p:plain

図2:Webhooksの有効化

Pull Requestを契機にコメントを返す

先ほどのWebhooksの使用例のように、あるリポジトリに対するPull Requestを契機とするWebhooksに対して、そのリポジトリへ何らかのコメントを返すという流れを以下に記述してみます。

まずはGitHub側設定のWebhooksの設定において、HTTPリクエスト送信先サーバのURLを指定、pull_requestにチェックをし、WebhooksをActivateします。図1,2がその設定画面です。設定を終えたら、そのWebhooksを受け取るサーバに以下のような実装のサンプルを用意します。

require'sinatra'require'octokit'# https://github.com/octokit/octokit.rbrequire'hashie'

post '/hook_sample'do# Webhooksのヘッダに含まれる要素X-Github-Eventを取得する# 'pull_request'のような文字列が含まれている
  github_event = request.env['HTTP_X_GITHUB_EVENT']

  # WebhooksのJSON形式のペイロード(https://developer.github.com/v3/)を取得する
  req_body =  Hashie::Mash.new(JSON.parse(request.body.read))
  repository = req_body.repository.full_name

  # GitHub Web APIクライアントのOctokitのインスタンス作成# ACCESS_TOKEN: Web APIを使うさいに必要となるOAuth認証に使う文字列
  client = Octokit::Client.new access_token: ACCESS_TOKENcase github_event
  when'pull_request'# GitHub Web API 経由でコメントを送る
    client.add_comment(repository, req_body.pull_request.number, 'コメント')
  else# pull_request以外の処理endend

GitHub EnterpriseでOctokitを使う時に注意すべきこと

クックパッドでは、GitHub Enterpriseを使っています。GutHub Enterpriseのように操作したいGitHubのAPIが独自ホスト上にある場合、以下のようにnewする前にホスト名を指定する必要があります。

require'octokit'Octokit.api_endpoint = 'http://api.github.dev'Octokit.web_endpoint = 'http://github.dev'
client = Octokit::Client.new access_token: ACCESS_TOKEN

このようにすることで、以下のようにGitHub Enterpriseへアクセスするようになります。

puts client.api_endpoint # => 'http://api.github.dev/'
puts client.web_endpoint # => 'http://github.dev/'

Pull Requestへの応答以外にも

Webhooksを調べると、issueが作成されたりissueに対してコメントがつけられたことを契機に、そのissueに記載された文字列を読んで所定の文言をコメントするという流れも実現することができます。これにより、例えばWebhooksを設定しているリポジトリのissueにおいて、please comment meとすると、そのissueに対して機械的にコメントを返すことができるようになります。

GitHubにBotが生息し、開発を補助する

HubotのようなBotがチャットツール上で意思を持つかのように開発プロセスを補助するChatOpsという言葉が存在するように、GitHub上においてもBotが開発を補助しているような形にみえてきました。どこまで任せるかなどの考慮が入ると思いますが、いろいろBotに作業の補助を任せるような未来を実現できそうですね。

最後に

GitHubのWebhooksのように、様々なサービスをつなぎ合わせ、各々の目的や体制に見合ったプロセスの構築を支援する手段は巷にあふれています。今回の実例は、品質の底上げを目的とした、不具合作り込みを避けるための気づきを開発の一連のプロセスの中に組み込んでいく一例でした。一方で、様々な用途への未来へ進むことができそうな連携だったのではないでしょうか。

ところで、このような取り組みはテストエンジニアとしての改善活動のひとつの側面でした。皆さんの近くにいますテストエンジニアは、品質をより向上させるためにどのような創造的な取り組みをしていますか?色々な取り組みを聞いてみたいものですね。

オリジナルフォントを使ったデザイン

$
0
0

こんにちは、ユーザファースト推進部デザイングループの元山です。

デザイナーの皆さんはWebやアプリなどをデザインする上でフォントを作った事があるでしょうか? ずいぶんと前から「これからはWebフォントの時代だ」なんて言われながらも、現実は中々使うのが難しいなぁと感じている方は多いかもしれません。 今回はクックパッドで実際に作ったオリジナルフォントを使ったWebやアプリのデザインについて事例を交えながら紹介してみたいと思います。

クックパッドでの事例

印象と機能を向上させるデザインのためのフォント

ブラウザやアプリの標準のフォントではない特殊なフォントを使う理由として真っ先にあげられるのは、やはり文字の雰囲気や見た目でデザイン的な印象や見え方を向上することができる点だと思います。最近ではAppleのWebサイトもオリジナルのWebFontを使ったデザインに変わりましたね。

クックパッドでは特売情報という近所のスーパーの特売・チラシ情報が見れるというサービスを運営しています。特売情報では「お値打ちだ!」「見ていて楽しい!」とより感じてもらえるように実際のチラシのエッセンスを取り入れた雰囲気の見た目にしていて、そのデザインに一役買っているのが価格表示のところに使っているオリジナルの特売情報専用フォントです。

特売情報専用のフォントを作るときに考えたこと

実は当初、特売情報のスマートフォンのサイトデザインはスクロールして見やすいようにリスト形式で構成されており、価格の表示部分も商品名の下に標準フォントで小さく表示されているだけでした。しかしその後、アプリケーションへの展開などを考える段階でチラシ情報を見るにあたってどのようなデザインが良いのか再度検討しました。その時の結論として考えたことは、ただ情報が見やすいではなく「お買い物がしたいと思えるような雰囲気作り」が必要だということと、実際のチラシを見る自分の目線を客観的にたどっていて気づいた「写真と価格で流し見ができるレイアウト」が重要だということでした。 過去(左)と現在(右)で見比べてみると全体の雰囲気や見え方の違いで大きく変わっていることが分ると思います。

f:id:kudakurage:20141010151737j:plain

このように、よりお買い物がしたくなる雰囲気のお値打ち感を感じるオリジナルフォントを作成し使用しています。写真と価格で流し見ができるように写真は大きくし、価格はその上にのせて写真に埋もれてしまわないように強めのweightにしています。

古いブラウザやマルチプラットフォームでもOK

さらにこれらを画像ではなくフォントを作って実現したことによって、PCサイト・スマートフォンサイト・iOS/Androidアプリとマルチプラットフォームで簡単に同じように展開できるようになりました。

f:id:kudakurage:20141010151750j:plain

WebフォントはIE5.5でも使用できる(eot形式)など割りと古いブラウザからサポートされている技術でもあり、もちろんスマートフォンなども含めたモダンブラウザでも利用できるので実はかなり現実的な手段です。 よくあるWebフォントの抱える問題として、日本語のフォントはファイルサイズが大きくなりページ読み込みの負荷になりがちという話がありますが、例えば特売情報の価格表示のような限定的な利用であれば必要なグリフの数は少なく済むため、問題なくクリアできると思います。

Scalable, Reusableなシンボルフォント

もう一つクックパッドで使われているオリジナルフォントの事例として、iOSアプリで使用されているシンボルフォントがあります。

f:id:kudakurage:20141010151806j:plain

こちらは以前の「iOSアプリデザインリニューアルの舞台裏」という記事でも少し紹介されていました。CookpadのiOSアプリのデザインを見直すにあたりよりコンテンツを強調したUIに変更し、極力装飾のないシルエットのアイコンに変更したため、ほとんどのアイコンをシンボルフォントを使って表示しています。

f:id:kudakurage:20141010151815j:plain

最近では様々な解像度のデバイスも増えてきているので、2倍・3倍・4倍の大きさの画像も用意する必要があったり実に大変です。その点シンボルフォントはベクターデータであるためどの解像度でもスケーラブルに対応でき、わざわざ画像を書き出したりする手間を大幅に減らすことができます。さらに、今使用しているシンボルフォントは50KB程度と画像で用意するよりも圧倒的に軽量で、アプリのサイズ自体も大きく減らすことができ、一石二鳥です。

Prototypeにもかんたんに使える

アプリやサイトを作るにあたってよく使われるようなモチーフもシンボルフォントは多く含んでいますので、機能の追加や改善、新しいアプリのプロトタイプづくりにもエンジニアが簡単に使えて、クックパッドらしさといった雰囲気も統一されるので大変役立っています。 ちなみに、PhotoshopやIllustrator、Sketch、Keynoteといったグラフィックやワイヤーを描くツールにも使用できるので、手軽にアイコンを呼び出せてスピーディーに画面モックなどを作成できて便利です。

f:id:kudakurage:20141010151828j:plain

こちらはまだiOSアプリのみでしか利用されていませんが、スマートフォンサイトやAndroidアプリでも簡単に利用できるようにしたいと考えています。

フォントの作り方

今回ご紹介した特売情報専用フォントとシンボルフォントの作り方についても簡単にご紹介したいと思います。

手軽に作れる簡単なフォントの作り方

基本的にはどちらも元となるグリフのパスデータをAdobe Illustratorを使って作成しました。 特売情報専用フォントの場合は価格表示に使うメイン数字だけを主に作成し、それ以外のアルファベットや一部かな文字・漢字はM+ FONTSをベースに利用させていただき、一部修正して作成しました。M+ FONTSは商業目的での利用・フォント内容の改変・再配布にも制限の無い自由なライセンスで公開されているフォントです。 フォントとしてアウトプットするのはGlyphs MiniというMacのアプリケーションを使用しました。予め作っておいたパスデータをGlyphs Miniにコピー&ペストし、それぞれの文字間が均等に見えるように文字の余白などを調整しフォントファイルを生成しました。Glyphs Miniは有償のフォント作成アプリケーションの中では格段に安く、使いやすいので一度フォントを自分で作ってみたいという方にはおすすめのアプリケーションです。

f:id:kudakurage:20141010151840j:plain

細かいポイントとして、半角数字は仮名文字より少し小さくするのが普通ですが、特売情報専用フォントではあえて同じサイズ感で表示するようにして、調整せずにただ平打ちしただけでも良い感じに見えるようにしています。

f:id:kudakurage:20141010151849p:plain

Ligature機能を使ったシンボルフォントの作り方

シンボルフォントの方もまずは同じ大きさ感でシンボルのパスデータを作るところから始めました。私の場合はfont作成アプリの方ではパスの編集はしなかったので、この時点で全体の統一感を合わせて調整したりしています。 シンボルフォントではLigature(合字)という機能も使っています。Ligatureとは複数のグリフをまとめて1つのグリフとして表示する機能で、主にアルファベットで用いられているものです。これを利用することで、ある程度意味的な文字列でシンボルを呼び出すことができるのでとても便利です。(cameraと打つとカメラのシンボルが表示されるなど) Ligatureについては以前にLTで話した↓以下のスライドも参考になると思います。

Ligatureの機能をうまく使ってフォントを作成するには、Glyphs miniだけでは難しいため、オープンソースで提供されているFontForgeを使用して作成しました。FontForgeはオープンソースな上でかなり高機能なアプリケーションですが、その分少し使いこなすのが難しいところもあります。(なので、その使い方については省きます)

最近ではシンボルフォントを簡単に作れるWebサービスがいくつかあり、中でもIcoMoonというサービスはSVG形式で作ったオリジナルのシンボルをフォントにできたり、サイズやフォント名、ユニコードやLigatureまで設定してフォントを生成できるのでとても便利です。簡単にシンボルフォントを作ってみたいという方はぜひこちらをおすすめします。

まとめ

今回はクックパッドで実際に使用されている2つのオリジナルフォントの使い方についてご紹介しましたが、フォントを使った広い意味でのデザイン的な解決方法はまだまだあると思います。フォントという形式にすることによってデザイナーだけでなくエンジニアや他の職種の人など、誰もが扱えるツールとして提供できることは大規模な人数の開発になるほど効果のあることだと思います。 オリジナルフォントだけでなく、こんな技術を使えばもっとたくさんのことをデザイン的に解決できるよ!わたしはデザイナーとしてこんな方法で開発にコミットしているよ!というようなことがあればぜひぜひ教えてください。デザイナーだからこそできる新しい開発のスタイルみたいなものがもっともっと増えて盛り上がっていったら嬉しいです。


Android Studioに追加されたGoogle App Engineテンプレートを試そう 実装編

$
0
0

モバイルファースト室の@sys1yagiです。

Android Studioに追加されたGoogle App Engineテンプレートを試そう 導入編の続きです。今回はCloud Endpointsのテンプレートを使ってAndroid Studio上でTodoアプリを作る例を解説します。

Google App Engineテンプレートの利点

Androidアプリケーションの開発においてGoogle App Engine(以下、GAE)テンプレートの利用には以下の利点があると考えられます。

  • Android Studioでバックエンドも同時に開発できる
  • GAE関連の依存性をGradleで管理できる
  • バックエンドとフロントエンド間のインタフェースの実装が省略できる
  • バックエンドのモデル変更がすぐにフロントエンドに反映されるので迅速なプロトタイピングができる
  • バックエンドはGAEなのでそのまま運用が可能
  • クライアントライブラリでエンドポイントのURLやパスを変更したり、HttpRequestInitializerをセットしてカスタムした通信処理が可能になる。これによりバックエンドの切り替えも可能となる

Google App Engineテンプレートは「バックエンドに何を使うか決まっていない」、「MBaaSでは要件を満たせない」といったケースや「バックエンドを含めてプロトタイピングしたい」といったケースなどに非常に有効な選択肢なのではないかと思います。

Todoアプリケーションを作る

Cloud EndpointsはGAEで動作するので、バックエンドの詳細な作り込みについてはGAEアプリケーションと差がありません。ですのでGAEに関する部分の解説等は省略します。またAndroid側についてもCloud Endpointsのクライアントライブラリを利用する部分以外については省略します。

プロジェクト構成

プロジェクトは以下の構成とします。"app"がAndroidアプリケーションのモジュール、"api"がCloud Endpointsのモジュールです。

f:id:sys1yagi:20140912105217p:plain

TODOのモデル

Todoは以下の構造を用います(と言っても直接JSONを触ることはありません)。

{
  "id": long,
  "text": string,
  "created": long,
  "updated": long,
  "checked": boolean,
}

Objectifyを使ってモデルを定義する

まずはapi側にモデルを定義し、永続化の準備をしましょう。モデルの永続化についてはGAE向けに既に色々なライブラリが存在します。今回はObjectifyを利用します。

Objectifyはbuild.gradleのdependenciesに以下の定義を追加すればOKです。

compile 'com.googlecode.objectify:objectify:5.0.3'

Todoクラスを作成し、Objectifyのアノテーションを使ってモデルを定義します。少し長いように見えますが、フィールドを定義してgetter/setterを生成すればすぐに出来上がります。最下部のcreateTodo()はヘルパーメソッドなのであっても無くても良いです。Objectifyによるモデル定義の詳細についてはEntities - objectify-appengineを参照してください。

package com.sys1yagi.hellocloudendpoints.api;

import com.googlecode.objectify.annotation.Cache;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Index;

import java.util.Date;

@Entity@Cachepublicclass Todo {

  @Id
  Long id;
  private String text;
  privatelong created;
  @Indexprivatelong updated;
  @Indexprivateboolean isChecked;

  public Long getId() {
    return id;
  }
  publicvoid setId(Long id) {
    this.id = id;
  }
  public String getText() {
    return text;
  }
  publicvoid setText(String text) {
    this.text = text;
  }
  publiclong getCreated() {
    return created;
  }
  publicvoid setCreated(long created) {
    this.created = created;
  }
  publiclong getUpdated() {
    return updated;
  }
  publicvoid setUpdated(long updated) {
    this.updated = updated;
  }
  publicboolean isChecked() {
    return isChecked;
  }
  publicvoid setChecked(boolean isChecked) {
    this.isChecked = isChecked;
  }
  publicstatic Todo createTodo() {
    long date = new Date().getTime();
    Todo todo = new Todo();
    todo.setCreated(date);
    todo.setUpdated(date);
    todo.setChecked(false);

    return todo;
  }
}

エンドポイントを実装する

あとはエンドポイントの実装です。Objectify用の実装も入っているので少しややこしいですが、それぞれ解説します。

package com.sys1yagi.hellocloudendpoints.api;

import com.google.api.server.spi.config.Api;
import com.google.api.server.spi.config.ApiMethod;
import com.google.api.server.spi.config.ApiNamespace;

import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyService;

import java.util.Date;
import java.util.List;

import static com.googlecode.objectify.ObjectifyService.ofy;

@Api(name = "todoApi", version = "v1",
    namespace = @ApiNamespace(ownerDomain = "api.hellocloudendpoints.sys1yagi.com",
        ownerName = "api.hellocloudendpoints.sys1yagi.com", packagePath = ""))
publicclass TodoEndpoint {

  static {
    ObjectifyService.register(Todo.class);
  }

  @ApiMethod(name = "list", httpMethod = "get")
  public List<Todo> list() {
    return ofy().load().type(Todo.class).list();
  }

  @ApiMethod(name = "add", httpMethod = "post")
  public Todo add(Todo todo) {
    todo.setCreated(new Date().getTime());
    return update(todo);
  }

  @ApiMethod(name = "update", httpMethod = "post")
  public Todo update(Todo todo) {
    todo.setUpdated(new Date().getTime());
    Key<Todo> key = ofy().save().entity(todo).now();
    return ofy().load().type(Todo.class).id(key.getId()).now();
  }

  @ApiMethod(name = "delete", httpMethod = "post")
  publicvoid delete(Todo todo) {
    ofy().delete().entity(todo).now();
  }
}

Objectifyのコード

Objectifyを利用するには、ObjectifyServiceクラスのregisterメソッドでモデルをセットする必要があります。今回はTodoEndpointクラスのstaticイニシャライザで以下の様にTodoクラスをセットしています。

static {
  ObjectifyService.register(Todo.class);
}

Objectifyでのモデルの操作はObjectifyクラスを用います。ObjectifyクラスはObjectifyServiceクラスのofyメソッドで取り出せます。ofyメソッドをstatic importしておくと楽です。

import static com.googlecode.objectify.ObjectifyService.ofy;

ofy().load().type(Todo.class).list();
ofy().save().entity(todo).now();
ofy().delete().entity(todo).now();

エンドポイントのコード

エンドポイント側のコードについては前回解説した以上のことはあまりありません。

READ系のエンドポイントは単純にモデルを読み込んで返却すれば、自動的にJSON形式のレスポンスが構築されます。

@ApiMethod(name = "list", httpMethod = "get")
public List<Todo> list() {
  return ofy().load().type(Todo.class).list();
}

上記コードによって以下の様なJSONを返します。

{
 "items": [
  {
   "id": "5639445604728832",
   "text": "出張日程の調整",
   "created": "0",
   "updated": "1411609163221",
   "checked": false,
   "kind": "todoApi#resourcesItem"
  },
  ...
 ]
}

エンドポイントのhttpMethodをpostやputにすると、モデルをそのまま引数に受け取るスタイルで記述できます。これによりクライアント側で、「モデルのオブジェクトを渡してリクエストする」という操作が可能になります。

@ApiMethod(name = "update", httpMethod = "put")
public Todo update(Todo todo) {
  todo.setUpdated(new Date().getTime());
  Key<Todo> key = ofy().save().entity(todo).now();
  return ofy().load().type(Todo.class).id(key.getId()).now();
}

また、エンドポイントとして実装したメソッドを通常のメソッドと同じように使うこともできます。以下の実装ではaddメソッド内でupdateメソッドを呼び出して永続化の処理を共通化しています。

@ApiMethod(name = "add", httpMethod = "post")
public Todo add(Todo todo) {
  todo.setCreated(new Date().getTime());
  return update(todo);
}

クライアント側を実装する

次はクライアントの実装です。クライアントライブラリが自動的に生成されるので、そのライブラリを使ってエンドポイントにアクセスするだけです。

TodoApiクラスを管理するクラスを作る

ライブラリを使ってエンドポイントにアクセスするだけと言ってもいくらかの準備が必要です。エンドポイントにアクセスする為のTodoApiクラスを毎回生成するのは無駄なので、Singletonにしておきます。

import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.extensions.android.json.AndroidJsonFactory;

import com.sys1yagi.hellocloudendpoints.api.todoApi.TodoApi;

publicclass ApiClient {
  privatestaticfinal TodoApi INSTANCE = getApiClient();

  publicstatic TodoApi getInstance() {
    return INSTANCE;
  }

  privatestatic TodoApi getApiClient() {
    returnnew TodoApi.Builder(AndroidHttp.newCompatibleTransport(),
        new AndroidJsonFactory(), null)
        //for genymotion.//if you use android emulator, you should replace to "10.0.2.2".//.setRootUrl("http://10.0.3.2:8080/_ah/api/")
        .build();
  }
}

エンドポイントへのアクセスをAsyncTaskで包む

エンドポイントへアクセスするメソッドはブロッキングされるので、AsyncTaskなどで包む必要があります。今回は操作毎にAsyncTaskを定義しています。

以下はTodo一覧の取得の例です。エンドポイントではList<Todo>を返していましたがクライアント側ではTodoCollectionクラスを受け取ります。TodoCollection.getItems()List<Todo>を取り出せます。

import com.sys1yagi.hellocloudendpoints.api.todoApi.model.TodoCollection;
import android.os.AsyncTask;
import java.io.IOException;

publicclass ListTask extends AsyncTask<Void, Void, TodoCollection> {

  @Overrideprotected TodoCollection doInBackground(Void... params) {
    try {
      return ApiClient.getInstance().list().execute();
    } catch (IOException e) {
      returnnull;
    }
  }
}

Todoの追加はaddメソッドにTodoクラスのインスタンスを渡すだけです。エンドポイント側でモデルを受け取るスタイルにしておけばクライアント側で非常にシンプルに記述できます。

import com.sys1yagi.hellocloudendpoints.api.todoApi.model.Todo;
import android.os.AsyncTask;
import java.io.IOException;

publicclass AddTask extends AsyncTask<Todo, Void, Todo> {

  @Overrideprotected Todo doInBackground(Todo... params) {
    Todo todo = params[0];
    try {
      return ApiClient.getInstance().add(todo).execute();
    } catch (IOException e) {
      returnnull;
    }
  }
}

デプロイする

バックエンド、クライアント共に準備が整いました。実装したバックエンドをデプロイしましょう。

デプロイするにはGoogle Developers ConsoleでプロジェクトIDを作成する必要があります。Google Cloud Platformのページの「今すぐ試す」などからプロジェクトIDを作成のフローに遷移できるのでお試し下さい。

プロジェクトIDを作成したらapiモジュールのsrc/main/webapp/WEB-INF/appengine-web.xmlのapplication要素にプロジェクトIDを記述します。

f:id:sys1yagi:20141016182002j:plain

あとはgradleで以下のタスクを実行すればデプロイできます(初回は認証処理が挟まります)。

./gradlew appengineUpdateAll

デプロイがうまくいけばアプリケーションが動作するようになるはずです。

f:id:sys1yagi:20141016181959j:plain

APIs Explorerでデプロイしたエンドポイントを確認する

デプロイ後はGoogle Developers ConsoleでGAEアプリケーションとして管理できます。また、APIs ExplorerでエンドポイントをWeb上で操作できるようになります。

以下のURLのyour-project-idをプロジェクトIDに置き換えると、定義したエンドポイントの一覧が見れます。

https://apis-explorer.appspot.com/apis-explorer/?base=https://your-project-id.appspot.com/_ah/api#p/

f:id:sys1yagi:20141016182007j:plain

特定のエンドポイントを選択し、リクエストを投げる事もできます。

f:id:sys1yagi:20141016182013j:plain

実際に返却されるJSONを見ることも出来るのでトラブルシューティングに役立ちます。

f:id:sys1yagi:20141016182016j:plain

まとめ

駆け足でしたがCloud Endpointsを使ってTodoアプリを作る方法について解説しました。簡単にバックエンドを含めて開発できる事がわかったかと思います。詳細な実装についてはhttps://github.com/sys1yagi/TodoCloudEndpointsを参照して下さい。

クックパッドの検索の裏側

$
0
0

初めまして、インフラストラクチャー部の加藤 (@EugeneK) です。

クックパッドでは現在178万ものレシピが公開されていますが、目的のレシピを探すために検索機能を提供しています。 今回は検索機能の裏側の仕組みについて、インフラストラクチャーの観点からお話ししようと思います。

全ての検索機能を支えるSolrと周辺のアーキテクチャ

クックパッドにはレシピの検索だけでなく様々な検索機能がありますが、その全てはSolrを活用して実装されています。 以前はMySQL Tritonnによる全文検索機能を使用していましたが、2011年頃からSolrに切り替わりました。

クックパッドではSolrをマスタ - スレーブ構成にすることで冗長性と負荷分散を実現しています。以下の構成図をご覧ください。

f:id:EugeneKato:20141021205109p:plain

マスタとスレーブの間には、リピータと呼ばれる検索インデックスを中継するためだけの役割のサーバがいます。このサーバを介することで、多数のスレーブが同時にマスタから検索インデックスを受け取ってもマスタが高負荷にならないようにしています。 また、クックパッドのサービスが稼働するアプリケーションサーバはスレーブのみを参照するので、間にバランサを挟むことで冗長性と負荷分散を同時に実現しています。

スレーブのグルーピング

クックパッドの検索インデックスは1つではなく、複数の検索インデックスがあります。 一番よく使われるものはレシピのインデックスですが、他にもつくれぽやユーザ等があり、変わったところでは検索窓に文字を入力したときに検索語の候補を表示するためのオートコンプリート用の検索インデックスがあります。

クックパッドではSolrのマルチコアという機能を利用して、一つのSolrサーバで複数のインデックスを運用しています。 複数の検索インデックスを使用することで、サービスの機能ごとに検索インデックスを使い分けることができるようになっています。

ところが、全ての検索インデックスの使われ方は均一ではありません。 均一ではないことから、インデックスごとに負荷の偏りが生じます。

検索を実行するときにかかる負荷は主に以下の三つの要素で決まります。

  • インデックス自体の大きさ (メモリの大きさが重要)
  • クエリ自体の複雑さ (高速なCPUが必要)
  • クエリが実行される回数 (多くの台数のサーバが必要)

クックパッドではこの三つの要素を踏まえ、スレーブをいくつかのグループに分けて、似たような特性のインデックスのみを持つスレーブ群を作成することで負荷の偏りに対応しています。 たとえば、レシピ検索のインデックスはデータサイズも大きく、実行されるクエリも複雑で、実行回数も多いため専用のグループにしています。 また、オートコンプリートのインデックスはデータサイズは小さく、実行されるクエリも簡単なのでユーザのインデックスと同居させたりしています。

検索クエリの特徴を捉える

クックパッドでは毎日多くの検索が行われています。「豚肉」や「カレー」といったよくある単語はもちろん、「老干媽」といった非常に珍しい単語で検索されることもあります。 もちろんどんな単語でも検索が行えるようになっていますが、よくある単語は非常に多くのユーザから何度もリクエストされます。 一方で珍しい単語は一日に数回検索されるかどうか、といった具合です。

検索回数の多い単語のバリエーションはある程度限られているため、検索結果のキャッシュを行うことでSolrの負荷を軽減しつつ高パフォーマンスを引き出すことが可能になります。

負荷の軽減と高パフォーマンスを出す仕組み

Solrには同じ検索クエリを受け付けたときの結果をキャッシュから返すクエリキャッシュの仕組みがありますが、クックパッドではSolrの前段に専用のキャッシュサーバを設置して、よりアグレッシブにクエリキャッシュを行っています。 クエリキャッシュを外部で行うことで、キャッシュ内容がサーバによって異なることで起きるフラグメント化(=非効率)を避けることが可能になります。 構成図でバランサと書いたサーバは、バランサであると同時にクエリキャッシュを行うサーバになっています。

さて、クエリキャッシュをSolrの外部で行うためにはどのような要件が必要でしょうか。 SolrはHTTPで通信を行うため、HTTPのリクエスト毎のレスポンスがキャッシュできれば、検索クエリ毎の検索結果をキャッシュするという要件を満たせそうです。

そこで、クックパッドではクエリキャッシュとバランシングを同時に行うサーバとしてVarnishを採用しました。 Varnishは高速なHTTPページキャッシュサーバとして知られていますし、同時にバランサの機能も内包しています。 このキャッシュサーバが検索クエリキャッシュとして機能することで、バックエンドとなるSolrへのリクエスト数の軽減と高速なレスポンスを実現しています。

また、前述のスレーブのグルーピングに関しても、検索リクエストのURLに含まれるインデックス名を元にして、バックエンドを切り替えています。

まとめ

クックパッドにおける検索の仕組みについて紹介しました。 検索で一番重要なことは、探しているものが見つかることですが、素早く見つけられることも重要です。 高速な検索システムを提供することで、ユーザが少しでも早く見つけられるように上記のような工夫をしています。

スマートフォンWebのフロントエンドを高速化する取り組み

$
0
0

ユーザファースト推進部の丸山(@h13i32maru)です。

先日「撮るレシピ」というサービスを cookpad.com にて公開しました。「撮るレシピ」というサービスは料理本や雑誌のレシピを写真に撮ってクックパッド上に保存できるというものです。料理本や雑誌でレシピを良く見る方はぜひ使ってみてください(Androidアプリ版もあります)。

f:id:h13i32maru:20141023094045p:plain

この「撮るレシピ」は全体公開前に一部のユーザに限定公開をしていました。そして全体公開をするにあたりフロント側のコードを全面的に書き換え高速化を行いました。その結果、最大で30倍高速化することができユーザの使い勝手が向上しました。以下が「書き換え前」と「書き換え後」の計測結果です(Android端末8機種 + iOS3機種で各操作のターンアラウンド時間*1を計測)。

  • 閲覧系
    • 最大: 30倍高速化(4.2秒→0.14秒)
    • 平均: 15.7倍高速化(3.6秒→0.25秒)
  • 更新系
    • 最大: 7倍高速化(2.6秒→0.4秒)
    • 平均: 3.7倍高速化(3.9秒→1.2秒)

というわけで、今回は高速化するために行った「SPAによる画面遷移」「写真のリサイズとローカル表示」「タップ処理の最適化」という内容についてご紹介します。

SPAによる画面遷移

SPA(Sigle Page Application)とは「1つのURLに複数の画面を紐付けて、ユーザ操作に応答して画面を動的に書き換えながらサーバとの通信を必要最低限にしたWebアプリケーション」というものです。ここ数年Web界隈では話題になっている技術です。SPAの「画面遷移時にサーバとの通信を必要としない(or 最小限)」「HTML/JavaScript/CSSの再評価が行われない(or 最小限)」という特徴により画面遷移や操作が高速化されます。

このSPAを実現するにはAngularJSなどのオールインワンなフレームワークを使ったり、Backbone.jsやjQueryなどのフレームワークを組み合わせて使ったりなど様々な方法があります。

画面管理

今回SPAを実現するために、Androidの画面管理を真似るという方法をとりました。Androidの画面管理は主に以下の4つから構成されています。

  • XMLからViewを作るInflate機能
  • 画面の生成、活性、停止、破棄を扱うライフサイクル機能
  • Intentによる画面間呼び出し機能
  • 画面の遷移スタックを管理する機能

これらの機能を簡易的にJavaScriptで実装してSPAを実現しました。実際にどのように書くのかはコードを見てもらったほうが早いと思うので、以下にサンプルコード(CoffeeScript)を示します。

# このサンプルコードではStartPageとSubPageという2つの画面があります。# StartPageからSubPageに遷移することができ、遷移するときに数値を渡すと、SubPage側で何らかの処理を行います。# SubPageを離れる(バックする)と処理を行った数値を呼び出し元の画面(StartPage)に渡します。# StartPageは渡された数値を使って画面を描画しなおします。# 画面のライフサイクルメソッドは以下のようになっています# onCreate -> onResumeBefore -> onResume -> onResumeBefore -> onPauseBefore -> onPause -> onPauseAfter -> onDestroyclassStartPageextendsBasePage# BasePageがすべての画面の基底クラス(AndroidのActivityに相当)num:nullonCreate:(data)-># 画面が生成された時に呼び出されるメソッド(AndroidのActivity#onCreateに相当)super(data)@num=0@inflateView('.start_page.template')# 元となるHTMLをクローンしてこの画面専用のViewを作る(AndroidのActivity#setContentViewに相当)@handleView()# Viewの各Elementに適切なハンドラ(click, changeなど)を設定する(Androidでボタンなどにリスナを設定する処理に相当)@renderView()# この画面の状態から適切なViewをレンダリングする(Androidでは開発者が明示的に実行しなくてもよい。あえて言うならView#invalidate)onResumeBefore:-># 画面が活性化するたびに呼び出されるメソッド(AndroidのActivity#onResumeに相当)@num++handleView:->@view.on('click','.button',@onTapButton)renderView:->@view.find('.num').text(@num)onTapButton:(evt)=>@next(SubPage,{num:@num},@onResultFromSubPage)# 別の画面に遷移する(AndroidのActivity#startActivitForResultに相当)
 
  onResultFromSubPage (data)=># 呼び出し先画面から戻ってきた時の処理(AndroidのActivity#onActivityResultに相当)@num= data.num
    @renderView()classSubPageextendsBasePagenum:nullonCreate:(data)->super(data)@num= data.num +Math.random()@setResult({num:@num})# 呼び出し元画面に戻った時に渡す値(AndroidのActivity#setResultに相当)@inflateView('.sub_page.template')@handleView()@renderView()handleView:->@view.on('click','.back_button',@onTapBackButton)renderView:->@view.find('.num').text(@num)onTapBackButton:=>@back()# 自身を破棄して呼び出し元画面に戻る(AndroidのActivity#finishに相当)

なぜ既存のフレームワークを使わずにこのような仕組みを作ったかというと、スマートフォンの場合は各画面がすごく単機能になっており、いくつもの画面を行ったり来たりしながらアプリを使うというのが普通だからです。そうなってくると各画面を論理的に分割し、画面間をなるべく疎結合にすることが必須になってきます。さらに各画面は表示/非表示(活性/非活性)を繰り返すことになるので、ライフサイクルという考えも必要になってきます。僕が知るかぎりこのような仕組みをもったフレームワークが存在しなかったため自作したという経緯があります。また、このような仕組みを持ったAndroidに実績があるというのも理由になります。

ちなみにこの仕組はCoffeeScriptで200行程度で実装されており、非常にシンプルなものになっています。

データの状態管理

SPAには画面管理以外にも解決しなければならない問題があります。それはデータの状態管理です。通常のWebページであればステートレス(それぞれの状態に一意のURLが紐づく)なためクライアント(ブラウザ)側で状態を持つことはありません。しかしSPAにすることでデータの状態をもつ必要が出てきます。例えば一覧画面 → 詳細画面 → 編集画面と遷移し、編集画面でアイテムの状態を変更したとします。そうすると一覧画面、詳細画面で表示を変更する必要がでてきます。つまりデータの状態を管理してそれに応じて表示も変更するということです。

このようにクライアント側でデータの状態管理を行うために以下のようにしました。

  • サーバサイドはJSONデータを返すAPIとして実装する
  • クライアントサイドにモデルクラスを実装する

通常のWebページであればサーバは組み立てられたHTMLを返しますが、SPAでは必要なデータだけをJSONで返し、HTMLの組み立てはクライアント側で行います。そしてサーバから取得したJSONをそのまま使うのではなく、そのJSONに対応するモデルクラスを実装します。少し動的な要素があるWebページの場合、サーバからのJSONをObjectにパースしてそのまま使用することがあると思います。しかしある程度規模が大きくなってくるとひずみが出てきます。

例えば「itemのリストからid = 10のitemを取得する」を実装する場合、生ObjectだとUtil.findItem(items, 10)のようになると思います。しかしモデルクラスを実装すればitems.find(10)のように処理の責務を正しくitemsに持たせることができます。クライアントサイドのアプリケーション(Android, iOS, Qtなど)では普通はこのようにモデルクラスを実装すると思います。Webはステートレスなアーキテクチャから始まっているためこのようなモデルクラスを実装する習慣がないのだと思います。そしてこのモデルはサーバサイドのモデルよりも寿命が長く様々なところから操作されます。ここがクライアントサイドを実装する面白さの一つだと思います。*2

余談ですが、データの状態更新と画面更新を自動的に結びつける技術としてデータバインディングという仕組みがあります。もしデータバインディングを採用する場合は保守性、パフォーマンス、チーム開発のしやすさ、既存のコードベースとの相性などを検討する必要があると思います。

SPAに適した条件

このようにしてSPAを実現し高速化を行いました。しかしSPAを実現しやすくするためにはいくつかの条件があります。

  • 画面数が少ない
    • 画面数が多ければ多いほど、扱うデータやHTML/JavaScript/CSSの量が多くなりメモリシビアになります
  • 通常のURL遷移がない
    • 途中でURL遷移してしまうとそれまでのステートがすべて消えてしまうため、前の状態に戻るのが非常に難しくなります
  • SEOを考えなくて良い
    • SPAはひとつのURLに様々な画面が付随し、かつステートを持つためURLベースのクローラと相性が非常に悪くなります
    • サーバサイドでのHTML構築、pushState、#!URLなどを使って解決する方法もあります

これらの条件は必ずしもクリアする必要がなく、SPAによる恩恵とのトレードオフになると思います。

今回の撮るレシピでは画面数は5画面程度、ユーザのみが見れるプライベートなデータなのでSEOは考慮する必要がありませんでした。URLの遷移はcookpad.com下で展開しているので必ず発生するのですが、「他の画面(マイフォルダやトップなど)にURL遷移して戻ってきた場合は撮るレシピのトップから始める」ということにして対応しました。これは撮るレシピから他の画面に遷移したということはユーザのコンテキストが変わったと推測できるので、撮るレシピに戻ってきた時はトップから始めても差し支え無いだろうという判断です。つまりSPAによる恩恵(高速化)と画面遷移のトレードオフを行ったわけです。

その他にもブラウザの履歴機能と相性が悪かったり(進むの実装が難しい)、端末スペックを要求されたりします。撮るレシピでは履歴を進む機能には対応しませんでした。端末スペックはファイルアップロードを行う必要があるため必然的にAndroid4.0以上、iOS6以上となり対応することができました。


これらのSPAを実現する方法はWebアプリケーションとしては新しい実装方法です。ですがクライアントアプリケーションとしては至って普通の内容になっています。

写真のリサイズとローカル表示

撮るレシピでのユーザ操作は主に2つあり「写真を閲覧する」と「写真をアップロードする」です。前者はSPAで快適にすることができました。そこで次に後者を快適にする必要があります。

ユーザの写真サイズ、回線速度

写真のアップロードを快適にするためには「ユーザの写真サイズはどの程度なのか」と「ユーザの端末の回線速度はどの程度なのか」という2つを知る必要がります。これらをcookpad.comにアクセスするユーザに対して計測しました。

  • 写真サイズ(つくれぽで測定)
    • 1MB以上の写真は全体の約40%
    • 一番多かったのは1.8MB付近の写真
  • 回線速度
    • 高速回線(LTE/Wi-Fi)は全体の約80%

高速回線を使ってるユーザがほとんどですが、低速回線を使っているユーザも20%程度います。そしてたとえ高速回線を使っていたとしても1MB以上の写真をアップロードするのは時間と通信料がかかります。特に最近はスマホの通信料は増えておりキャリアによっては通信制限がかかる場合もあります。

では実際にアップロード時間はどの程度かかるのかも見てみます。

  • Nexus5を使って写真をアップロードして計測(写真サイズ: 1.5MB〜2MB)
    • LTE: 6秒〜7秒
    • 3G: 40秒〜50秒

LTEでも結構な時間がかかります。3Gに至ってはサービスとして致命的です。これらを改善するために「リサイズ」と「ローカル表示」を行いました。

リサイズとローカル表示

ここ1, 2年のAndroidやiOSのカメラは8MPx(3264x2448)の解像度を持っています(2014年夏モデルのAndroidは20MPxを持っています)。クックパッドでは写真サイズはサーバ側で1MPx(1000x1000)にリサイズされます。つまり8MPxの解像度でそのままアップロードするのではなくクライアント側で1MPxにリサイズしてからアップロードすればアップロード時間、通信料ともに少なくて済むようになります。技術的な詳細は割愛しますが、ブラウザ側でも写真のリサイズは可能です。しかしAndroid、iOSともに幾つか問題があるのでJavaScriptを使って補助する必要があります。リサイズに関しては写真サービス機能のブラウザ内実装 | 株式会社サイバーエージェントが非常にわかりやすくまとまっています。

そしてこのリサイズが終了した段階でサーバにアップロードするのですが、アップロードと同時にブラウザに表示(ローカル表示)してしまいます。こうすることでユーザの体感的にも処理が速く終わったように見え、他の写真を撮ったり、レシピタイトルを入力している間にアップロードを終わらせます。

これで写真アップロードも快適になったと思ったのですが、例のごとくAndroidブラウザに問題があります。それはリサイズ処理にも時間がかかるということです。Androidブラウザでは4〜5秒、AndroidChromeでは1〜2秒、iOSでは0.2〜0.5秒かかります。5秒前後もかかっていたらリサイズせずにアップロードするのと大差ありません(Androidブラウザが提供する画像処理の機能に問題があるためこれだけの時間がかかってしまいます)。

しかし検証を進めていくとAndroidでは更に別の問題が出てきました。

  • メモリの空き容量が少ない状況では写真のリサイズが失敗する場合がある(Xperia SX, Xperia A2, Nexus5で再現)
  • 一部のギャラリーアプリから写真を選択すると、JavaScriptでFileオブジェクトを扱うことができない(Xperia A2で再現)

このようにAndroidでは写真のリサイズ、ローカル表示が不安定なため現在はiOSのみ対応しています。Androidの対応は今後の改善課題となります。

タップ処理の最適化

スマートフォン版cookpad.comではHTMLの要素がタップされた時のイベントとしてclickイベントが使われています。しかし実はclickイベントはユーザが要素をタップした瞬間には発生せず、300ミリ秒前後待ってからclickイベントが発生します。これはスマートフォンのブラウザではダブルタップすると画面を拡大する機能が付いているため、タップなのか画面拡大なのかを判断するために300ミリ秒待つからです。 つまりイベントの発生順序としては以下のようになります。

  • touch start
  • touch move
  • touch end
  • wait 300ms
  • click

300ミリ秒の遅延というのはかなり大きいものです。サーバサイドでAPIのレスポンスタイムを300ミリ秒速くするのはかなり大変だと思いますが、クライアントサイドではclickイベントをやめてtouchイベントを使うようにすればそれだけ高速化でき費用対効果に優れています。

しかし実際にclickイベントをやめてtouchイベントにする場合にはいくつかの問題があります。

  • clickイベント(aタグによるリンクも含む)とtouchイベントは相性が悪いのでclickイベントを無効化する必要がある
    • 厳密にはtouchイベント直後のclickイベントのみを無効化する
  • スクロールなのかタップなのかを指の動きや時間から判断する必要がある

撮るレシピではclickイベントは一切使わず、すべてtouchイベントでハンドリングしています。この辺りの詳しい内容は300ms tap delay, gone awayを参考にしてください。


まとめ

フロントエンドの改善というとUIやアニメーションなどに手をいれることが真っ先にあげられます。しかし、そもそもスマートフォンWebの場合は動作が遅いという欠点があり、高速化がユーザの使い勝手向上に大きく影響してきます。ここで紹介した高速化の方法以外にも色々な方法があるので、サービスごとに適切な方法を選択してユーザにとって快適なサービスづくりをしていく必要があります。

以上クックパッドで行っているスマートフォンWebのフロントエンドを高速化する取り組みでした。

*1:ユーザが操作を開始してから処理が完了し、ユーザが次の操作ができるようになるまでの時間

*2:この辺りがWebフロントエンドとWebクライアントサイドの境界線かなと思います

第5回 開発コンテスト24 総まとめ

$
0
0

イベント概要 : 2014年も開発コンテスト24を開催します! - クックパッド開発者ブログ

開発コンテストへのご参加ありがとうございました。みなさんが朝早くから夜遅くまで開発しているところや、作品を提出してそのまま倒れ込むように休むところも見ていました。本当にお疲れさまでした。

時間帯と提出数のグラフは下のようになっています。

やはり7〜9時台の提出が一番多いですね。最速ではコンテスト開始から7時間後に提出があり、締め切りの2秒前に最後の応募がありました。

また、プラットフォームの割合を見るとWebアプリが56.4%、スマートフォンアプリがAndroidとiOS合わせて41.8%ありました。

去年のスマートフォンアプリの応募数は3割くらいだったと思うので、増えてきていますね。 受賞作品を見るとWebアプリの方が多いので、24時間という短い時間だとWebアプリの方が作りやすいのかもしれません。

開発コンテストは終わりましたが、今週の土曜日(10/25)に 第5回開発コンテスト24授賞式&LT大会を行います! ここでは、受賞者の方々にどういうことを考えて、どういう環境で、どうやって開発したのかということや、作品のこだわりポイントを紹介してもらったりしていただこうと思っています。 また、受賞者でなくても自分の作品を自慢したり、うまくいかなかったところや失敗は共有したりして、今後の開発に役立てたいと思っております。まだ空きがありますので、よろしければご参加ください。

受賞作品一覧

【最優秀賞】マゴトーーク

  • 受賞者 : よっきー&英吉 さん
  • 説明 : 孫の数も増えてきて、彼らがどんどん成長する姿は日々の楽しみ。でも、それにともなって会話が減っていませんか? マゴトーークは、年齢と性別を入力するだけで、孫世代に話しかけられるキーワードがすぐに分かるサービスです。 会話のきっかけを作って、どんどんコミュニケーションしていきましょう!

【優秀賞】EmotionTopic

  • 受賞者 : SUGARSPOT さん
  • 説明 : Twitterで知り合ったはじめての人と実際にあうとき、どんな話題を話して良いかこまります。 そんなときにEmotionTopicを使うと、過去のツイートから興味のあるTopicを取得してはなしのきっかけを作ることができます。 Topicには、それぞれ次のような感情情報があるので、それを参考に興味のある話題をしましょう。 【Positive】ポジティブな話題 【Normal】とくに感情が関係ない話題 【Negative】ネガティブな話題

【グローバル賞】GentlyAsk

  • 受賞者 : harip0 さん
  • 説明 : GentlyAsk はあなたの「話しかけたい」を解決します。 仕事中の同僚や、勉強している友達に声をかけづらいと思ったことはありませんか? GentlyAsk は相手の作業を邪魔することなく話しかけるために作られたアプリケーションです。 パソコンのウェブカメラを用いて顔の動きを検知し、 作業者に負担のないタイミングで「話しかけたい」を知らせることができます。

【デザイン賞】ダレトーーク

  • 受賞者 : よっきー&英吉 さん
  • 説明 : どうしても誰かと話したい。 そんな時、誰かと話すキッカケになること間違いなし!! ダレトーークは知らない誰かと電話できるサービスです。 ダレトーークの電話番号にかけた人同士で通話をします。 電話番号を通知することなく誰かと電話ができます。 今回のテーマを言葉の通り受け取り制作しました。

【技術賞】viiiideo

  • 受賞者 : 不動産屋の手先 さん
  • 説明 : viiiideoは4人以下でyoutubeの動画を一緒に楽しめるWEBサービスです。 ビデオチャットをしながらyoutubeを同時再生します。 友人と顔を見て話しながら動画が見れるので、怖い動画や笑える動画で絶大な威力を発揮します。 特に複雑な会員登録が必要なく、URLを生成して送るだけという簡単なスキームもこだわりです。


審査員も開発者に負けないくらいの熱量で審査を行いました。審査員からは普段とは違った視点でサービスを見ることができてとても良い勉強になったという声も聞いています。

クックパッドでは新しい技術の検証や技術の理解のために勉強会を頻繁に行っており、このブログでもよく記事を投稿しています。それらはエンジニアとしての技術的興味はもちろんですが、その機能は「誰」の「どんな問題」を解決するのか、どうやってユーザーのために役立てるか、ということを一番に考えてます。 今年のテーマは「誰かと話すキッカケを作るサービス」でしたが、普段の開発と同じように、リアルにユーザーが使っているところを想像できるかということを意識して審査を行いました。

どの作品も甲乙付けがたく、惜しくも受賞を逃した作品もその差はほんの少しだけだったので、これに懲りずまたご参加いただければと思います。

たくさんのご応募どうもありがとうございました。

正常なAndroidアプリをビルドできない問題とその対策

$
0
0

モバイルファースト室の山下(@tomorrowkey)です。
先日撮るレシピというAndroidアプリをリリースしました。
f:id:tomorrowkey:20141027085113p:plain

みなさんの自宅には開かずにずっとおいてあるレシピ雑誌はないでしょうか。その中でも作ってみたいと思うレシピは何品あるでしょうか。
また母親や友達から教えてもらったレシピを付箋に書いて冷蔵庫に貼っていたりしませんか。冷蔵庫が付箋だらけになっていませんか。
このアプリはそんなレシピたちを写真に撮って残せるアプリです。雑誌や冷蔵庫のドアなどちらばったレシピを1つにまとめることができます!

f:id:tomorrowkey:20141027085224p:plain

そんなとっても便利なアプリなのですが、今回このアプリをリリースするときにGoogle Playからインストールできなくなるという現象に遭遇しました。
同じ轍を踏む人がでてこないように、その原因と対策を紹介します。

ビルド環境

この問題が発生したのは以下の環境です。
例えばantなどの他のビルド環境では検証していません。

./gradlew --version

------------------------------------------------------------
Gradle 2.1
------------------------------------------------------------

Build time:   2014-09-08 10:40:39 UTC
Build number: none
Revision:     e6cf70745ac11fa943e19294d19a2c527a669a53

Groovy:       2.3.6
Ant:          Apache Ant(TM) version 1.9.3 compiled on December 23 2013
JVM:          1.7.0_45 (Oracle Corporation 24.45-b08)
OS:           Mac OS X 10.9.4 x86_64

問題の発覚

リリース日の昼ごろにGoogle PlayのDeveloper Consoleの公開ボタンを押し、夕方には配布開始されました。
以前は公開ボタンを押したらすぐに公開されていましたが、最近は時間がかかるようになりましたね。
早速手元の端末でインストールしようと思ったのですが、インストールボタンが無効になっていてインストールできません。
いろんな端末で試してみたのですが、どれもインストールできませんでした。

Developer ConsoleでAPKの詳細を見ることで、配布するAPKファイルのデータを見ることができます。
正常なAPKファイルはこのようになっています。

f:id:tomorrowkey:20141027085256p:plain

対応するAndroid搭載端末に、対応するAndroid端末の数が表示されます。
このスクリーンショットのアプリは4000種類以上のAndroid端末に対応しています。
問題のあったAPKファイルの詳細を見てみると…

f:id:tomorrowkey:20141027085308p:plain

対応するAndroid搭載端末が0になっていました。つまり、どの端末でもインストールすることはできません。
そしてネイティブプラットフォームという欄が増え、commons-io-2.4.jarと表示されています。

原因

結論から話すと、変な構成のライブラリを参照していたためどんな端末でもインストールできなくなってしまってました。
問題のあったライブラリはこれです。
org.apache.directory.studio:org.apache.commons.io:2.4

通常のライブラリはルートディレクトリからパッケージのディレクトリが切られ、クラスファイルが配置されています。

.
├── META-INF
│   ├── LICENSE.txt
│   ├── MANIFEST.MF
│   ├── NOTICE.txt
│   └── maven
│       └── commons-io
│           └── commons-io
│               ├── pom.properties
│               └── pom.xml
└── org
    └── apache
        └── commons
            └── io
                ├── ByteOrderMark.class
                ├── Charsets.class
                ├── CopyUtils.class
                ...

問題のあったライブラリはルートディレクトリのlibディレクトリにjarファイルが入っていました。

./
├── META-INF
│   ├── DEPENDENCIES
│   ├── LICENSE
│   ├── MANIFEST.MF
│   └── NOTICE
└── lib
    └── commons-io-2.4.jar

この構成がそのままマージされてしまったためlibディレクトリにcommons-io-2.4.jarが入り込んでしまったようです。

apkファイルのlibディレクトリ直下にはサポートするプラットフォームの名前でディレクトリが作られます。
ネイティブコードを含む正常なapkファイルをunzipすると以下のようなディレクトリ構成になっています。

./
├── AndroidManifest.xml
├── META-INF
│   └── ...
├── assets
│   └── ...
├── classes.dex
├── lib
│   ├── armeabi
│   │   └── ...
│   ├── armeabi-v7a
│   │   └── ...
│   └── x86
│       └── ...
├── manifest
└── res
    └── ...

armやx86などに対応していると解釈されます。

問題のあったapkファイルをunzipすると以下のようなディレクトリ構成になっていました。

./
├── AndroidManifest.xml
├── META-INF
│   └── ...
├── assets
│   └── ...
├── classes.dex
├── lib
│   └── commons-io-2.4.jar
├── manifest
└── res
    └── ...

問題のあるライブラリを参照していたためlibディレクトリにcommons-io-2.4.jarが入り込んでいました。
このようなapkファイルをリリースしようとすると対応するネイティブプラットフォームがcommons-io-2.4.jarになってしまいます。

対策

リリースする前に対応するAndroid端末を見ればリリースに失敗することはありませんが、使っているライブラリが実は使えないものだとリリースする直前に発覚しても遅いです。
なんとか開発中に気づけないものかと仕組みを考えました。

CookpadではCIツールにJenkinsを使っています。
開発中に何度かJenkinsでビルドをするので、ここで意図しないファイルが含まれていないかチェックすることができそうです。

if [ `unzip -l ./app/build/outputs/apk/app-debug.apk | grep -e "lib/" | grep -v "lib/armeabi" | grep -v "lib/x86" | wc -l` -eq 1 ]; then
    echo "The apk may include invalid library"
    exit 1
fi

完成したapkファイルをunzipし、libディレクトリ配下にarmもしくはx86以外のファイルまたはディレクトリがないかチェックしています。
もし不正なライブラリが入っていると判断された場合はビルドが中断します。

このような事態を防ぐためには「リリースする前に表示を確認する」ということで防ぐことはできそうですが、作業するのはやはり我々人間なのでついつい忘れがちですし注意しないといけないことが増えてくるとだんだんストレスになってきます。
このスクリプトを使うことによりリリース失敗するリスクが減りました。

まとめ

CIにスクリプトを追加することによって、不正なapkファイルの検出を自動化することができるようになりました。
この不具合にはいくつかパターンがあるらしく、不正なライブラリを参照する以外にも再現する可能性があるので、ぜひビルドスクリプトに組み込んでみてはいかがでしょうか。

Viewing all 802 articles
Browse latest View live