POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

本記事は、原著者の許諾のもとに翻訳・掲載しております。


私たちの救世主DHH™は最近の Full Stack Radioのインタビュー で、 Basecamp の最新版で彼がどのようにRailsのコントローラを書いたかを説明しています。下記は、彼のすばらしい話を書き取ったものです。

これまでに思うようになってきたのは、「RESTの原則に従うには、どのタイミングで新たなコントローラを作るべきかを一度決めたら、ほぼ異例なくその原則を遵守するべきだ」ということです。いつだってその方がうまくいくんです。自分の作ったコントローラの状態を悔やむのは決まって、作ったコントローラの数が少なすぎた時です。多くの処理を任せようとしすぎてしまうんです。

そこでBasecamp 3では、ある程度理にかなったサブリソースがあれば、毎回コントローラを分割していきます。フィルタなどの場合ですね。例えば画面があって、それがある状態になっているとします。もしこれにいくつかのフィルタとドロップダウンを適用したら、違う状態になります。私たちはこうした状態を受け、そこに全く新しいコントローラを作ることがあります。

これを実行するのに私が用いているヒューリスティクスはこうです。コントローラが元々持っているRESTアクションやデフォルトの5つの機能にはないメソッドを付け加えたいと思ったら、いつだって新しいコントローラを作る。それだけでいいのです。

例えば、 InboxController があって、メールの受信ボックス内にあるものを全部表示する index があるとします。もしかすると、別のアクションも入れるかもしれません。例えば、「未送信メールとかも見られた方がいいな。未送信もindexに表示させるか何かしよう」と思うかもしれませんね。そうしたら pendings というアクションを追加します。

class InboxesController < ApplicationController
  def index
  end

  def pendings
  end
end

これはとても一般的なパターンですよね。私も以前はもっとこのパターンを使っていました。

しかし今は「いやいや違う」と思うようになりました。 index という単一のメソッドのみを持つ Inboxes::PendingsController という、新しいコントローラを持つのです。

class InboxesController < ApplicationController
  def index
  end
end
class Inboxes::PendingsController < ApplicationController
  def index
  end
end

私は、今や各コントローラにそれぞれ独自のフィルタを持つスコープがあることの自由に気づきました。(中略)

ということで、ここまででコントローラがものすごく増えてきました。特に名前空間の中ではかなり増えています。例えば、 MessagesController があってそのコントローラの下に Messages::DraftsControllerMessages::TrashesController があって、さらに他の全てのサブコントローラやサブリソースが同じものの中に入っています。これは大きな成功です。

すばらしいですね。

ということで、基本的に彼が言っているのは、コントローラはデフォルトのCRUDアクション indexshowneweditcreateupdatedestroy のみを使うべきだということです。その他のアクションはどれも専用の(それ自体はデフォルトのCRUDアクションしか持たない)コントローラの作成につながるのです。

私がこれについて思うこと

今からここで述べるのは私自身の考えです。考え方が違うというのは全く問題ないことです。いいですか、私を熱狂的でもったいぶった人間だなどと言う前に、まず落ち着いてくださいね。

とにかく、コントローラを書くのにこれまで1年以上“DHHのやり方”をしてきて、今や自分がDHHのファンであるということが分かってよかったです。でも彼が挙げた例はフィルタリングについてのみで、シンプルなコントローラのロジックにはあの例はやりすぎだろうと思います。RESTでの一般的なフィルタリングの仕方はクエリパラメータを使ってやる方法(例えば GET /inboxes?state=pending という感じ)ですから、コードが短くてシンプルであれば私はこの一般的なやり方でいきます(コードが長く複雑になったり、アクションやコンサーンがあまりにも混在してきたりした場合は、彼と同じようにやります)。

しかし私はコントローラを分割するという考えについては文句なしに彼の考え方に賛成です。それにはいくつかの理由があります。

よりシンプルなコードを作れる

この手法を用いることで、コントローラを好きなだけ作ることができます。でも常識的な判断をしてくださいね。コントローラがデフォルトのCRUDアクションしか持たず比較的短くてシンプル(RailsのScaffoldのような感じ)ならば、早まって indexshow などそれぞれを独自のコントローラに切り出す必要なんてないでしょう。

手法を分割するコントローラがいいとされるのは、CRUDアクションしか持っていなくてもだんだんとコントローラが重くなってきた場合です。その時はどうしたらいいのでしょう? そう、重いコードをそれ専用のコントローラに入れてしまえばいいのです。

例えば、以下は私が現在勤めている会社で最も複雑なコントローラの類似コードです(私たちは“比較的薄いモデル、厚いコントローラ”を採用しています。まあ感覚は人それぞれですね)。これはアプリのAPIで製品を購入することができるものです。

class Api::V1::PurchasesController < Api::V1::ApplicationController

  rescue_from Stripe::StripeError, with: :log_payment_error

  def create
    load_product
    load_device
    load_or_create_user

    create_order
    create_payment
    authorize_payment
    confirm_address

    render json: @order, status: :created
  end

  private

  def load_product
    @product = Product.find_by!(uuid: params[:product_id])
  end

  # …

end

パブリックメソッドは1つしかありません(デフォルトのCRUD create アクション)。厚いモデルへの早まった抽象化もなければ、“サービスクラス”もオブザーバもありません。何もありませんからバカなことも起きません。全てがこのコントローラにうまくおさまっています。何が起こっているかを理解するためにたくさんのファイルを確認する必要もありません。コードエディタにあるたった1つのファイルを開くだけでいいのです。また、どんなWebアプリケーションコードでもコントローラがエントリポイントですから、コーディングをする際は、どちらにしろそのファイルを開くことになります。明確なコードです。

でももしかすると、「おいおい、全てを詰め込んだということは、どれだけ長いクラスになってるんだ? 絶対ものすごい行数だろう」などと思う方もいるかもしれません。いいえ、144行です。

これが私の会社で最も複雑なコントローラなのです。最悪の最悪がこれです。おそらくこのコードをもっと小さな塊に分割することはできるでしょうが、これで十分だと私たちは考えています(人それぞれですが)。その他の“厚い”コントローラはこれよりずっとシンプルです。長さは6~103行で、平均すると1つのコントローラあたり コードは15行 程度です(現在150くらいのコントローラがあります)。

コントローラに関するコードが200行以上に及ぶのに、リクエストに関わるコードはほんの少ししかなく、残りは無数のサービスオブジェクトや、オブザーバ、モデルなどに分散している、というプロジェクトに取り組んでいないでしょうか。ここでは、こんなくだらない状況は起こりません。理由はいくつかあり、 rule of 3 (同じコードが3回重複したらリファクタリングするというルール)」などの容易な方法を取り入れていることもありますが、特に大きいのはコントローラを分割する技術を使っていることです。実際には、 重複する方が、間違った方法で抽象化するよりずっと負担が小さくなります 。これは、私が(DHHと同じように)、 DRY (Don’t repeat yourself:重複するべきではないという考え方)や SRP (Single responsibility principle:モジュールやクラスはそれぞれ単一の機能的責任だけを持つべきだという考え方)という概念をクズだと思う理由の一つになっています。これらの概念は過大評価されすぎですし、それ自体を最終目的とするべきではありません。本当に最悪で、それに、それに…。とりあえずこの話題は置いておいて、私たちのトピックに戻りましょう。

コードをそろえる方法

1つのコントローラで実現できるCRUDアクションを少なくできると分かっていたら、すごくうれしいですよね。この1つのおもしろいアクションを見つけるために、長ったらしいコントローラのコードの中で、想像力を働かせたり、むやみに探し回ったり、延々とスクロールしたりなんてことはもう必要ありません。どうやってカスタムコントローラメソッドがルートにマッピングするのか悩まなくてもいいのです。

普通に組織化するのに、 驚かされる のは嫌いです。整然としたコードが好きですし、 設定より規約 を重視する方がいいと思っているので、それがRubyの他のフレームワークより、いまだにRailsを好む理由の一つとなっています。Railsでは全て同じように組織化されているため、一般的な決断に時間がかからず、ビジネス上、本当に大切なエリアでの進捗が迅速になります。

理論上では、あるコードベースから別のコードベースに移ることが可能で、短期間で100パーセントの生産性を有することができます。実際には、放置された状態の”最悪なRailsアプリ”が蔓延していて、これらに出くわすことが多いようです。ある企業では オブザーバ などのアーキテクチャパターンを使い(私の好みではありません)、別の企業では Trailblazer のような全体に追加するアーキテクチャレイヤーを使っているようです(これも好きではありませんが、少し興味深いアイデアです)。また、別のツールを使ったり、独自のカスタムソースを使ったりする企業も出てくるでしょう。

これらは全て、Vanilla JSを使ったRailsアプリで”構造が欠如している”と言われるのが嫌だというのが理由のようです。そのため、他の場所から追加するストラクチャを手に入れようと探しているのです。皆さん、解決策は目と鼻の先にありますよ。コントローラを分けて、デフォルトのCRUDアクションを使うだけです。とても簡単で、経験の少ない開発者にも使いやすい方法です。

Railsは、コントローラを分割するヒューリスティックスをうまく促進できるはずです。 Railsドキュメント では、「いつもリソースの豊富なルーティングを使うべき」と、簡単に書かれているだけです。しかし、CRUDアクションとRESTfulなルーティングを使うというアイデアは、長年に渡って、Railsドキュメントの 特に目立つ大きな特徴 となっていました。マニュアルを読んだことがあれば、カスタムアクションの追加は、全く”Railsらしいやり方”ではないのでは、と一度は頭をよぎったでしょう。コントローラの分割こそが、その不安を拭えるすばらしい答えです。

RESTについて考えさせられる

RESTは統一されていて簡単なので人気があります。(本当に)RESTfulなAPIを理解できたら、他のものを理解するのは更に簡単です。少なくとも理論的にはそうなので、誰か立証してみてください。ビジネスロジックはアプリごとに明確に異なるため、理解する必要があります。しかしそのロジックを実行する方法は同じです。Stripeで 請求を”作ったり” (つまり、お金を得たり)、Twilioで SMSを”作ったり” (つまり、ショートメッセージを送ったり)、Githubで リポジトリを手に入れたり します。

最初のうちはアクションよりもRESTの言葉の使い方に、少し気を取られるかもしれません。”支払う”ではなく、”支払いを作る”と言ったり、”収支に資金を追加する”ではなく、”収支内に資金を作る”と言ったりするからです。最初は少し奇妙な感じがするかもしれません。しかし、SOAPやWSDLなどに戻るくらいなら、いつでもこれくらいの小さな我慢はするつもりです(旧世代のJavaやJEEの開発者なら、私が何を言いたいか分かるでしょう)。

余談ですが、ビジネスロジックのインターフェース全体をRESTに基づいた設計にすると(実装する必要はありません)、簡潔なビジネスロジックを作れると思います。たくさんのアクションが含まれるオブジェクトを持つだけでいいのです。それ以上でもそれ以下でもありません。それでもRESTあればどんな表現も可能ですし、安定していて信頼性があります。これによって制約が解放されるのです。

分割されたコントローラにマッピングするRESTfulなRailsのルーティングの例を挙げましょう。このコントローラはCRUDアクションを使うだけとなっています。

resources :purchases, only: :create

resources :costs_calculations, only: :create

namespace :company do
  resource :account_details, only: :update
  resource :website_details, only: :update
  resource :contact_details, only: :update
end

namespace :balance do
  resources :funds, only: :create
end

resource :bank_account, only: :update

最良のRESTの設計法(特にサブリソースを含む場合)として、私はRESTのアクションとリソースを、いつも最初にメモに書き出すようにしています(たとえば、投稿/収支/資金などです)。実装については気にしません。そして、ネーミングに満足すると、それをRailsのルーティングに変換します。RailsにはすばらしいRESTのサポートがあるので、この作業はすぐ終わります。

まとめ

スコープが特異だったり、ロジックが多すぎたり、関係性が複雑だったりする場合はRailsのコントローラを分割すると、コードに対する有益な影響がたくさん生じます。

これは絶対に抽象化してはいけないということではありません。今はまだやらなくていいということです。どこかのタイミングで、複数のコントローラによって共有しなければならないロジックも出てきます。1つのパブリックメソッドしか持たない分割されたコントローラでも、大きくなりすぎる場合などがあるからです。ここでコンサーンやモデルメソッド、場合によってはバックグラウンド処理、そして時にはサービスオブジェクトまで(これは多くないといいのですが)が動き始めます。

アプリが成長するにつれ、アプリを理解するために費やす時間が増えます。コードがどれだけきれいに書けているかは関係ありません。しかし、コントローラを分割すれば、それを簡単にすることができるのです。