Service Objectがアンチパターンである理由とよりよい代替手段(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Why Service Objects are an Anti-Pattern — INTERSECT 原文公開日: 2018/03/06 著者: Jared White サイト: Intersect — Whitefusion社の技術ブログです。 whitefusion.ioより 画像は元記事からの引用です。 Service Objectがアンチパターンである理由とよりよい代替手段(翻訳) 近年、RailsアプリにService Objectを追加するメリットを説く記事が次から次へと量産されています。私は本記事において、それがなぜ正しくないかを述べたいと思う次第であります。もっとよい方法はあるのです。 私はこれまで、Service Objectに関するネット上の議論にときおり参加しては、まっとうな問題に対する解決方法としてなぜService Objectが正しくないのかという見解を述べてきました。実際、私は多くの場合においてService Objectよりもっとよい解決方法があると考えるのみならず、Service Objectはオブジェクト指向設計原則への配慮が損なわれている兆候を示すアンチパターンとして取り扱っています。 Service Objectを追加したところであんたのRailsコードベースが今より良くなることはねえと言ったらどうするよ? このような深遠なポイントを細切れのツイートやコメント欄を追って理解するのは大変です。そこで私は、私の見解を正確に表すいくつかの現実的なコードを詳しく追って記事にすることにしました。 私が使う「アンチパターン」という用語については、StackOverflowにうまい説明があったので引用します。 「アンチパターン」とは、ソフトウェア開発においてプログラミング上の悪手と考えられる特定のパターンのことです。デザインパターンが、よい開発手法と一般的に考えられている定型化された共通のアプローチであるのと対照的に、アンチパターンは望ましくないものです。 引用元: https://stackoverflow.com/a/980616 私がService Objectを好きになれない理由を説明するために、昔ある顧客のプロジェクトで当時の開発チームが書いたコードを私が継承した中から、いくつかを詳しく見ていくことにします。そのアプリは未だにpublic betaなので当時の状況についてはあまり立ち入ったことは書けませんが、メディア(画像や動画)にレーティングするソーシャルプラットフォームであり、レーティングは特定のコールバックスタイルの操作(データ処理のアルゴリズム更新や、さまざまなユーザーのタイムラインへのアクティビティの追加など)でトリガされる、とだけ言っておきましょう。 比較的シンプルなデータモデルが1つあり、UserオブジェクトとMediaオブジェクトにbelongs_toで属しているRatingオブジェクトをそこに作成できます。以下のコード例はいずれもproductionファイルそのままではなく、抜粋です。 class Rating < ActiveRecord::Base belongs_to :user belongs_to :media end class Media < ActiveRecord::Base has_many :ratings end 以下を見ればおわかりのように、前任の開発者は、ユーザーから届くレーティングを扱うのにMediaRatingというService Objectをこしらえたのです。これはコントローラから呼ばれます。 class MediaRating def self.rate(user, media, rating) mr = MediaRating.new(user) rating_record = mr.update_rating(media, rating) end def initialize(user) @user = user end def update_rating(media, rating) rating_record = @user.ratings.where(media: media).first if rating_record.nil? # 何か作成する else # 何か更新する end # データのアルゴリズム処理の実行やら # タイムラインでのソーシャル活動の追加やらを行う end end 対応するコントローラのコードは次のとおりです。 media = Media.find(params[:media_id]) rating = params[:rating].to_i MediaRating.rate(current_user, media, rating) このコードが最初に書かれたのはかなり前であることを念頭に置いてください。当時、Service Objectを書きたがる猫やら杓子やらが、表現するAPIの観点から言えば割りと真っ当な部分に居座ってしまったのです。このService Objectを私が書き直してもよいのであれば、きっとこう直すことでしょう。 # これはGemfileに追加 gem ‘smart_init’ class UserMediaRater extend SmartInit initialize_with :user, :media, :rating is_callable def call rating_record = @user.ratings.where(media: @media).first # etc. end end # コントローラからの更新済みコマンド UserMediaRater.call(current_user, media, rating) 問題が始まるとき 私のコードは満更でもありませんよね?そこそこクリーンですし、よく構造化され、楽にテストできます。私がService Objectを書き直したコードを今皆さまが見ているわけですが、問題はこのコードが実際より相当簡略化されたものだという点です。コードベース内の実際のコードは74行ものスパゲッティコードになっていて、メソッドが別のメソッドを呼び、それがまた別のメソッドを呼んでいるという具合です。というのも、データのアルゴリズム処理だのタイムライン更新だのが全部1個のService Objectに押し込められていたからです。実際のフローはこんな具合でした。 コントローラ > Service Object > Rateメソッド > Ratingの更新 > 何か別の更新メソッド + (アルゴリズムの実行 > 関連データの更新)、そしてキャッシュの無効化 + アクティビティをタイムラインに追加 そんなわけで、私がコードベースをエディタで新たに開いて、ユーザーがレーティングしたメディアオブジェクトの作成や更新を行っているコードのブロックをちょっと見ようとすれば、そのたびに本質的でない付属機能を山ほどかき分けて基本のコードパスにたどり着かなければなりませんでした。 「ちょっとちょっとぉ、さすがにその開発者のService Objectの書き方はいくらなんでもマズいでしょ!他のオブジェクトに(おそらく他のService Objectであっても)そうやって処理を押し込めるんじゃなくて、もっとシンプルかつ対象を絞り込んで書けばよかったのに」とおっしゃる方もいるかもしれません。 まあまあ、もうしばらくお付き合いください。標準的なRails MVCパターンに含まれるコードを抽出してService Objectに置く必要があると言われていた本来の理由は、要するに複雑なコードフローを単独の関数に分割しやすくなるからだということでした。しかしここで問題なのは、ルールを強制するものが何もないということです。これっぽっちもないんです!シンプルなService Objectを書くことは間違いなく誰でもできます。しかし、メソッドをたらふく抱えてあっという間にスパゲッティコード化してしまう複雑なService Objectも、同じぐらい間違いなく誰にでも書けるのです。 私が何が言いたいかおわかりでしょうか?つまり本質的にService Objectパターンそのものには、コードベースを読みやすくする力も、メンテしやすくする能力も、concernをうまく分割する手腕もありはしないということです。 あるパターンが、シンプルなものから複雑なものまでほぼ無限に、ほぼあらゆる種類のプログラミングスタイルを無節操に許容して促進するのであれば、そんなものは開発者にとって最早有用なパターンでも何でもありません。 ではどうすればいいんでしょうかね? 私がそこそこの量のコードを書き始めるときは(データを受け取って、他の機能に配慮しながら作成なりレコード更新なりを行わなければならないぐらいの規模感であれば)、大抵の場合最も適切なモデル上に(訳注: わざと)クラスメソッドを1つ書くところから始めます。まま、そう慌てないで。これがService Objectの上を行く優秀なパターンと言いたいのではありません。「ここにうまくはまるパターンがないかなぁ」と私があれこれ考えるよりも前にこれを行っている点にご注目いただきたいのです。 もし仮に、Rating自身のクラスメソッドを用いてメディアのレーティングを行っていたとしたら果たしてどうなっていたか、見てみましょう。 class Rating … Continue reading Service Objectがアンチパターンである理由とよりよい代替手段(翻訳)