Railsでの厚いモデルのリファクタリングの7つのパターン

厚いモデルは保守が困難です。 もちろん、ドメインロジックで散らかったコントローラーよりも優れていますが、原則として、 単一責任原則 (SRP)に違反しています。 「ユーザーが行うことすべて」は単一の責任ではありません。

プロジェクトの開始時には、SRPは簡単にフォローできます。 しかし、時間の経過とともに、モデルはビジネスロジックの事実上の場所になります。 そして2年後、ユーザーモデルには500行を超えるコードと50のメソッドが公開されています。

設計目標は、成長するアプリケーションを小さなカプセル化されたオブジェクトとモジュールにレイアウトすることです。 ファットモデル、スキニーコントローラーはリファクタリングの最初のステップなので、2番目のステップを実行しましょう。

RailsではOOPを使用するのは難しいと思います。 私もそう思いました。 しかし、いくつかの説明と実験の後、RailsはOOPを実際に混乱させないことに気付きました。 Railsの規約は変更する価値はありません。 ただし、Railsが契約を結んでいない場合は、OOPとベストプラクティスを使用できます。



モデルをモジュールに分割しないでください



それなしで行きましょう。 メソッドが1つのモデルのみに接続されている場合、モジュールにメソッドを配置することは承認しません。 この方法でモジュールを使用することは、ベッドの下やクローゼットの中の部屋にあるものすべてを押すようなものです。 もちろん、モデル内のコードは小さくなりますが、そのようなコードのデバッグとリファクタリングは困難です。

リファクタリングについて。



1.値オブジェクトを強調表示する



値オブジェクトは、金額や日付の範囲などの数量を格納するための単純なオブジェクトであり、その等価性は値に依存します。 日付、URI、パス名はRuby標準ライブラリの例ですが、独自に定義することもできます。

Rails Value Objectsは、複数の属性とそれに関連するロジックがある場合に最適なソリューションです。 たとえば、私のSMS交換アプリケーションではPhoneNumberでした。 オンラインストアにはMoneyが必要です。 コード気候には、格付け-クラス格付けがあります。 文字列を使用できますが、評価ではデータとロジックの両方が定義されています:

class Rating include Comparable def self.from_cost(cost) if cost <= 2 new("A") elsif cost <= 4 new("B") elsif cost <= 8 new("C") elsif cost <= 16 new("D") else new("F") end end def initialize(letter) @letter = letter end def better_than?(other) self > other end def <=>(other) other.to_s <=> to_s end def hash @letter.hash end def eql?(other) to_s == other.to_s end def to_s @letter.to_s end end
      
      





また、ConstantSnapshotの評価は次のとおりです。

 class ConstantSnapshot < ActiveRecord::Base #… def rating @rating ||= Rating.from_cost(cost) end end
      
      





このアプローチの利点:



2.サービスオブジェクトの強調表示



アクションが次の場合、サービスオブジェクトを作成します。



たとえば、UserAuthenticatorでUser#authenticateメソッドを出すことができます。

 class UserAuthenticator def initialize(user) @user = user end def authenticate(unencrypted_password) return false unless @user if BCrypt::Password.new(@user.password_digest) == unencrypted_password @user else false end end end
      
      





SessionsControllerは次のようになります。

 class SessionsController < ApplicationController def create user = User.where(email: params[:email]).first if UserAuthenticator.new(user).authenticate(params[:password]) self.current_user = user redirect_to dashboard_path else flash[:alert] = "Login failed." render "new" end end end
      
      





3.フォームオブジェクトの選択



1つのフォームを送信すると、複数のモデルが変更されると、ロジックをフォームオブジェクトに移動できます。 これはaccepts_nested_attributes_forよりもずっときれいです。私見では、一般に削除する必要があります。 会社とユーザーが作成する登録フォームの例を次に示します。

 class Signup include Virtus extend ActiveModel::Naming include ActiveModel::Conversion include ActiveModel::Validations attr_reader :user attr_reader :company attribute :name, String attribute :company_name, String attribute :email, String validates :email, presence: true # … more validations … # Forms are never themselves persisted def persisted? false end def save if valid? persist! true else false end end private def persist! @company = Company.create!(name: company_name) @user = @company.users.create!(name: name, email: email) end end
      
      





Virtusを使用して、ActiveRecordのような動作を持つ属性を取得しました。 そのため、コントローラーでこれを行うことができます。

 class SignupsController < ApplicationController def create @signup = Signup.new(params[:signup]) if @signup.save redirect_to dashboard_path else render "new" end end end
      
      





簡単な場合、このように機能します。 データを保存するためのロジックが複雑な場合は、このアプローチをサービスオブジェクトと組み合わせることができます。 おまけとして:ここでは、検証を配置できますが、モデルの検証を塗り付けることはできません。



4.クエリオブジェクトを強調表示する



モデルを圧迫する複雑なSQLクエリの場合は、クエリオブジェクトを強調表示します。 各クエリオブジェクトは1つのビジネスルールを実行します。 たとえば、放棄されたアカウントを返すクエリオブジェクト:

 class AbandonedTrialQuery def initialize(relation = Account.scoped) @relation = relation end def find_each(&block) @relation. where(plan: nil, invites_count: 0).find_each(&block) end end
      
      





バックグラウンドジョブで使用してメールを送信できます。

 AbandonedTrialQuery.new.find_each do |account| account.send_offer_for_support end
      
      





ActiveRecord ::リレーションはRails 3のファーストクラスのオブジェクトなので、入力パラメーターとしてクエリオブジェクトに渡すことができます。 そして、リレーションとクエリオブジェクトの組み合わせを使用できます。

 old_accounts = Account.where("created_at < ?", 1.month.ago) old_abandoned_trials = AbandonedTrialQuery.new(old_accounts)
      
      





そのようなクラスの隔離されたテストに夢中にならないでください。 テストでオブジェクトとデータベースの両方を使用して、答えが正しいこと、およびN + 1 SQLクエリのような予期しない影響がないことを確認します。



5. [オブジェクトの表示]を選択します



データを表示するためだけにメソッドが必要な場合、そのメソッドはモデルに属するべきではありません。 「たとえば、アプリケーションに音声インターフェイスがある場合、この方法は必要ですか?」と自問してください。 そうでない場合は、ヘルパーまたはビューオブジェクトに持って行きます。

たとえば、コードクライメートのドーナツチャートは、プロジェクトのすべてのクラス(たとえば、 Rails on Code Climate )の評価を示し、プロジェクトコードのスナップショットに基づいています。

 class DonutChart def initialize(snapshot) @snapshot = snapshot end def cache_key @snapshot.id.to_s end def data # pull data from @snapshot and turn it into a JSON structure end end
      
      





ほとんどの場合、1つのビューオブジェクトで、ERBテンプレート(HAML / SLIM)が1つあります。 したがって、今ではRailsでの2ステップビューパターンの適用を理解しています。



6.ポリシーオブジェクトの強調表示



複雑な読み取り操作は、独自のオブジェクトに値する場合があります。 この場合、ポリシーオブジェクトを作成します。 これにより、モデルに直接関連しないモデルからロジックを作成できます。 たとえば、アクティブと評価されているユーザー:

 class ActiveUserPolicy def initialize(user) @user = user end def active? @user.email_confirmed? && @user.last_login_at > 14.days.ago end end
      
      





ポリシーオブジェクトは、1つのビジネスルールを記述します。メールが確認され、2週間前までにログインした場合、ユーザーはアクティブと見なされます。 ユーザーがアクセスできるデータを記述する承認者などの一連のビジネスルールにポリシーオブジェクトを使用できます。

ポリシーオブジェクトはサービスオブジェクトに似ていますが、書き込み操作にはサービスオブジェクトを使用し、読み取りにはポリシーオブジェクトを使用します。 クエリオブジェクトにも似ていますが、クエリオブジェクトはSQLクエリを実行し、ポリシーオブジェクトはメモリにロードされたモデルを使用します。



7.デコレータをハイライトする



デコレータを使用すると、既存のメソッドを使用できます。これらはコールバックに似ています。 デコレータは、特定の条件下でコールバックを実行する必要がある場合、またはモデルに含まれている場合にコールバックを汚染する場合に役立ちます。

ブログに書かれたコメントは、コメント作成者のFacebookウォールに投稿できますが、これは、このロジックをCommentクラスで定義する必要があるという意味ではありません。 コールバックが多すぎるという兆候は、テストが遅くて壊れやすいこと、またはこれらのコールバックを多くの場所で安定させる必要があることです。

たとえば、FacebookのDecoratorで投稿ロジックを引き出す方法は次のとおりです。

 class FacebookCommentNotifier def initialize(comment) @comment = comment end def save @comment.save && post_to_wall end private def post_to_wall Facebook.post(title: @comment.title, user: @comment.author) end end
      
      





そしてコントローラーで:

 class CommentsController < ApplicationController def create @comment = FacebookCommentNotifier.new(Comment.new(params[:comment])) if @comment.save redirect_to blog_path, notice: "Your comment was posted." else render "new" end end end
      
      





デコレータは、既存のメソッドを使用するという点でサービスオブジェクトとは異なります。 FacebookCommentNotifierインスタンスは、Commentインスタンスと同じ方法で使用されます。 Rubyでは、メタプログラミングを使用してデコレーター簡単作成できます。



結論として



Railsアプリケーションのモデルの複雑さを管理するための多くのテクニックがあります。 Railsに代わるものではありません。 ActiveRecordは素晴らしいライブラリですが、それだけに頼るべきではありません。 これらの手法を使用して、モデルからロジックの一部を抽出してみてください。そうすれば、アプリケーションがより簡単になります。

これらのパターンは非常に単純であることに気付くかもしれません。 オブジェクトは単なるRubyオブジェクトです。 これは私があなたに伝えたかったことです。 すべてのタスクをフレームワークまたはライブラリで解決する必要はありません。



元の記事はこちらです。



All Articles