UUUM攻殻機動隊

UUUMのエンジニアによる技術ブログです

Railsアプリ開発中に思い出す、StrategyとRackミドルウェア

こんにちは、エンジニアのナカハシです。

最近は、しばらくRailsでのWebアプリ開発に勤しんでいる毎日です。

開発中にStrategyとRackミドルウェアを復習したので、軽くまとめてみました。

Strategyってなんだっけ?

今私が開発に参加しているファンクラブサイトは、複数種類のファンクラブサイトを1つのアカウントでログインできるようにしています。

各ファンクラブサイトのアクセス許可は、共通アカウント基盤に対してOAuth2で行うようにしています。Railsでのこの手のログイン認証を得る場合、対応するomniauth-xxxというgem(omniauth-twitterとかomniauth-facebookなど)を利用すると手軽に実現できるわけですが、それらのライブラリは以下のようなモジュール構造(名前空間)を持っています。

module OmniAuth
  module Strategies
    class Uuumid < OmniAuth::Strategies::OAuth2
      ...

※ 今回は自前のOAuthサーバーに認証を得に行くので、外部gemではなく、自前で Uuumid < OmniAuth::Strategies::OAuth2 という名前で実装しています。

ここで Strategies という名前が登場します。

GOFのデザインパターンにStrategyという名前の手法があるのですが、私は名前だけ覚えていて中身を忘れていました。今一度、ここを復習してみます。

GOFのデザインパターンにおけるStrategy

GOFのStrategyパターンは、ロジックを提供するオブジェクトを使う側と分離し、交換可能にするという設計手法です。

Party クラスに回復役のメンバー( curer )を配置し、回復魔法を実行( cure )を実行する構成を考えてみましょう。

Priest クラスのインスタンスが回復役の場合「ホイミ!」を唱え、 WhiteWiz クラスのインスタンスが回復役の場合は「ケアル!」と唱えることを実現できるようにします。

Rubyで実装するとすれば、以下のようなクラス構成になるでしょう。

f:id:k_nakahashi:20170423231932p:plain

コードで表現すると以下のようになります

class Party
  def initialize(c)
    @curer = c
  end

  def cure
    @curer.cure
  end
end

class Priest
  def cure
    "ホイミ!"
  end
end

class WhiteWiz
  def cure
    "ケアル!"
  end
end

コンストラクタに渡すインスタンスを差し替えるだけで、 Party#cure で実行されるロジックを簡単に差し替えることができます。

これはDependency Injectionなどとほとんど同じ構造ですし、オブジェクト指向プログラミングをしている場合は、無意識のうちにどんどん使うでしょう。

omnioauth-xxxはStrategyパターン??

さて、 Uuumid < OmniAuth::Strategies::OAuth2Strategies という名前空間に属していることもあるので、私はStrategyパターンを利用した設計と考えました。

なので、omnioauth-xxxのインスタンスを保持してロジックを実行するモジュールは、当然それが何なのかについて、大まかに知っているでしょう。(先ほどのRubyコードの例で言えば、 Party クラスは、実装の詳細は知らなくても、回復役に cure と命令させています)

ですが、 binding.pryUuumid < OmniAuth::Strategies::OAuth2 のロジックを実行しているモジュールを調べてみました。すると、以下のクラスから実行されているということがわかりました。

module Bullet
  class Rack
    include Dependency

    def initialize(app)
      @app = app
    end

    def call(env)
      return @app.call(env) unless Bullet.enable?
      ...

Strategyのロジックを呼んでいる箇所は、 @app.call(env) の部分でした。

さて、このBulletは、N+1問題を検出してくれるためのgemです。

そう、注入している Uuumid < OmniAuth::Strategies::OAuth2 とBulletは全く関係ありません。果たしてこれは、GOFのStrategyパターンによる実装といっていいのでしょうか?

GOFのStrategyパターンに関して言えば、重要なエッセンスは「ロジックを独立したインスタンスで提供し、交換可能にする」という部分でしょう。なので、OmniAuthのStrategyは、GOFのStrategyパターンと全く関係がない、ということはないのだろうと思います。(OmniAuthリポジトリのドキュメントをあさっても、「これはStrategyパターンを実装したものですよ」といちいち言及してはいませんので、真相は不明です)

Rackミドルウェア

そもそもなぜ、OmniAuthと全く関係のないBulletに対し、Strategyのインスタンスが注入されているのでしょうか? それは、両者がRackミドルウェアだからです。

Railsアプリは、Rackインターフェイスの要件を満たしています*1。従ってRailsアプリは、Rackミドルウェアの集合体と見ることもできます。

以下のコマンドを打てば、現状のアプリを構成しているRackミドルウェアのスタックを見ることができます。

bundle exec rails middleware

...
use Bullet::Rack
use OmniAuth::Strategies::Uuumid

Railsアプリは、リクエストを受け付けると、このRackミドルウェアスタックを順に実行します。

開発しているアプリに組み込んでいた Uuumid < OmniAuth::Strategies::OAuth2 は、Rackミドルウェアスタック的にはBulletのすぐ下に配置されていました。これなら、OmniAuthのStrategyがBulletという全く関係のないモジュールから呼び出されていることも理解しやすいです。

GOFのStrategyパターンを頭に入れると、「これがStrategyなのか?」と少しわからなくなりますが、疎結合という意味ではむしろ好ましい形の実装といえるのかもしれません。

*1:そのおかげで、RackをサポートするどのWebサーバー上でも動作することができます