Help us understand the problem. What is going on with this article?

翻訳: 似て非なる Phoenix と Rails(原題『Phoenix is not Rails』)

More than 3 years have passed since last update.

Phoenix の開発者である Chris McCord さんが 2015/11/18 に書いた記事「Phoenix is not Rails」の翻訳です。

僕は Rails 未経験の状態で Phoenix を始めたクチなのですが、最近 Rails もやるようになり、両者を比較して考えることが多くなってきたので、いい機会だと思い翻訳してみました。

誤訳があれば編集リクエストを頂けると幸いです。


まえがき

昨年12月、ブライアン1は年次総括で 開発を Elixir と Phoenix に移行する計画を公表しました。それから1年、実際に Rails から Phoenix へ移行してみて分かったのは、この作業はそれほど大変ではないということです。というのも、Phoenix は Rails と非常によく似た作りをしているからです。もちろん、フレームワークのきちんとした理解にはそれなりの学習が必要ですが、Ruby と似ているということは、Ruby 開発チームに Phoenix を抵抗なく使ってもらう大きなポイントとなっています。ただ残念なことに、このことは同時に Phoenix のコア哲学に対する誤解を生じさせてしまってもいます。

例えば、Ruby コミュニティに Ruby 開発者と Rails 開発者がいることは自然なことですが、私はその関係性を Elixir と Phoenix の間では作りたくないと思っています。Phoenix は独自の抽象概念を持っているものの、Pheonix アプリケーションの開発は、結局のところ Elixir アプリケーションの開発であり、Phoenix コードをテストすることは、Elixir 関数をテストすることなのです。この記事ではそのような Phoenix の哲学をはっきりさせるために、Phoenix と Rails の類似点と相違点を比較していこうと思います。

類似点

Phoenix 開発コアチームのほとんどが Rails 開発者なので、Rails の良いところを継承した作りになるのは自然なことと言えます。例えば、以下のような類似点が挙げられます。

  • 両者ともにクライアントサイドからサーバサイドまでの開発にフォーカスしている
  • 両者ともにデフォルトのディレクトリ構造を持つ(ただし、Phoenix は Elixir が推奨する構造に依存している)
  • 両者ともにルーターをトップに据える MVC フレームワークである(ただし、Phoenix は設計上、機能のねじれを生じさせている)
  • 両者ともに RDB に対するデフォルトのスタックを持つ(Rails: sqlite3, Phoenix: PostgreSQL)
  • 両者ともに最善のセキュリティ対策を講じている
  • 両者ともにテストの機構が用意されている

相違点

このような類似点を持ちつつも、両者の根本的な違いがあります。それは、Phoenix 自体はビルドやデバッグ、例外処理やリモートクライアントとの通信といったことについてほとんど関与していないという点です。Phoenix は Elixir と OTP の資産をフル活用していて、Phoenix アプリケーションがその他巨大なアプリケーション基盤の中の一部として存在できるように設計されているのです。これが Rails との一番な違いであり、その影響はあらゆるレイヤに及んでいます。

アプリケーション

そもそも、厳密には "Phoenix アプリケーション" などというものは存在しません。Phoenix プロジェクトとはあくまで Elixir アプリケーションであり、その配信を Phoenix に頼っているにすぎないのです。つまり、ビルドや実行、デプロイは特別なものではなく、ただ、Elixir のやり方をします。

ポイント: シングルトンを持たない

Rails には Rails.application からアクセスされる唯一のアプリケーションが存在します。アプリケーションの起動や設定、さらにはコマンドラインタスクの実行に至るまで、これで行います。それゆえ、原則として2つの Rails アプリケーションを並列で実行することはできませんし、それをやるためには、多大な手間がかかってしまいます。

一方、Phoenix にはグローバルなものや一枚岩のような存在はありません。

新しく作った Phoenix アプリケーションには、エンドポイント、ルーター、そして PubSub サーバが含まれていますが、より多くの機能を追加しても構いません。

全体に影響を及ぼすものがないので、アプリケーションを細かく分割してスケールアウトさせることも簡単です。

ポイント: 起動と停止

Elixir にはアプリケーションの起動と停止のメカニズムを統一させる制約があります。これがどういうものなのか、Phoenix を例にとって説明してみます

1) すべてのアプリケーションは、起動時に呼び出されるモジュールを定義できる

def application do
[mod: {Phoenix, []},
 applications: [:plug, :poison, :logger, :eex],
...]
end

source

2) このモジュールは start/2 という関数を持ち、これが起動時に呼び出される

defmodule Phoenix do
def start(_type, _args) do
  ...
  Phoenix.Supervisor.start_link
end
end

source

3) start/2 はスーパーバイザプロセスの ID を返却する(上記でいう Phoenix.Supervisor.start_link

source

アプリケーションの停止についても、これと似たようなフローを持ちます。この制約により、Phoenix を含む様々な Elixir アプリケーションが、統一された起動と停止のメカニズムを持つことになるのです。

対照的に、Rails の初期化フローは極めて複雑で、たくさんのエクステンションの上に成り立っています。例えば Rails 4.2.2 では、

$ rails c
Loading development environment (Rails 4.2.2)
irb(main):001:0> Rails.application.initializers.length
=> 74

なんと、74 ものコードスニペット(Ruby ブロック)を必要としていて、これがいろいろなファイルに無秩序に書かれているのです!起動と停止の仕組みが分からないと、そのアプリケーションが何を実行しているのかも分かりませんし、また、起動時間が遅くなってきた際の対処も出来なくなります。

ポイント: モニタリングと内部調査

アプリケーションを使えば、実行中のシステムを監査し、耐障害性を持たせ、内部を調査することができます。例えばobserver2のようなツールを使えばアプリケーションをユニットごと、また全体として動作している様子を簡単に見ることができます。

fig1

素晴らしい点として以下が挙げられます。プロジェクトはまずひとつのアプリケーションとして始められ、そのうち自然と複数のアプリケーションに分解されていきます(そうでない場合もありますが)。アプリケーションはそれぞれが単一のノードもしくはひとつのサービス志向アーキテクチャの中で実行されていくでしょう。ランタイム処理は信頼できる適正なパターンの上に組み立てられているので前もってそのためのコストをかけておく必要はありません。Programming Phoenix bookに追加される章にはそれに関する実例が入ります。

リクエストのライフサイクル

ベンチマークで証明されている通り、Phoenix は常識はずれのパフォーマンスを実現しています。これを支えるリクエストとレスポンスの構造は、Rack の上に成り立つ Rails のそれとは大きく異なっています。

ポイント: 理解のしやすさ

Phoenix ではすべてのレイヤで暗黙性よりも明示性を要視します。例えば、Phoenix アプリケーションではリクエストが通る全ての処理が plug として定義されていて、 lib/my_app_endpoint.ex 上で確認できます。Rails ではこのあたりは Rack に委譲されていて暗黙的な存在となっているのですが、Phoenix ではすべてが明示的に記述されているので、エンドポイントとルーターにある plug を見れば、リクエストのライフサイクルを簡単に理解できます。

defmodule MyApp.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  socket "/socket", MyApp.UserSocket
  plug Plug.Static, at: "/", from: :my_app, gzip: false, only: ~w(css images js)
  plug Plug.RequestId
  plug Plug.Logger
  plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"]
  plug Plug.MethodOverride
  plug Plug.Head
  plug Plug.Session, store: :cookie
  plug MyApp.Router
end

リクエストはエンドポイントから始まり、並べられた plug を通過し、ルーターへと伝播されます(このルーター自身も plug として定義されている点に注目してください)。その後、ルーターがまた自身が持つ plug を適応させてコントローラーへと伝播させるのです(ご察しの通り、このコントローラーもまた、plug として定義されています!)。この plug という単一レベルの抽象化こそが、リクエストライフサイクルを極限まで理解・推測しやすいものにしています。また、このシンプルさゆえ、サードパーティパッケージの統合も容易です。

Rails と Phoenix のコントローラーを比較して、plug を用いた関数型的アプローチがいかにわかりやすいか、確認してみましょう。

controller.rb
before_action :find_user

def show
  @post = @user.posts.find(params[:id])
end

def find_user
  @user = User.find(params[:user_id])
end
controller.ex
plug :find_user

def show(conn, %{"id" => id}) do
  post = conn.assigns.user |> assoc(:posts) |> Repo.get(id)
  render conn, "show.html", post: post
end

defp find_user(conn, _) do
  assign(conn, :user, Repo.get(User, conn.params["user_id"]))
end

まず、ベテランの Rails 開発者でない限り、show が暗黙的に render "show.html" を呼んでいることは分からないでしょう。また、全てのインスタンス変数はコントローラからビューに対してコピーされるのですが、これもほとんどの Rails 初学者が気がつかない、複雑な挙動となっています。「設定より規約」とは素晴らしい考え方ではありますが、挙動の明示性を犠牲にしている反面もあるのです。Phoenix では API を使いやすくするために、明示性を追求しています。さらに、オブジェクト指向プログラマであれば、Ruby で暗黙的に利用されている paramsrequestbefore_action にセットされる変数の状態を把握したいと思うでしょう。安心してください、Phoenix ではすべてが明示的です。Web サーバとの通信に関するデータはすべて conn に含まれていて、これが plug 間で引き渡され、最終的にレスポンスを返却しているのです。

ポイント: テストのしやすさ

関数型プログラミングと plug の概念により、コントローラ単体のテスト及び全エンドポイントに対する統合テストは、ただ conn を plug のパイプラインに通して結果をアサートするだけで済みます。さらに、Phoenix ではコントローラのアクションもただの関数であり、暗黙的なステートを一切含みません。つまり、もしそれ単独でテストをしたいのであれば、ただ呼び出すだけで良いのです!

test "sends 404 when user is not found" do
  conn = MyController.show(conn(), %{"id" => "not-found"})
  assert conn.status == 404
end

関数型のおかげで難なくテストの構築ができると思います。エンドポイントに対して統合テストをしたい場合は、以下のように関数のパイプラインを呼び出します。

test "shows users" do
  conn = get conn(), "/users/123"
  assert %{id: "123"} = json_response(conn, :ok)
end

Phoenix のこの考えかたは、コントローラにも共通して適応されます。すべては純粋な関数であって、暗黙的なデータはどこにも潜んでいません!

ポイント: 共通化のしやすさ

Rails では、一度でもコントローラの変数やメソッドを作ってしまうと、それらを Rack レイヤにまで持ち出すことは非常に困難です。コントローラの内部実装はそれだけ入り組んでいるのです。

一方で、Phoenix の plug は純粋な関数なので、そのインプットとアウトプットを理解することは容易です。また、エンドポントやルーター、コントローラといった HTTP スタックが、plug という抽象概念を共有しています。例えば、AdminAuthentication という plug をすべての /admin 以下のリクエストと、DashboardController にも適応させたいとします。この場合、全く同一の plug をそれぞれのレイヤに使えるのです。

defmodule MyApp.Router do
  pipeline :browser do
    plug :fetch_session
    ...
    plug :protect_from_forgery
  end

  pipeline :admin do
    plug AdminAuthentication
  end

  scope "/" do
    get "/dashboard", DashboardController
  end

  scope "/admin" do
    pipe_through [:browser, :admin] # plugged for all routes in this scope

    resources "/orders", OrderController
  end
end

defmodule MyApp.DashboardController do
  plug AdminAuthentication # plugged only on this controller

  def show(conn, _params) do
    render conn, "show.html"
  end
end

plug をあらゆるレイヤに適応できるということは、AdminAuthentication plug をよりきめ細やかなリクエストルールのもとに適応できるということです。Rails でこれをやろうとすると、AdminController の継承を考えるかもしれませんが、これではリクエストに対してどのような変換がされるのかが分かりにくくなってしまいます。どこでどのルールが適応されるのかを知るために継承のツリーを追跡していたら大変ですね。Phoenix なら、ルータのパイプラインを見るだけで済みます。

チャネル

Phoenix の開発は、モダンでハイレベルなリアルタイムウェブ実現への挑戦でした。その成果物であるチャネルでは、プラットフォームに依存しない通信のもと、1台のサーバで何百万ものコネクションを捌くパフォーマンスを実現しました。これは Rails では実現しえなかったことです。

fig2

ポイント: 進化する Web

チャネルは Web をブラウザの概念を超越します。今や、Web はコネクテッドデバイス(携帯電話や時計、スマートトースターなど)全般で使われるまでに進化していて、ブラウザはそのうちの1つにすぎません。 Web 開発者には、この進化に耐えうるフレームワークとプロトコルが必要なのです。チャネルでは iOS や Android, Windows といったプラットフォームに依存しない通信が可能となっています。こちらの動画を見ると、その様子がとてもよくわかると思います。

ポイント: 少ない依存関係

Rails も最近 ActionCable でリアルタイム通信に参入してきましたが、これは Faye, Celluloid, EventMachine, Redis など、大量の依存関係を持っていました。Phoenix のリアルタイム通信に関する機能は、Phoenix を動かしている Erlang に依存しています。そのため、Redis やその他 PubSub サーバを別立てすることなく、リアルタイム通信の機能が使えます。

命名規則

Phoenix には Rails のような厳格な命名規則がありません。

ポイント: 学習コストの低さ

Phoenix ではモジュール名とそれが記述されているファイル名は関係がありません。一方、Rails では UserController のファイル名は user_controller.rb でなければいけません。こういった規則は素晴らしいものではありますが、Phoenix ではあえて採用していません。柔軟に命名すれば良いのです。Rails のこの命名規則は、初学者に混乱をもたらす反面もあります。というのも、Rails はライブラリの読み込みで const_missing というメソッド3を活用しているのですが、先述のファイル名とクラス名の関係性ゆえに挙動がとても複雑で、他のプログラミング言語を書いたことのある人からすると、非常に奇妙奇天烈なものとなっているのです。

Phoenix には、controller や view、その他様々なファイルを入れる web という名前のディレクトリがありますが、これはファイルの更新を検知してリフレッシュ駆動開発を実現するために存在しています。

Phoenix はまた、単数形・複数形の命名ルールも設けていません。Rails ではこれを設けているのですが、モデル名は単数形・コントローラ名は複数形・URL ヘルパーはどちらも...と、初学者からベテランまでもれなく混乱させてしまっています。Phoenixでは、テーブル名やルーティングのパスに複数形を使うことがあるかもしれませんが、基本的には一貫して単数形を使っていれば良いのです。

アセット

Phoenix ではアセットファイルをビルドするために brunch を採用していますが、これは他のビルドツールでも代替可能です。一方、Rails では使えるのはアセットパイプラインのみです。また、Phoenix にはチャネルを使ったライブリロード機能も備わっています。

ポイント: 未来は ES6/ES2015

Phoenix では CoffeeScript ではなく、ES6/ES2015 を推奨しています。CoffeeScript が JavaScript 界隈にもたらした恩恵は計り知れませんが、これから先は ES2015 とその transpiler 4 の時代になると言えます。

ポイント: ライブリロード機能は必須

Phoenix にはライブリロード機能が標準で備わっています。これは、JavaScript や CSS といったファイルに変更が加わると、即座にブラウザに自動反映させるものです。ライブリロード機能は開発を劇的に快適にするため、一度経験したら手放せなくなります

まとめ

Phoenix が Rails の素晴らしいアイディアを多々拝借していることは認めざるを得ません。ただ一方で、Phoenix は Elixir を軸にして、モダンな Web 開発フレームワークとしての独自路線を切り開いていっているのです。


  1. Brian Cardarella。DockYard の CEO 

  2. observerはErlangのプロセス監視ツール。iexからだとiex(1)> :observer.startで起動できる。ただしElixirがwxWidgetライブラリを含めてビルドされている必要がある。 

  3. クラスやモジュールで定義されていない定数(またはクラス)にアクセスしようとしたときに呼び出されるメソッド 

  4. 別の言語にコンパイルされる、いわゆるメタ言語のこと 

mserizawa
Web エンジニアやっています。なめろうが好きです。
smarthr
社会の非合理を、ハックする。
https://smarthr.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした