Rubyのクラスメソッドがリファクタリングに抵抗する理由(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Why Ruby Class Methods Resist Refactoring 原文公開日: 2012/11/14 著者: Bryan Helmkamp サイト: https://codeclimate.com/ Rubyのクラスメソッドがリファクタリングに抵抗する理由(翻訳) 私の記事『肥大化したActiveRecordモデルをリファクタリングする7つの方法』に対して、「クラスメソッドでできることをなぜわざわざインスタンスでやるんですか?」という質問をよくいただきました。お答えしましょう。要するに以下が理由です。 私がクラスメソッドよりオブジェクトインスタンスを好む理由は、クラスメソッドはリファクタリングに抵抗するからです。 詳しく説明するために、データを外部アナリティクスサービスと同期するバックグラウンドジョブを例に取ることにします。次をご覧ください。 class SyncToAnalyticsService ConnectionFailure = Class.new(StandardError) def self.perform(data) data = data.symbolize_keys account = Account.find(data[:account_id]) analytics_client = Analytics::Client.new(CC.config[:analytics_api_key]) account_attributes = { account_id: account.id, account_name: account.name, account_user_count: account.users.count } account.users.each do |user| analytics_client.create_or_update({ id: user.id, email: user.email, account_admin: account.administered_by?(user) }.merge(account_attributes)) end rescue SocketError => ex raise ConnectionFailure.new(ex.message) end end このジョブは、ユーザーごとに属性のハッシュをHTTP POSTとして繰り返し送信します。SocketErrorがraiseされるとSyncToAnalyticsService::ConnectionFailureでラップし、こちら側のエラートラッキングシステムで正しく分類されるようにします。 このSyncToAnalyticsService.performメソッドはなかなか複雑で、責務をいくつも抱えています。単一責任原則(SRP)はちょうどフラクタルのように、より細かいレベルでアプリ全体/モジュール/クラス/メソッドに渡って適用されると考えることができます。SyncToAnalyticsService.performは、そのメソッドのさまざまな動作が必ずしも同じ抽象化レベルにないため、Composed Methodパターンには該当しません。 訳注: Composed Methodパターンについては以下もどうぞ。 「文章のように読めるメソッドを作る」Composed Method パターン 解決法のひとつは、Extract Methodパターンを何回か適用してメソッドを切り出すことです。結果は以下のような感じになります。 class SyncToAnalyticsService ConnectionFailure = Class.new(StandardError) def self.perform(data) data = data.symbolize_keys account = Account.find(data[:account_id]) analytics_client = Analytics::Client.new(CC.config[:analytics_api_key]) sync_users(analytics_client, account) end def self.sync_users(analytics_client, account) account_attributes = account_attributes(account) account.users.each do |user| sync_user(analytics_client, account_attributes, user) end end def self.sync_user(analytics_client, account_attributes, user) create_or_update_user(analytics_client, account_attributes, user) rescue SocketError => ex raise ConnectionFailure.new(ex.message) end def self.create_or_update_user(analytics_client, account_attributes, user) attributes = user_attributes(user, account).merge(account_attributes) analytics_client.create_or_update(attributes) end def self.user_attributes(user, account) { id: user.id, email: user.email, account_admin: account.administered_by?(user) } end def self.account_attributes(account) { account_id: account.id, account_name: account.name, account_user_count: account.users.count } end end 元のコードと比べれば少しはマシになりましたが、どうも今ひとつです。これではオブジェクト指向にならず、手続き型プログラミングと関数型プログラミングの不気味な折衷案がオブジェクトベースの世界で立ち往生しているような感じです。さらに、切り出したメソッドはどれもクラスレベルなので、private宣言も簡単にはできそうにありません(おそらくclass << selfのような見苦しい方法に切り替えざるを得ないでしょう)。 私だったら、SyncToAnalyticsServiceの元の実装を目にしたとしてもこんな形でリファクタリングを完了する気にはとてもならないでしょう。代わりに、次のようにリファクタリングを始めるでしょう。 class SyncToAnalyticsService ConnectionFailure = Class.new(StandardError) def self.perform(data) new(data).perform end def initialize(data) @data = data.symbolize_keys end def perform account … Continue reading Rubyのクラスメソッドがリファクタリングに抵抗する理由(翻訳)