読者です 読者をやめる 読者になる 読者になる

Rails アプリケーションにおけるリファクタリングの実践

Ruby on Rails

こんにちは、MERY のサーバサイド開発をしている末並 @a_suenami です。 TDD、アジャイル、DB 界隈等によく出没しますが、最近では糖質警察としてのほうが広く知られている気がする今日この頃です。糖質制限に興味ある方はぜひウィスキーを片手にケトン体の話でもしながら飲みましょう。

さて、現在、MERY は Ruby on Rails で開発されていますが、最初にリリースされたのはもう 3 年近く前であり、その頃とはサービスを取り巻く状況が大きく変わってきています。これまで多くのユーザの「かわいい」を支え、よい体験を提供し続けてきた現在の MERY とそのコードベースを否定することは決してできませんが、日々変わるユーザの「かわいい」ニーズと我々のビジネス状況の変化にスピード感を持って追随していくためにはいわゆる「技術的負債」を返済していく必要があることも否定できない事実です。

そこで今回は実際の Rails アプリケーションにありそうなサンプルコードとともにそのリファクタリングについて紹介しようかと思います。普段、私が MERY のコードをリファクタリングする際にも意識しているテクニックになります。

技術的負債とは?

ソフトウェアエンジニアの人で「技術的負債」という言葉を聞いたことがないという人はあまりいないと思いますが、巷では単なるクソコードを揶揄するためだけにその言葉が使われている印象を受けることも少なくありません。もちろんクソコードを書かないに越したことはありませんが、それは技術的負債以前の話であり採用や教育によって解決するべき課題でしょう。 本来の金融的意味での負債がそうであるように技術的負債もそれ自体は悪いことではありません。資金がないと事業に推進力が生まれないのと同様、今この瞬間にこの機能をリリースしないと貴重なビジネスチャンスを失うということはありえます。重要なのは通常の負債と同様、きちんと返済計画を立てることであり、そのためにはリファクタリングのパターンを知り使いこなせるようになる必要があります。

リファクタリングはいつやるべきか

リファクタリングは「時間があればやる」という優先度になりがちな作業ですが、私の知る限り、その感覚でうまくいく事例はほとんどありません。 外部からの振る舞いが変わらない以上、非エンジニアからの価値はゼロに等しく、そのためのまとまった時間を確保することは実際にはかなり厳しいでしょう。(実際には開発スピードの改善やチームの士気向上という効果はあるのですが、それはエンジニア同士でしか計測ができません。)

したがってリファクタリングを継続的におこなえるチームを作るためにはそれを「特別なタスク」としてとらえるのではなく「定常業務のひとつ」ととらえなければなりません。朝出社したらタイムカードを押すように、数時間おきにメールチェックをするように、帰る前には日報を書くように、リファクタリングも日々の業務の中に紛れ込ませて進めていくべき業務なのです。 リファクタリングといえばそれがズバリそのまま書籍名になっているマーチン・ファウラー氏の名著がありますが、そこでは「いつリファクタリングをすべきか?」という問いに対していくつかの回答があります。

新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES)

新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES)

  • 同じコードを 3 回書いてしまったとき
  • 既存機能に変更をするために該当のコードを読むついでに
  • 新機能を実装してバージョン管理システムにコミットする前

同じコードを書いてしまったときというのはわかりやすいでしょう。いわゆる DRY 原則にしたがって重複を排除しようということです。ここで重要だと思うのは「3 回」と回数を指定しているところです。2 回目だとたまたまコードが同じになった可能性を否定できず、早すぎる最適化になりうるることを示唆しています。文脈を無視してコードの見た目が似てるからまとめたという過度な抽象化は DRY であるどころか害悪ですので気をつけましょう。

2 つ目のシチュエーションは私が最も多く遭遇するケースです。既存機能に変更をおこなうケースではまずその部分のコードを読み、どこをどのように変更すれば要件を満たせるかを検討しますが、コードを読む時点で読みにくいと感じたり意味がわからなかったりするケースがあります。この場合、後述する仕様化テストを書き、自分が理解しやすく、かつ、これから自分がやろうとしている変更をしやすいだろうと思うように書きなおしてみます。これが機能変更前のリファクタリングになります。

3 つ目はテスト駆動開発( TDD )をちゃんと実践できている人にとっては当たり前だと思いますが、機能を実装したあとに自分以外の誰かに見てもらう前にリファクタリングをおこないます。TDD に関してここで言及すると紙面が足りなくなるので割愛しますが、テストファーストを意識していれば今書かれているプロダクトコードに対しては確実にテストコードが存在することになりリファクタリング可能ですし、このシチュエーションは主に新機能の開発時に遭遇すると思うので既存のコードベースは関係なく、もっともリファクタリングを実践しやすいケースかもしれません。

レガシーコードのリファクタリングを行うための重要なテクニック

リファクタリングはソフトウェアそのものの振る舞いを変えずに内部の実装方法を変えることであり、通常は振る舞いを変えてないことを保証するためにテストコードが必要です。しかし、世の中にはテストコードが存在していないコードベースも存在しており、そういったソフトウェアのリファクタリングをおこなうためのテクニックとして「レガシーコード改善ガイド」という書籍ではいくつかの手法を紹介しています。

レガシーコード改善ガイド (Object Oriented SELECTION)

レガシーコード改善ガイド (Object Oriented SELECTION)

これらのテクニックは非常に多くの現場で活用可能なのでぜひ積極的に活用していきましょう。

シグネチャの維持

テストコードがない状態でも可能なリファクタリング手法として「レガシーコード改善ガイド」では 2 つの手法が紹介されています。

1 つは「コンパイラまかせ」で、テストが本来担う役割をコンパイラに任せる方法です。コンパイラの静的型検査を利用することによって修正必要箇所を教えてもらうという方法で、その言語の型システムや静的検査時の厳密さ次第ではかなり有効な方法ですが、残念ながら動的型付き言語である Ruby でこの手法は適用できません。

もう 1 つの方法が「シグネチャの維持」です。これはある場所に定義されたメソッドを"そのまま"別のもっとテストしやすいクラスやモジュールに移動させることです。当たり前だと思う人もいるかもしれませんが、人間の欲望というのはおもしろいものでリファクタリングをするつもりになったらつい"ついでに"他のこともやりたくなってしまうものです。しかし、テストコードのないプロダクトコードに対して、それがどんなに簡単に見えて自分自身は修正することに自信があったとしても、シグネチャを維持しない状態で変更をするのはリスクをともないます。

リファクタリングの第一歩は何よりもまずテスト可能な状態にすることであり、それ以上のことを最初にやるべきではないのです。

メソッドの抽出

ある場所にあるメソッドをそのまま別のクラスやモジュールに委譲する場合は「シグネチャの維持」が使えますが、巨大なモンスターメソッドのごく一部が非常に複雑になっていてその部分だけ別に切り出したいケースもあるでしょう。むしろ実際の現場ではそのケースのほうが多いと思います。

この場合には「メソッドの抽出」という手法が使えます。その名の通り、あるメソッド内のある処理を別のクラスやモジュールのメソッドとする手法で、本来はもともとのメソッドにテストコードがある場合か IDE に自動リファクタリング機能ががある場合に利用するのが好ましいですが、そうでない場合にも以下の手順で慎重に行えばリスクを最小化して変更を行うことが可能です。

  1. 抽出したいコードを特定してコメントアウトする。
  2. 新しいメソッドを作成し、抽出したいコードをコピーする。
  3. 新たに作成したメソッドをただ実行するだけのテストコードを書き、引数・戻り値を調整する。
  4. コメントアウトした箇所を新たに作成したメソッドの呼び出しに変更する。
  5. コメントアウトした部分を削除する。

「レガシーコード改善ガイド」の中では 3 の手順が「コンパイラまかせ」を使うように紹介されていますが先述したように Ruby ではこの手法は使えないため、テストコードで代用します。この手順を経ることでグローバル変数やもともとのクラスのインスタンス変数へ依存していたことがわかることがあり、それらも引数で渡してあげるようにしましょう。「シグネチャの維持」でも述べたようにこの状態におけるリファクタリングではコードそのものはコピー以外のことをしてはいけません。

仕様化テスト

TDD では何を実装するべきかをテストコードで表現し、それをパスするようにプロダクトコードを実装します。しかし、すでに存在しているコードにテストコードがない場合はこの方法は使えません。そこで用いる有効な手段が「仕様化テスト」です。

仕様化テストとは現在の実装が正しく仕様を満たしていると仮定して書かれるテストコードのことです。仕様に基づいて実装をするという本来のソフトウェア開発の真逆の考え方で、現在の実装を仕様として再定義するということになります。

シグネチャの維持やメソッドの抽出もそうですが、リファクタリングの最中というのはついついいろいろなところに手を出したくなるものです。リファクタリングの仮定で今まで気づかなかったバグや好ましくない挙動に気づくことも少なくなく、そのときに"ついでに"なおしてしまおうとすることはよくあります。しかし、テストコードがない状況においてはこれは危険な発想で、自分にとって不具合だと思ったことがある状況において重要な振る舞いであることもあるので、まずは現在の振る舞いを正しく表現するテストコード、つまり仕様化テストを準備することが大切なのです。

Rails アプリケーションにおけるリファクタリングの実践

ここからは実際の Rails アプリケーションでどのようなリファクタリングを行っていくかをサンプルコードを交えながらご紹介していこうと思います。MERY にもまだファットコントローラーやファットモデルとなっている部分があるため、今回はその改善について 2 つのアンチパターンとその対処法という形で紹介します。

アンチパターン: とりあえずインスタンス変数

私はこれは明確にアンチパターンだと思っているのですが、ビューテンプレートで利用する変数をすべてインスタンス変数としてアクションメソッド内で宣言してしまうケースがよくあります。MERY 内でも最近はあまりありませんが以前に実装された部分だとまだ残っていることがあります。これは以下のような点で扱いが難しいです。

  • アクションメソッドが長くなってしまい可読性が下がる。
  • 一般にコントローラーはテストしづらく、やろうと思うと実際にもしくは擬似的に HTTP リクエストを該当のエンドポイントに送る必要がある。
  • コントローラーのインスタンス変数とはいっても実際はそのリクエスト内においてはグローバル変数のようなものであり、どこでどういう風に参照・更新されているかを把握するのが難しい。

特に重要なのは 3 つ目で、MERY だと PC とモバイルで異なるテンプレートを使っていたり、パーシャルテンプレートを多用していたりするため、影響範囲が広く小さな修正でも確認箇所が増えてしまうということになりがちです。

対処法

不幸中の幸いはこれらがメタプログラミングによって動的に生成されたものでなく、ソースコード内の検索で宣言されている箇所・参照されている箇所をほぼ見つけられることです。Ruby のよいところでもあり悪いところとしてメタプログラミングのやりやすさがあり、例えば define_methodclass_evalinstance_eval などで動的にメソッド定義可能ですし、逆に send で動的に呼び出し可能です。今回問題にしているインスタンス変数も instance_variable_setinstance_variable_get で同様の問題は起こり、実際にそのメソッドや変数が定義されている箇所や参照されている箇所を特定しにくいことがしばしば問題になります。

アクションメソッド内で宣言・代入されビューテンプレートで参照されるインスタンス変数において、現状の MERY ではほぼこういった問題はないため、きちんとしたテクニックさえ知っていればリファクタリングをおこなうことが可能です。

  1. コントローラーのインスタンス変数に代入されている部分を"そのまま"モデルやヘルパーに移動する。(メソッドの抽出・シグネチャの維持)
  2. views/ 以下の全ファイルを対象にコード内検索をかけ、そのインスタンス変数を使っているところを新たに作成したメソッドを呼ぶようにする。
  3. 作成メソッドの仕様化テストを書く。
  4. ブラウザで手動動作確認をする。

もちろん 4 も理想的には feature spec や Cucumber ライクなフレームワークによる自動シナリオテストで行われるのが理想ですが、まずはファットになっているコントローラーから他のコンポーネントへ処理を移譲していき、モデルやヘルパーのテストで品質を安定させていくことが目的であり、シナリオテスト自動化はこれらの積み重ねによってようやく達成可能になるので、これが第一歩と言えます。

以下のように条件分岐やループ処理を含むコードがアクションメソッド内にあるとします。

class UserController < ApplicationController
  def mypage
    @user = User.find(params[:id])
    # 制御構造を含む複雑な処理
    @some_array = []
    if @user.some_condition?
      @user.some_collection.each do |element|
        @some_array << do_something_to(element)
      end
    else
      @user.another_collection.each do |element|
        @some_array << do_otherthing_to(element)
      end
    end
  end
end

これを次のようにほぼそのまま User クラスのメソッドとして抽出します。

class User
  def some_array
    some_array = []
    if self.some_condition?
      self.some_collection.each do |element|
        @some_array << do_something_to(element)
      end
    else
      self.another_collection.each do |element|
        @some_array << do_otherthing_to(element)
      end
    end
    some_array
  end
end

これによりビュー側では

<%= @some_array.each do |element| %>

としていたところが

<%= @user.some_array.each do |element| %>

のようになります。

ここのポイントは some_array の処理は @user から self への置換こそあるもののそれ以外に一切手が加えられてないことです。これ以上手を加えるのはこのメソッドのテストコードが十分に整備されるまで決してやってはいけません。

アンチパターン: 賢すぎるビュー

ドメイン駆動設計(DDD)をご存知の方はご存知の人も多いかと思いますが、本来ドメイン層(ビジネスロジックと言い換えてもよい)にあるべきはずの振る舞いが画面に漏れ出してしまい、画面の変更がしにくくなるという状態を「スマートUI(利口なUI)」と呼びます。

これは Rails のアプリケーションではモデルやヘルパーではなくビューテンプレートに複雑な処理が出現するという形で現れます。UI というのはニーズの変化によって変更されやすい箇所であり、それがテストしにくいビューテンプレートに直接書かれていることは大規模なサービスになると非常に問題になってくるので改善が必要になります。

対処法

このアンチパターンも「とりあえずインスタンス変数」アンチパターンと同様、モデルやヘルパーに振る舞いを移動するということで対処するのが基本ですが、ERB や Haml 等のテンプレートエンジンから通常のクラスやモジュールへコードを移動することになるので本来の意味での「メソッドの抽出」は適用できず単なるコピー以上のコード変更をすることになります。そのため、先に紹介したリファクタリングよりさらに慎重に行う必要があります。

  1. ビューテンプレートに直接記述されている複雑な処理を"制御構造はそのままの状態で"モデルやヘルパーに移動する。(「メソッドの抽出」の応用)
  2. 対象のビューテンプレートでは新たに作成したメソッドを呼ぶようにする。
  3. 作成メソッドの仕様化テストを書く。
  4. ブラウザで手動動作確認をする。

ポイントはメソッドの制御構造を変えないことです。

以下はパンくずリストの表示をビューテンプレートで実装している例になります。

<ul class="breadcrumb">
  <li itemscope itemtype="http://data-vocabulary.org/Breadcrumb">
    <a href="<%= root_url %>" itemprop="url">
      <span itemprop="title">MERYトップ</span>
    </a>
  </li>

  <% if @user.group.present? %>
    <li itemscope itemtype="http://data-vocabulary.org/Breadcrumb">
      <a href="<%= group_url(@user.group) %>" itemprop="url">
        <span itemprop="title"><%= @user.group.name %></span>
      </a>
    </li>
  <% end %>

  <%# 複雑な処理 %>

  <li itemscope itemtype="http://data-vocabulary.org/Breadcrumb">
    <a href="<%= user_url(@user) %>" itemprop="url">
      <span itemprop="title"><%= @user.name %></span>
    </a>
  </li>
</ul>

このコードは制御構造は複雑ですがレンダリング処理そのものは単純なループ処理で実現でき、簡単なバリューオブジェクトの導入で以下のように整理できます。

class BreadcrumbItem
  attr_reader :title
  attr_reader :url

  def initialize(title, url)
    @title, @url = title, url
  end
end
<ul class="breadcrumb">
  <% user_breadcrumb_items(@user).each do |item| %>
    <li itemscope itemtype="http://data-vocabulary.org/Breadcrumb">
      <a href="<%= item.url %>" itemprop="url">
        <span itemprop="title"><%= item.title %></span>
      </a>
    </li>
  <% end %>
</ul>

この変更によって複雑なコードはすべてヘルパーへ移動することができます。

module UsersHelper
  def user_breadcrump_items(user)
    items = []
    items << BreadcrumbItem.new('MERYトップ', root_url)

    if user.group.present?
      items << BreadcrumbItem.new(user.group.name, group_url(user.group))
    end

    # 複雑な処理

    items << BreadcrumbItem.new(user.name, user_url(user))
  end
end

この変更はコードのコピー以上のことをやっているため厳密には「メソッドの抽出」とは言えませんし、それをテストコードなしの状態で行っているため「リファクタリング」と呼んでいい変更とも言えませんが、十分に小さい範囲を慎重に行う限りにおいては通常の「メソッドの抽出」や「シグネチャの維持」と同様にリスクを最小化して実施することができます。「レガシーコード改善ガイド」で紹介されている手法のある種の応用と言えるでしょう。

これ以降は抽出したヘルパーメソッドに仕様化テストを記述し、通常のリファクタリングを行っていくことになります。

まとめ

リファクタリングには多くのテクニックがあり、正しい手順でリスクを最小限にしながらおこなうためにはそれなりの知識と習熟が必要になります。しかし、リファクタリングの結果を保証するためのテストケースが十分に存在している限り、それを必要以上に恐れる必要はありません。

それよりも難しいのは何よりもまず「リファクタリング可能な状態にする」ことであり、世の中に多く存在しているであろうテストコードがないソフトウェアにとってはむしろこちらのほうが重要なスキルであり、単にリファクタリングをおこなうことよりもさらに多くの知識と高い技術が必要になります。

今回は実際の Rails アプリケーションで陥りがちな 2 つのアンチパターンを例にとってその具体的な改善方法を紹介しましたが、技術的負債の返済方法は他にも多くのテクニックやアプローチがあります。これを読んだみなさんがこれをきっかけにそれらを学び、技術的負債を適切にマネジメントしてサービス価値の最大化につながれば幸いです。

推薦書籍

テストコードがないコードをリファクタリング可能にするテクニックについては先に紹介した「レガシーコード改善ガイド」に数多く紹介されていますし、その後のリファクタリングについてはマーチン・ファウラー氏の「リファクタリング」を参考にすれば多くのコードを改善していくことができるでしょう。

ここではさらに役に立つであるであろう 2 冊の書籍をご紹介します。

リファクタリング: Ruby エディション

その名の通り、「リファクタリング」の Ruby 版になります。もともとの書籍は多くのサンプルコードが JavaC++ で記述されており、コンパイラによる静的検査を前提とするテクニックも多いため、Ruby ではそのまま適用できない手法も存在します。この書籍ではそういった部分を Ruby ではどうするかという観点から紹介されているため、Ruby を利用しているエンジニアは必見の書籍になるかと思います。

リファクタリング:Rubyエディション

リファクタリング:Rubyエディション

ソフトウェアテスト技法ドリル

リファクタリングが可能になればその後はテストケースの追加とプロダクトコードの修正を繰り返しながら設計を改善していけばよいのですが、それを続けていくと、どの程度・どういう観点でテストを書けばいいかという問題に直面することがあります。 もちろんこれは TDD 的な開発者視点でのテスト、ユーザ視点での QA テスト等によっても違いますし、テストレベル(ユニットテスト結合テストシステムテスト etc. )によっても違うのですが、そういったテスト観点の洗い出しや分析、適切なテスト設計を学ぶためにはこの書籍が最良かと思います。

ソフトウェアテスト技法ドリル―テスト設計の考え方と実際

ソフトウェアテスト技法ドリル―テスト設計の考え方と実際

MERY PASS における MVP の実践

開発プロセス

MERY PASS における MVP の実践

ペロリ MERY PASS チームの libitte です。 今回は我々のチームで実践している MVP について簡単にご紹介します。

目次

  • MERY PASS とは
  • 新サービスで使うと便利な MVP
  • MERY PASS での MVP の実践例
  • おわりに

MERY PASS とは

MERY PASS はネイル、マツエク、リラクなどのビューティー系サービスがどれでも毎回 3000 円で受けられるサブスクリプションサービスです。

f:id:libitte:20160429121824p:plain

月額 980 円で会員になることができ、その特典として、MERY PASS 内に掲載されているネイル、マツエク、リラクなどのビューティーサロンでのサービスがどれでも会員限定価格 3000 円で受けられるものになっています。

美容サロンに対しては、空席とブランディングの支援を行い、更に費用は一切無料で掲載させていただいています。

現在、渋谷エリアを中心としてサービスを展開中で、今後も順次拡大していく予定です。

新サービスで使うと便利な MVP

新サービスを作るとき、まず最初に大きな課題を定めたうえで、「oo によってその課題を解消できるのではないか」 という仮説を立てます。

そして、その仮説を検証するためにプロダクトを開発し、提供してみて、ユーザーからフィードバックをもらいます。 このようなサイクルを何度も回すことで、ユーザーが満足するプロダクトが形作られます。

MERY PASS の場合、最初の仮説は例えば以下のようなものでした。

  • 一定以上のサロンは空席を抱えており、一定のコストを払ってでもその空席を埋めたい、と考えるのではないか
  • ビューティー系サービスのユーザーは、サービスの利用料に課題を感じており、利用料を抑えられるのであればもっと利用したい、と考えているのではないか

サービス立ち上げ時、これらの仮説はより早く検証されるべきものであり、その時に用いた有力な開発手法が MVP でした。

MVP (minimum viable product) とは、仮説を検証するために、最小限の労力と時間で開発できるものを指します。

MVP を活用することで、仮説−構築−計測のサイクルを回す時間を最小限に抑えることができ、その結果より速いサイクルで学習し、プロダクトの軌道修正を行うことができます。

このような特長をもつため、MVP は高い品質でつくり上げることは重要視されません。

この考えはもしかしたら、大きなプロダクトを開発されている方であると違和感をお持ちになるかもしれません。 顧客が価値を認めていて特性が既にわかっているプロダクトの開発において、品質は非常に重要だからです。プロダクトの先に多くの顧客がいて、彼らに最も直接的な影響をあたえるものが品質であることを考えると、これは当然のように思われます。

しかし、新サービスにおいては「誰が顧客なのかわからなければ、何が品質なのかもわからない」という前提に基づいています。

まだ顧客にとって必要な機能を探し当てられていない状態なので、顧客にとって有益かどうかわからないものの開発に時間をかける価値はないという立場に立っているのです。

したがって、MVP では検証までのサイクルを最小にし、そのためには高品質であることは重視しない、ということになります。

MERY PASS でのMVPの実践例

ここで、MERY PASS が取り組んでいた MVP を一つご紹介しようと思います。

サロンの空席情報掲載

MERY PASS では

「掲載メニューのページにサロンの空席情報を掲載すれば、予約件数は今まで以上に増えるのではないか」

という仮説を検証するために、空席情報掲載を行いました。

f:id:libitte:20160208112812p:plain

今でこそサロン向け管理画面を提供して入力してもらっているこの空席情報ですが、 MVP 実践当時は、サロンの方に空席情報をチャットツール経由で送ってもらい、それをチーム内の入力部隊が社内向け管理画面から全サロン分ひたすら登録を行う、という運用を行っていました。 (このようなMVPの手法はオズの魔法使いと呼ばれています)

f:id:libitte:20160428122056p:plain

振り返り

このような形式でユーザーに提供することで、提供までの時間を非常に短くすることができました。 この結果、この施策の効果検証を素早く行うことができました(予約件数は今までの数倍に増えました)。

この施策では、効果検証の結果予約件数は今までの数倍に増加することがわかったので、より高品質にこの価値を提供するための仕様決めに時間を割くことができたのでした。

おわりに

本記事では、MVP を活用し、品質よりもユーザーに提供するまでのスピードを重視することで、一定の効果を上げることができました、という事例をご紹介させていただきました。

ちょっと視点を変えるだけで簡単に実践できるかと思いますので、もし新サービスの開発や、効果があるか未知数な機能開発を行う場合など、試してみてはいかがでしょうか。

MERYには社内カメラマンがいる話。

初めまして、MERYのカメラマンの本山です。 え!?カメラマン!?という感じですね。 IT企業にカメラマンが居るのは珍しいかと思います。 これまでは割りとTechieな記事が多かったので、少し路線変更になりますが、お付き合い下さい。

MERYは社内で作成する記事もあり、私はそこで使う写真の撮影/編集をしています。
タイアップ記事、オリジナルコンテンツ記事プレス記事など...etc
撮影対象としては、人物、物(服、化粧品、アクセサリー等)、店舗、イベント...etc

その他、弊社はMERY PASSというサービスも展開しており、そちらで使用する写真であったり、採用ページの写真であったり、写真を使うあらゆる場面でお仕事をさせて頂いております。

MERYの環境と雰囲気

f:id:arikou:20160425181254j:plain

MERYには小さいながらもスタジオルームがあります。
このスタジオルームでは物撮りや人物撮影がすぐに出来るようになっています。
社内にスタジオルームがあることで「写真が必要だけど、無くて作業が進まない。。。」とか「イメージ通りの欲しい写真がストックフォトサイトにない。。。」など、制作あるあるを解決してくれています。

また、私のデスクの隣はCI(Corporate Identity)デザインチームです。
いつでも私の写真をチェック/コメント/フィードバックしてもらえる環境で、
デザイン等で少し質問すれば、字詰めまでしてくれるという、素晴らしい神対応っぷりです。
CIデザインチームの隣で仕事をすることで、MERYのブランドイメージを常に共有・意識しながら制作に取り組むことができています。

社内の雰囲気は、全員が個人を尊重しているなと感じます。
それは仕事面にも良い影響が出ていて、「こうした方が良いんじゃないか」「コッチの方がかわいい」といった意見を全員が言えて、全員が受け止めて考える、そんな雰囲気があります。
個々人の得意な分野をお互いが意識しているので、意見をお互いに求めているというような風潮があります。
写真やビジュアルイメージだと個人の趣向性もあるだろうとは思いますが、そこをフラットに共有し合い、より良い物を制作出来るよう励んでいます。

かわいいは、質とスピードとチームワークがカギ

f:id:arikou:20160425181302j:plain

MERYはユーザーである読者に「かわいい」や「おしゃれ」を届け続けなければなりませんし、読者を魅了し続けるためには、写真やビジュアルイメージというのは常に「良質」なモノでなければなりません。
今日、誰でもスマートフォンで簡単に写真が撮れる時代ですが、適切なライティングを計算した上で、一眼レフカメラで撮影した写真はハイクオリティな状態でユーザーに届けられます。

また、1日に3〜4件の撮影が入ることも多いのですが、エディターやデザイナーに写真を納品するのに時間がかかってしまうのもナンセンスです。
カメラマンのせいで誰かの制作をストップさせてはいけません。
編集作業も現状私が行っているので、クリエイティブ・シンキングはしながらも、効率が上がる手段も考えて、Adobeのアプリケーションを活用して日々制作しています。
MERYの制作現場のスピード感を落とさない、むしろ引っ張るくらいの写真納品スピードを保ちながら、より良いビジュアルイメージを届けられるように仕事しています。

しかし、ビジュアル制作の仕事は私一人で行えるものではありません。
例えば、「こういうテーマで記事を作成したいから、物撮りの写真が欲しい」とエディターの方に言われたら、私は季節はいつぐらいでどういうテーマなのかを尋ねて、ライティングのイメージを共有したり、イメージ画像を頂いたりして、脳内イメージの摺り合わせを行うようにしています。
こうしたちょっとしたコミュニケーションが実は凄く大事で、MERYとして世に出た時の最終形を現場で意識しながら効率的に制作することが出来ています。

まとめ

f:id:arikou:20160425181311j:plain

今回はカメラマンの私視点からMERYの制作現場の雰囲気をお伝えできればと思い、書かせて頂きました。
MERYを運営する株式会社ペロリにはいろんな色(個性/得意分野)をもったメンバーがいて、全員がMERYを通して読者へ「かわいい」や「おしゃれ」を届けるべく日々励んでいます。
ちなみに、ちなみに、ちなみに絶賛採用強化中なのでWantedlyを覗いてみてください。
www.wantedly.com

Sidekiq による非同期処理を Redis の分散ロックで排他制御した話

Redis Sidekiq

MERY のサーバーサイドエンジニアの @saidie です。

MERY では画像アップロードや記事投稿による検索インデックス作成などなど、ユーザからのリクエスト起因で起こる時間のかかる処理の多くを非同期に行うことでレスポンスタイムの向上に努めています。また、重複した非同期処理が並行して走ることによるスループットの劣化を分散ロックを用いた排他制御で緩和する取り組みなども行っています。

MERY は Ruby on Rails を用いて開発されており、非同期処理には Ruby 製のフレームワークである Sidekiq を採用しています。この記事では Sidekiq と Redis による分散ロックを使って、同一の非同期処理が(あまり)重複しないような MERY の非同期処理システムについてご紹介します。

Sidekiq

Sidekiq はクライアントサーバモデルのマルチスレッド型のジョブキューシステムで、Redis をキューとして使っています。クライアントがジョブをエンキューすると、サーバ側で動いているワーカスレッドが順番にジョブを処理していきます。

このようなジョブキューシステムでは処理のスループットが一般的な性能指標となっています。というのも、仮にクライアント側のエンキュー速度が高く、サーバ側の処理が追いつかなくなってしまうと、キューにジョブが溜まり続けてしまいます。結果として、エンキューから処理完了までの時間が長くなり UX を大きく損なったり、最悪の場合は Redis のメモリを使いきって障害に繋がる可能性もあります。そのため、事前にエンキューの頻度とジョブの性質などを考慮しておくことが重要です。

ジョブのユニーク性

ジョブキューシステムを用いた非同期処理の設計においては、ジョブの粒度を比較的小さくすることでシステムをできるだけ疎に保つ傾向があるように思われます。そのため、ワーカー同士は基本的にお互いの存在を知らずに処理を行う形になることが多いです。このような設計だと、異なるワーカーが同じ処理を同時に実行する可能性があり、重複したジョブが頻繁にエンキューされる場合などはスループットが著しく落ちることがあります。そのため、重複したジョブを処理しないような仕組み、言い換えるとジョブのユニーク性を保つ仕組みが必要なケースがあります。

ジョブのユニーク性と一言で言っても、そこにはいくつかの文脈におけるユニーク性が存在します。このあたりについては、Sidekiq でジョブのユニーク性を担保するための gem である sidekiq-unique-jobsREADME である程度まとめられています。6 つほどあるので、それらを図にしてみたのが以下になります。

f:id:sai-die:20160131222032p:plain

詳しくは上述の README を参照して欲しいのですが、簡単に説明すると、ジョブにはデキュー前の「待ち」とデキュー後の「実行」の二つの状態があり、これらの状態の組み合わせに対して異なるユニーク性が定義されています (Until timeout を除いて; エンキューから所定の時間が過ぎるまでユニーク)。それぞれ特性が異なるため、処理内容次第で適宜使い分ける必要があります。ここで重要なことは、「待ち」と「実行」状態のユニーク性の担保はそれぞれクライアントとサーバが行うという点になります。よって、前者のユニーク性担保をする場合、エンキューがブロックされる可能性が出てきます。

MERY でジョブのユニーク性を担保する仕組み

f:id:sai-die:20160210085914p:plain

MERY では、とある特定のデータの更新が起こった時に、その更新内容を非同期で別のデータストアに反映する仕組みが必要になったことがあります。平常時は何も問題ないのですが、バルクでデータ更新をする際にジョブが重複していくつも入ることが予想されました。そのため、上図のように重複を考えずナイーブに実装をしてしまうと意味のない更新処理でワーカーが専有されスループットが落ちてしまいます。

また、単に現在処理中のジョブと同一のジョブを捨てるという実装をしてしまうと、処理中に起こった更新の内容を取りこぼしてしまうこととなります (上図の「ダメな実装」)。そのため、ジョブ実行中にそれと同一のジョブを全て捨ててしまうのではなく、一つだけ取っておく必要があります。これは前述の "Until & while executing" に相当するユニーク性となります。

MERY では sidekiq-unique-jobs とは異なる方法でこのユニーク性の担保を行っています。

f:id:sai-die:20160131235744p:plain

まず、実行用と待機用の二つの分散ロック*1を用意し、各ワーカーは以下のような順序でロックの取得を試みます。

  1. 待機用ロックの取得を試みる
    • 取得に失敗したらジョブを終了
  2. 実行用ロックの取得を試みる
    • 取得に失敗したら sleep したのち複数回リトライ
      • 最大リトライ回数に達したら終了
  3. 待機用ロックを解放する
  4. ジョブの処理を行う
  5. 実行用ロックを解放する

このようにすることで、あるジョブを実行するワーカーを一台に制限し、かつ実行中も最大一つのジョブを受け入れることができるようになります。具体的なコードはこんな感じになります。

require 'securerandom'

def lock(redis, wait_key, exec_key)
  token = SecureRandom.uuid

  waiting = redis.set(wait_key, true, nx: true, ex: 60)
  while waiting
    if redis.set(exec_key, token, nx: true, ex: 60)
      redis.del(wait_key)
      waiting = false
      yield
      break
    end
    redis.expire(wait_key, 60)
    sleep(0.1)
  end
rescue
  redis.del(wait_key) if waiting
  raise
ensure
  redis.del(exec_key) if redis.get(exec_key) == token
end

コードをある程度シンプルにしたかったので一部簡略化しており、このままでは実用にはなりませんが (失効時間がハードコードされていたり、競合状態の問題が残っていたり)、上述の Redis の分散ロックを二重で取って処理を実行 (yield にしています) するというロジックをそのまま実装したものになります。以下がこのメソッドを使ったサンプルコードになります。

require 'redis'

redis = Redis.new

threads = []
threads << Thread.new do
  sleep 3
  lock(redis, 'wait key', 'exec key') { puts 'hoge' }
end
threads << Thread.new do
  sleep 1
  lock(redis, 'wait key', 'exec key') { sleep 2; puts 'fuga' }
end
threads << Thread.new do
  sleep 2
  lock(redis, 'wait key', 'exec key') { sleep 2; puts 'piyo' }
end

threads.each(&:join)

2 番目のスレッドが最初に実行用ロックを取得し、次に 3 番目のスレッドが待機用ロックを取得し、1 番目のスレッドは待機用ロックを取得できず即座に処理が終了します。結果は

fuga
piyo

となり、期待通りの挙動を実現することができました!

そもそも sidekiq-unique-jobs gem を使わずに、あえて独自実装を行った大きな理由の一つは、MERY での実装時に "Until & while executing" がサポートされていなかったことなのですが、サポートされた現在でもこちらの方式にはサーバ側でユニーク性の担保が完結するという利点があります。そのため、クライアント側でエンキューがブロックする可能性がなく、またクライアント側にその gem を導入する必要もなくなります。

まとめ

Sidekiq と Redis による分散ロックで重複ジョブを取り除いた MERY の取り組みについての話でした。

そもそも重複ジョブが多数投入されても札束で殴る (ワーカをスケールアウトする) というモダンな手法でなんとかなる話ではあるのですが、インフラコスト削減という建前の中に Redis で遊びたいという筆者の個人的な趣味をそっと包み込んだというところもないとは言い切れないです。 ただし、パフォーマンスを追求するために一種のロック機構を組み込むというのは止むを得ないと思っていて、その時 Redis を分散ロックとして使うのは理にかなった選択だと考えています。

株式会社ペロリでは女の子のかわいいのために堅牢な分散システム基盤の設計・開発・運用をしたいエンジニアを大募集しています。 www.wantedly.com

*1:Redis を使って実現しています。公式ドキュメントにある http://redis.io/topics/distlock をほぼそのまま実装しています。

TechBeer!

イベント

こんにちは、開発部のzooです。MERY EC チームのサーバーサイドを担当しております。

この記事では、先日、株式会社エウレカ様と合同で行った f:id:zoopon:20160411200916p:plain:h15 のご報告と、そもそもペロリで行われている f:id:zoopon:20160411200916p:plain:h15 というイベントのご紹介をさせていだきます。

f:id:zoopon:20160411200916p:plain:h24とは?

エンジニアが集まると様々なイベントが自然発生的に生じると思います。ペロリでも様々なエンジニアの活動があります。『朝活』や『エンジニアランチ』などいろいろあるのですが、 f:id:zoopon:20160411200916p:plain:h15 もそれらのエンジニアの活動の1つです。

f:id:zoopon:20160411200916p:plain:h15 の要素を箇条書きにすると次のようになります。

  • お酒を飲みつつ技術的な話をするペロリの自主的なイベント
  • 端的にいうとLT(ライトニングトーク)会
  • 開催頻度は1ヶ月に1回くらい
  • 発表はだいたい5人くらいで2~3時間
  • 広くはない会議室で行われる
  • カンパ制
  • 今年の1月から開催されている
  • だいたいお酒が回っているので半分以上は覚えていないw

LTの内容は様々で、

  • 普段の業務の中で得た知見
  • 朝活での学び
  • 業務に関連する知識
  • ちょっとしたTips

などの共有や紹介があります。

これまでの f:id:zoopon:20160411200916p:plain:h15 の紹介をちょっとだけすると

  • 在庫管理について よくある設計のシェア
  • ウェブサイト構築におけるSEOの基礎 canonicalとnoindexの話
  • 負荷試験つらたん
  • Goアプリのテスト実例
  • 特許のお話

などなど、多岐に渡ります。

f:id:zoopon:20160411200916p:plain:h24 への臨み方

なにはともあれお酒を飲む!(もちろん飲めない人は飲まなくて大丈夫です)

お酒は飲んでも飲まなくてもいいですが、飲むと肩の力が抜け、自然と発言が増えると思います。エンジニアで飲みに行くと、自然と議論が白熱する感じ。あんな感じを目指しています!

質疑応答ではなく議論!(になるといいなぁ)

自分が思っていること、他の人が思っていること、いろいろな思いが集まってチームになっています。質疑応答ではなく、議論をすることでお互いを理解でき、チームがより一体となることを目指しています!

図にするとこんな感じ。ポーンとみんなの中に話題を投げ入れてわいわいがやがや。ざっくばらんに技術について話します。

f:id:zoopon:20160411210724p:plain:h200

エウレカ様との合同 f:id:zoopon:20160411200916p:plain:h24 !!!

な、なんと、4月開催の f:id:zoopon:20160411200916p:plain:h15 は株式会社エウレカ様と合同で行わせていただきました!

f:id:zoopon:20160411160228p:plain:w400

場所はエウレカ様のオフィスにある、とても広くとても綺麗なセミナースペース。

発表は各社から6名ずつ。

最初の発表は、各社の開発責任者にそれぞれの開発チームの紹介やシステム構成、開発環境等について話していただきました。

f:id:zoopon:20160411202525j:plain:w600

こちらは、テクニカルディレクターであるエウレカ 泉森様の発表の様子。システム構成や開発の進め方、オフィスの様子をご紹介いただきました!

その後は、お互いに気になっているところを話してもらったり、普段の業務での知見を話してもらいました。 さっとタイトルのみ紹介すると…

エウレカ様の発表

  • エウレカの開発現場
  • pairsにおけるGo言語の利用について
  • テスタブルな設計を妄想してみた
  • multiple services (and multistage-env) with ansible
  • infrastructure as codeの実践とサーバ管理手法
  • CouplesにおけるRxSwiftの活用例

ペロリの発表

いろいろな発表がありましたが、発表内容だけでなく、その周辺領域まで含めて、わいわいがやがやと会場全体で話すことができ、勉強になったと同時に非常に楽しむことができました。そして、3時間の f:id:zoopon:20160411200916p:plain:h15 が終わってみると、なんと発表の進捗率が66%という結果に。想像の10倍以上発表後のトークが盛り上がり、全員発表することができませんでした!!!(発表の時間がなかった皆様、本当に申し訳ありません

最後は2社での記念撮影をして解散。エウレカの皆様、本当にありがとうございました。

f:id:zoopon:20160411160040j:plain:w600

きっと第2回合同 f:id:zoopon:20160411200916p:plain:h15 が開かれると思います!

おわりに

この記事では f:id:zoopon:20160411200916p:plain:h15 のご紹介、さらにはエウレカ様との合同 f:id:zoopon:20160411200916p:plain:h15 のご報告をさせていただきました。

ペロリ開発部では渋谷界隈のベンチャー、エンジニアを今以上に盛り上げていきたいという思いがあります。今後も様々な活動をしていきたいと思っております。今後もぜひペロリ開発部にご注目ください!

Serverlessフレームワークで Amazon API Gateway + Lambda のAPIを作った

こんにちは。開発部の朝香です。主にアプリのバックエンドのAPI開発とインフラ周りのことを担当しています。

Amazon API GatewayがTokyoリージョンで使えるようになってから、API Gateway + Lambda + データストアの組み合わせでサーバレスなAPIをって話題がますます盛り上がりをみせてそうな雰囲気がありますね。 先日、GCPでも同じようなサービスがローンチされてましたし、今後ちょっとしたAPIならこういうサービス利用が当たり前になってくるんでしょうか。

多分に漏れず弊社でも使う機会があったので、その内容をブログにしようかと思います。

なぜAPI Gateway + LambdaでAPIを作ることにしたか?

弊社のサービスはMERYというキュレーションプラットフォームなんですが、その記事の中で紹介されている一部の商品を購入することができるというECサービスも提供しています。 その購入方法に後払い決済というのがあり、その決済周りは外部サービスをAPI経由で利用しています。 その外部サービスでは決済情報をwebhookでも受け取れるようになってます。これにより、同期的に決済APIをコールしてレスポンスで決済情報が返ってくるまでの間にサーバが死んでも決済情報を落とさなくても良いようになっています。

そのようなセーフティネット的な用途のデータをどこにサーバ立てて、どう受け取ってどのように取りこぼしをRDBに入れていくかを考えた結果、API Gateway + LambdaでAPIを作って、S3にpayloadを貯めて、バッチでデータの取りこぼしを埋めるという方式を取るようにしました。 データの保存場所としてDynamoDBも検討しましたが、DynamoDBだとwriteのスループット上限の調整と、上限に引っかかった時のリトライ処理を考慮しないといけないので、ここではS3を利用することにしました。

このような方式を取ることにした主な理由は以下の通りです。

  • webhookを受け取る口を作るためだけに、サーバ(冗長化のため2台以上)、アプリケーション、データストアを用意したくない
  • EC関連のDBを置いているvpcに不用意にInboundの口を開けたくない
  • リアルタイムに反映される必要はない
  • 最悪、webhookで拾えなくても外部サービスのwebコンソールからデータは確認できる

どうやって作ったか

API GatewayやLambdaでAPIをつくる上で考えないといけないのが、これらのAWSリソースやLambda Functionのコードをどう管理するかだと思います。 簡単な内部的に利用するAPIならAWSコンソールからポチポチつくって、ちょろっとスクリプトを書けば作れますが、今回のようにサービスに影響のあるものだと、環境構築の自動化やステージングでの動作確認、コードレビュー、情報共有などできる必要があります。

この辺の仕組みを自前で作るのはけっこう大変ですが、最近はLambda Functionをローカルで動作確認したり、API Gatewayのdevelopment、production環境にデプロイまで可能なフレームワークが幾つかあります。 そこで今回はServerless(https://github.com/serverless/serverless )というフレームワークを使ってAPIを構築しました。

Serverlessフレームワーク

元々、JAWS Frameworkというフレームワーク前進としてますが、フルリニューアルしディレクトリ構成も全く変わっていて下位互換もないそうです。 最新のバージョンではLambdaの定期実行eventも作れたり、pluggableだったりと、Lambdaを中心に置いていてfluctよりLambda中心でフルスタックな感じがあります。 今回はServerless v0.5.2で実装しました。

install

Node.js v4以上をインストールして以下を実行すればさくっと入ります。

$ npm install serverless -g

Node.js v5.5.0で試しましたけど、問題なく動作しました。

create project

以下のコマンドを実行し、プロジェクト名、API Gatewayのstage名、AWS Profile、AWSリージョンなどの必要項目を入力するとプロジェクトが作られます。 (以降は、serverlessという長いコマンド名で書いてますが、slsというaliasも用意されています)

$ serverless project create


 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v0.5.2
`-------'

Serverless: Initializing Serverless Project...
Serverless: Enter a name for this project:  (serverless-vyyefd)
Serverless: Enter a new stage name for this project:  (dev)
Serverless: For the "dev" stage, do you want to use an existing Amazon Web Services profile or create a new one?
  > Existing Profile
    Create A New Profile
Serverless: Select a profile for your project:
    default
  > private
Serverless: Creating stage "dev"...
Serverless: Select a new region for your stage:
    us-east-1
    us-west-2
    eu-west-1
    eu-central-1
  > ap-northeast-1
Serverless: Creating region "ap-northeast-1" in stage "dev"...
Serverless: Deploying resources to stage "dev" in region "ap-northeast-1" via Cloudformation (~3 minutes)...
Serverless: Successfully deployed "dev" resources to "ap-northeast-1"
Serverless: Successfully created region "ap-northeast-1" within stage "dev"
Serverless: Successfully created stage "dev"
Serverless: Successfully initialized project "serverless-vyyefd"

また、この時点でAWSには、LambdaのログをCloudWatch Logに送るためのIAM Roleだけ作成されます。なので、AWS ProfileはIAM Roleを作成する権限が必要になります。 上記を作りたくない場合は以下でファイルのみ作成することも可能なようです。

$ serverless project create -c true

出来たプロジェクトのディレクトリ構成は以下のようになってます。

$ tree
.
├── _meta
│   ├── resources
│   │   └── s-resources-cf-dev-apnortheast1.json
│   └── variables
│       ├── s-variables-common.json
│       ├── s-variables-dev-apnortheast1.json
│       └── s-variables-dev.json
├── admin.env
├── package.json
├── s-project.json
└── s-resources-cf.json

出来上がったファイルを簡単に説明しておくと、

  • admin.env
    • プロジェクト作成時に選択した、AWSのprofile情報が入っており、デプロイ時などはこの情報が使われる。(.gitignoreに入っていてgit管理下には入らないです)
  • _meta/resources/s-resources-cf-development-apnortheast1.json
    • CloudFormation Templateファイル
  • _meta/variables
    • プロジェクトの設定ファイル
  • s-project.json
    • _metaのjsonファイルを統合したServerlessのプロジェクトファイル

API作成

以下のようにコマンドを実行すると、オプションで指定したpathのディレクトリが作成され、その下にAPI Gatewayのendpointの設定とそのendpointに紐付けるLambda Functionのテンプレートが作成されます。

$ serverless function create functions/function1
$ tree
.
├── _meta
│   ├── resources
│   │   └── s-resources-cf-dev-apnortheast1.json
│   └── variables
│       ├── s-variables-common.json
│       ├── s-variables-dev-apnortheast1.json
│       └── s-variables-dev.json
├── admin.env
├── functions
│   └── function1
│       ├── event.json
│       ├── handler.js
│       └── s-function.json
├── package.json
├── s-project.json
└── s-resources-cf.json

上記の例ではLambdaのruntimeにNode.jsを選択した結果ですが、python2.7も選択できます。

上記コマンドで作成されるfunctions/fanction1以下のファイルはそれぞれ以下のようになってます。

  • handler.js
    • Lambda Functionのソースコード
    • このコードがzip圧縮されてLambdaにアップロードされる
  • event.json
    • serverless function runコマンドで上記handler.jsの動作確認を行う際にhandler関数に渡すeventのテストjsonデータ
  • s-function.json
  • s-resources-cf.json
    • CloudFomationのjsonファイル
    • Lambda FunctionからDynamDBやS3を使うためのIAM Roleの設定などもここに書く

deploy

デプロイは簡単で以下のコマンドを実行し、API Gatewayのstage, デプロイ対象などを選択するだけで、デプロイ出来ます。

$ serverless dash deploy

これだけで、以下のような感じでfunctions/function1/handler.jsに実装したLambda Functionを呼び出し、レスポンスを返すAPI GatewayGET /function1 が構築されます。

f:id:t-asaka:20160407191634p:plain (Lambda Function)

f:id:t-asaka:20160409170657j:plainAPI Gateway

また、デプロイはendpoint、functionなど個別にデプロイすることも可能です。

わかりにくかったこと

Serverlessの基本的なAPI構築は上記のような流れになりますが、POSTのendpointを作るにはどうすればよいかなどがちょっとわかりにくかったので、その辺の話を書いておきます。

基本的にはドキュメントを見ながらserverlessコマンドでapiとfunctionを作っていけば、WEB APIを構築できますが、上記で構築できるAPIは基本的にはGET /function1のようなGETメソッドでパスがlambda function名のAPIになります。 今回はPOSTでrequest bodyにjsonが入ったrequestを受け取る必要があり、またAPIのpathにパラメータを含めたかったのですが、その辺りはわかりにくかったので、以降にそのやり方を書いておきます。

API GatewayにPOSTメソッドAPIを作る

ServerlessのAPI Gatewayの設定はserverless function createコマンドで作成したfunctionのs-function.jsonに書かれています。

{
  ・・・
    "endpoints": [
    {
      "path": "function1",
      "method": "GET",
      "type": "AWS",
      "authorizationType": "none",
      "authorizerFunction": false,
      "apiKeyRequired": false,
      "requestParameters": {},
      "requestTemplates": {
        "application/json": ""
      },
      "responses": {
        "400": {
          "statusCode": "400"
        },
        "default": {
          "statusCode": "200",
          "responseParameters": {},
          "responseModels": {},
          "responseTemplates": {
            "application/json": ""
          }
        }
      }
    },
   {
      "path": "function1",
      "method": "POST",
      "type": "AWS",
      "authorizationType": "none",
      "authorizerFunction": false,
      "apiKeyRequired": false,
      "requestParameters": {},
      "requestTemplates": {
        "application/json": ""
      },
      "responses": {
        "400": {
          "statusCode": "400"
        },
        "default": {
          "statusCode": "200",
          "responseParameters": {},
          "responseModels": {},
          "responseTemplates": {
            "application/json": ""
          }
        }
      }
    }

  ],
  ・・・
}

APIメソッドをPOSTにするには、このjsonendpoints.methodにPOSTの設定を追加(デプロイ前なら修正)し、デプロイすれば追加できます。

APIのpathにparameterを指定できるようにする

例えば/function1/:idのように、APIのパスでパラメータを受け取りたい場合は、s-function.jsonendpoints.pathを以下のように変更することで出来ます。

{
  ・・・
    "endpoints": [
   {
      "path": "function1/{id}",
      "method": "POST",
      "type": "AWS",
      "authorizationType": "none",
      "authorizerFunction": false,
      "apiKeyRequired": false,
      "requestParameters": {},
      "requestTemplates": {
        "application/json": ""
      },
      "responses": {
        "400": {
          "statusCode": "400"
        },
        "default": {
          "statusCode": "200",
          "responseParameters": {},
          "responseModels": {},
          "responseTemplates": {
            "application/json": ""
          }
        }
      }
    }
  ],
  ・・・
}

また、API Gatewayで受け取ったrequest bodyやpathのパラメータをパースしてLambda Functionのhandlerのeventに渡すにはendpoints.requestTemplates以下のように書けばできます。

・・・
"requestTemplates": {
    "application/json": {
        "body": "$input.json('$')",
        "id" : "$input.params('id')"
},
・・・

(古いバージョンのServerlessでは

"body": "$input.json('$')"

と指定して、デプロイするとAPI GatewayのBody Mapping Templateにも

"body": "$input.json('$')"

と設定され、""の中にrequest bodyのjsonが展開されてしまい、

"body":"{"payload": "value"}"`

jsonパースでエラーが出るため、文字列で

"application/json": "{\"body\": $input.json('$')}"

のように書かないといけませんでしたが、0.5.0では現在は修正されているようです。)

おわりに

ServerlessフレームワークAPI Gateway + LambdaによるWEB APIを構築、デプロイする方法を簡単に紹介しましたが、ServerlessフレームワークにはローカルでのAPI Gatewayの確認や、CORSをサポートするpluginもあり、その辺りはまだ全然試せていないので、またどこかでそのあたりの話とか、fluctとの比較とかもしたいと思います。

Fashion Tech Meetup #2 のレポート

こんにちは。ペロリ開発部の bukuro です。

先日 Fasion Tech Meetup という Fashion x Technology をテーマとした勉強会の第二回を開催しました。 今回は iQON を運営する VASILY 様と FRIL を運営する Fablic 様との共同開催です。

イベント概要はこちら

弊社からは私 bukuro と編集チームが、開発と編集が一体となって MERY のかわいいを作っている話をしました。

また kazutoyo がアプリ UI のテストについて話をしました。


自分は今回発表の中で MERY Partner Program という外部のパートナーさんのコンテンツを MERY に掲載する仕組みについて話をしました。

パートナーさんのコンテンツは RSS の形式で配信され MERY の記事形式に変換されて掲載されます。

この記事では私が話した「開発と編集のちからでたくさんのかわいいを届けた話」 の中であまり触れることのできなかった外部コンテンツの MERY 記事変換に関する技術的な話をしようと思います。

変換の裏側

RSS で配信いただいたコンテンツの内容は HTML の形式で一度 MERY のデータベースに保存されます。 保存されたコンテンツのうち MERY ユーザに届けたい記事を編集が選定し、選定された記事を MERY の記事に変換してアプリに掲載します。 この変換処理は時間がかかることが想定されるため Sidekiq で非同期で処理され、完了すると Slack に通知がいく仕組みになっています。

Sidekiq を選んだ理由

Ruby での非同期処理は Resque と Sidekiq が有名ですが、今回は Sidekiq を選びました。 Resque はマルチプロセス、Sidekiq はマルチスレッドで動作するため、Sidekiq はプロセスを fork をすることによるオーバーヘッドが少なくて済むのが選んだ理由です。

また、今回共同でイベントを運営した VASILY 様が Resque と Sidekiq を比較した記事をブログに書かれていますのでそちらも読んでみてください。 http://tech.vasily.jp/entry/resque_to_sidekiq

変換処理

MERY の記事は以下のように アイテム の一次配列で構成されています。最終的には各パートナーさんのコンテンツもこの形式に変換されます。

# 記事データ
['見出しアイテム', '画像アイテム', 'テキストアイテム'...]

最初に、ネストされた構造データである HTML を Nokogiri を使って各 HTML 要素の一次配列へと変換します。

例えば以下のような HTML を

<html>
  <div>
    <h1>見出し</h1>
    <div>
      <a href="link">リンク</a>
      <text>テキスト</text>
    </div>
  </div>
</html>

このような HTML 要素の一次配列へと変換します。

['<h1>見出し</h1>', '<a href="link">リンク</a>', '<text>テキスト</text>']

次にこの HTML 要素の配列を MERY の記事アイテムと一対一の関係を持った トークン(内部的に HTMLToken と呼ばれているもの)に変換をします。

['見出しtoken', '画像token', 'テキストtoken'...]

この変換には 状態遷移 の考え方を使っています。

状態遷移とは

ある 状態 に対して、特定の 入力 を行うと、決まった 処理 が行われて次の状態に 遷移 すること。

今回の場合だと HTMLToken の値を一つずつ入力値として受け取り状態を遷移させていきます。 状態としては以下の4つがあります。

  • InDescription : テキストが続いている状態
  • DescriptionOrLink : テキストもしくはリンクになり得る状態
  • DescriptionBreak : 一度 <br> をはさんだテキストが続いている状態
  • None : その他

※ 参考図

f:id:peroli_dev:20160401112438p:plain

それぞれの状態において次の HTML 要素の値によって状態が遷移し出力が行われます。

例えば InDescription の状態で <h1> の HTML 要素が来た場合、テキストtoken と 見出しtoken が出力され状態が None に遷移します。

つまり次に受け取る HTML 要素を入力値として受け取って HTMLToken を出力し新しい状態へと変化することで、 HTML の要素を MERY のアイテムと一対一の関係を持った HTMLToken へと作り変えています。

MERY はインラインのリンクに対応していないのですが、DescriptionOrLink という状態を持つことで例えば <a> を受け取った時にそれがインラインのものなのか、独立してリンクとして扱っていいものなのかを次のタグを見て判断することができます。

また、DescriptionBreak という状態を持つことで改行を一つ含んだテキストを改行を挟んで同一のアイテムとして扱うことができます。

このように同じ種類の HTML 要素に対しても前後の要素によって出力を変更することができ、より柔軟にパートナーさんのコンテンツを表現することができます。

具体的な実装としては以下のようになっています。

それぞれの状態ごとにクラスが存在していて、初期状態は None クラスで与えられます。

繰り返し HTML 要素を受け取っては、現状の状態を元に HTMLToken の出力と次の状態の確定を行っています。

context = HtmlContext::None.new
tokens = html_array.map do |element|
  context, tokens = context.next_context(element)
  tokens
end

最後に HTMLToken を MERY のアイテムへと変換します。 ここはポリモーフィックに実装されています。

下のコードは実装の一部です。 HTMLToken はアイテム単位でクラスが実装されていて、tokens はそれらのクラスのインスタンスの配列です。 このように各 HTMLToken のクラスには save_as_mery_item という HTMLToken を MERY のアイテムとして保存するインスタンスメソッドが生えていて token の属するクラスよって適した処理を行い保存します。

※ 実装

class HtmlToken::Headline < HtmlToken
  # HTMLToken を MERY のアイテムとして保存する
  def save_as_mery_item
    ...
  end
end

class HtmlToken::Description < HtmlToken
  def save_as_mery_item
    ...
  end
end

tokens.each do |token|
  token.save_as_mery_item
end

まとめ

いかがでしたでしょうか? この記事では Fashion Tech Meetup ではお話できなかった MERY Partner Program のより技術的なお話をしました。

Fashion Tech Meetup ではこのようなテクニカルな話からもっとビジネスによった内容のものまで、各社の様々な取り組みを知ることが出来ます。 また、会の最後には懇親会の時間も用意されており参加者同士の悩みを共有したり、親睦を深める場となっています。

第三回目の Fashion Tech Meetup の開催も予定していますので、皆様ぜひぜひご参加ください。

© peroli, Inc.