こんにちは、エンジニアのナカハシです。
最近は、しばらく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で実装するとすれば、以下のようなクラス構成になるでしょう。
コードで表現すると以下のようになります
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::OAuth2
は Strategies
という名前空間に属していることもあるので、私はStrategyパターンを利用した設計と考えました。
なので、omnioauth-xxxのインスタンスを保持してロジックを実行するモジュールは、当然それが何なのかについて、大まかに知っているでしょう。(先ほどのRubyコードの例で言えば、 Party
クラスは、実装の詳細は知らなくても、回復役に cure
と命令させています)
ですが、 binding.pry
で Uuumid < 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サーバー上でも動作することができます