Rails: Service Objectはもっと使われてもいい(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

なお、Aaron Lasseigne氏の言うService ObjectとAvdi Grimm氏の言うService Objectは違うものを指しているのではないかという意見がBPS社内でありました。

Rails: Service Objectはもっと使われてもいい(翻訳)

Avdi Grimm氏は最近のブログ記事で、Service Objectの盛り上がりに苦言を呈していました。Service Objectを擁護しようと思い、Service Objectにどんな欠点があるのか読んでみたくなりました。私の使うこのツールの長所と短所を知りたかったのです。しかし記事の内容は私の期待とは異なり、私の経験と真っ向からぶつかるものでした。

Avdiは冒頭で、PayPalのIPNデータを処理する「重すぎるコントローラ」を取り上げていました。皆さんもこの巨大過ぎるコントローラアクションを記事でご覧になったでしょう。これはよくある問題であり、修正したくなる問題です。彼はService Objectを1つ作成してコントローラのコードのほとんどをそこに移動しました。移動後の構造は次のような感じです。

class IpnProcessor
  def process_ipn(...)
    # ここにコントローラのコードを書く
  end
end

続いてIpnProcessor.new.process_ipnには「コードの匂い」があることを指摘しています。この命名は冗長であり、同義反復的な悪いコードに見えるとのことです。

ipn processor new process ipn

まさしく彼の言うとおりです。この命名はよくないものであり、Service Objectで通常使われている命名法からも外れています。チームでService Objectを使う場合、callまたはrunといったメソッド名で統一するのが普通です。

class IpnProcessor
  def call(...)
    ...
  end
end

ポイントは、publicなメソッドを1つに限定し、クラス名を見ればそのメソッドの動作がわかるようにすることです。IpnProcessorオブジェクトは、アプリで行う操作の1つを表現します。モデル名は名詞で表すべきであるのと同様に、Service Object名は動詞で表されるべきです。

Avdiは続いて、彼がよりよいソリューションと考えるアイデアを提唱しています。

ところで私のアプリでは、アプリのコードに総合的な名前空間を与えるモジュールを1つ作ることがよくあります。このアプリは「perkolator」(訳注: percolator: 濾過器のもじり)と呼ばれていたので、このモジュール名はPerkolatorとしました。

こうして作成されるモジュールはアプリ名を踏襲し、IPN処理のメソッドを1つ持ちます。

module Perkolator
  def self.process_ipn(...)
    # ...
  end
end

このアプローチのメリットは、「責務の増加」や「Service間の結合」をうまく避けられることです。

彼は、定義に不備のあるオブジェクトは危険であると主張しています。そうしたオブジェクトは本質的にあいまいになるため、本当に追加してよいかどうか疑問の残る機能が追加されやすくなってしまいます。その点には私も賛成です。しかしアプリケーション名のモジュールに全てを入れてしまえば、それこそそのモジュールが乱雑なコード置き場になってしまうのではないでしょうか。多くのRails開発者は、そこにふさわしくない機能が山積みされる「夢の島」と化したapp/controllers/application_controller.rbのことを思い出すでしょう。

Service Objectであれば、IpnProcessorオブジェクトは1つしかなく、publicに呼び出せるメソッドも1つしかありません。これなら、そこにメソッドをさらに追加しようとするときに違和感を覚えてコードレビューで相談を持ちかけるでしょう。(メソッドを増やさずに)機能を追加するには、1個のpublicメソッドの機能を増やすしかありませんが、直ちにそれもおかしいと思えてくるでしょう。1つのメソッドにオプション引数を大量に追加する方法は正しくないと感じられるものです。

2番目の大きな懸念は「Service間の結合」です。Serviceにはアプリのさまざまな操作がカプセル化され、最終的にそれらの操作が組み合わせられて動作します。2つの操作がデータベーステーブルを共有したり、2つのステップとしてさらに大きな処理に組み込まれるかもしれません。コントローラで起きているのはこれではないでしょうか?Service Objectをモジュールに置き換えたところで、この点が変わるでしょうか?何も変わりません。

私の経験では、Service Objectはアプリを介したパスを定義するのに有用です。Service Objectを使って次のようにわかりやすい操作のリストを作成できます。

CreateUserCreateGroupAddUserToGroupBanUserFromGroup

ある処理の手順が正確に定まっており、それらの手順をいつもとは異なる方法で互いに結合したい場合は、それらの手順をservicesディレクトリ内のフォルダに配置して、次のように名前空間を与えます。

Purchase::MadePurchase::Redeemed

Service Objectを利用しても、モジュールが利用できなくなるわけではありません。購入で使う承認トークンを理解して扱えるモジュールならおそらく意味があるでしょう。この場合、Service Objectはこのモジュールを承認に活用して他の処理を行えます。もちろん、これは何がしたいかによってまったく変わってきます。ハンマーしか入っていない道具箱は、本当の道具箱ではありません。

私の経験から

わぉ(いい意味の「わぉ」ではありません)。私はとあるスタートアップ企業でずっと働き続けています。開発者は4人で、従業員は20人そこそこでしょうか。

その企業では、あるRailsアプリを何年も前から使っていました。そのアプリは、コンピュータサイエンス専攻の学生たちが学位を終了する前に無償で作ってリリースしたものでした。学生たちは頭脳明晰でしたが、経験についてははなはだ不十分でした。もし私がその場にいたとしても、もっとうまくやれたとは思えません。実際、彼らは動く製品を作ったのであり、収益にもつながっていました。DHHの言う「期待を遥かに上回る」というやつです。

しかし機能の作り込みはだんだん困難になり、バグ修正もどんどんトリッキーになっていきました。サイトの実行速度も以前より落ちてしまい、仕切り直しが必要になりました。

作業は大変でしたが、それでも進捗はありました。会社は成長し、チームも拡大しました。私たちはコードをモジュールに移動し、巨大なオブジェクトのいくつかを分割しました。そしてある日のこと、ある開発者が「Service Objectを使ってみてはどうか」と持ちかけてきました。やがてチームは、その決定が勝利を導いたことに気づきました。

あるグループにユーザーを1人追加する方法が必要だとしましょう。追加するのはUser#join_groupGroup#add_memberでしょうか?違います。追加するのは、グループとユーザーを1つずつ受け取るAddGroupMemberというService Objectです。今度は、送信の必要なメールアドレスについてはどうでしょう。新しいメンバーにはグループから紹介メールを送信すべきであり、グループは新メンバー追加のメールを受け取るべきです。これも問題ありません。グループに新メンバーを追加する処理に含めることができます。

私たちは、このような状況をある程度避ける方法を見つけました。操作を無様な方法でモデルにアタッチするのではなく、それにふさわしい住み家を与えました。私たちはここから最終的にActiveInteractionというgemを作り上げました。このgemは私たちにとって非常にうまく動きましたし、他の人にとっても有用です。ActiveModelが名詞を扱うように、ActiveInteractionでは動詞を扱います。グループで利用できるインターフェイスも統一されました。私たちはこれらの扱い方、エラーハンドリング、呼び方を理解して、作成のための枠組みを手に入れました。

訳注: ActiveInteraction gemは「Commandパターン」を実装したものだそうです。

クラスからの操作の切り出しも素直に行えました。こうしたコードは簡単にテストできます。私たちは巨大なクジラを一口ずつ、おいしく味わいました。

これを手続きとして実現することもできましたが、もしそうしていたら枠組みは失われていたでしょう。ある開発者はRubyエラーをraiseし、別の開発者はエラーをreturnオブジェクトにアタッチし、また別の開発者はタプルの一部として送信するというようにバラバラになっていたことでしょう。チーム開発において、コードの標準化は成功に欠かせない重要な部分のひとつです。

彼が間違っているとも限らない

私はAvdiをとても尊敬していますが、この件に関して彼と私の意見は明らかに異なっています。もしかすると、彼が気づいていることで私がまだ見落としていることがあるのかもしれません。私はこれまでにもいろんなことに気づきましたし、今後も気づくことがあるでしょう。しかし今のところ、彼の主張から学べる点がまだ見つかりません。

私にとって確かなのは、Service Objectは業務を改善してくれたということです。Service Objectを使っている他の人に尋ねてみた結果も、肯定的な経験が圧倒多数でした。私はフリーランス開発者として、コードをきれいにするためにチームにService Objectを導入する価値はおそらく今後数年変わらないと思います。本記事への肯定的な評価が多い方に賭けます。

関連記事

Railsで重要なパターンpart 1: Service Object(翻訳)

Railsで重要なパターンpart 2: Query Object(翻訳)

Ruby: Chain of Responsibilityパターンの解説(翻訳)

Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833

コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。
これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。
かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。
実は最近Go言語が好き。
仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ