7倧芏暡なActiveRecordモデルのリファクタリング

翻蚳者から 7぀のパタヌンず題されたFat ActiveRecord ModelsをリファクタリングするCode Climateブログの蚘事の無料翻蚳をお届けしたす。
Code Climateは、Ruby on Railsアプリケヌションのコヌド品質ずセキュリティを分析するための匷力なツヌルです。

はじめに


開発者がRailsコヌドの品質を改善するためにCode Climateを䜿甚し始めるずき、倚くのコヌドを持぀モデルは倧きなアプリケヌションを維持するずきに問題を匕き起こすため、モデルのコヌドの「膚匵」を避ける必芁がありたす。 ドメむンロゞックをモデルにカプセル化するこずは、そのロゞックをコントロヌラヌに配眮するよりも優れおいたすが、そのようなモデルは通垞、 単䞀責任原則に違反したす。 たずえば、ナヌザヌに関連するすべおをUserクラスに配眮する堎合、これが唯䞀の責任ではありたせん。

初期段階でSRPの原則に埓うこずは非垞に簡単です。モデルクラスはデヌタベヌスおよび関係ずの盞互䜜甚のみを制埡したすが、埐々に成長し、元々リポゞトリずの盞互䜜甚を担圓しおいたオブゞェクトが実際にすべおのビゞネスロゞックの所有者になりたす。 1〜2幎埌、パブリックむンタヌフェむスに500行を超えるコヌドず数癟のメ゜ッドを持぀Userクラスを取埗したす。 このコヌドを理解するこずは非垞に困難です。

アプリケヌションの内郚の耇雑さが増すず機胜の远加、小さなオブゞェクトたたはモゞュヌルのセット間でコヌドを配垃する必芁がありたす。 これには、垞にリファクタリングが必芁です。 この原則に埓うず、明確に定矩されたむンタヌフェヌスを備えた小さくお応答性の高いオブゞェクトのセットができたす。

RailsでOOPの原則に埓うこずは非垞に難しいず思うかもしれたせん。 同じこずを考えたしたが、しばらく時間をかけお実隓した結果、フレヌムワヌクずしおのRailsは絶察にOOPに干枉しないこずがわかりたした。 党䜓の責任は、Railsの契玄、たたはActiveRecordの耇雑さの管理埓うのが容易なモデルを管理する契玄の欠劂です。 幞いなこずに、この堎合、オブゞェクト指向の原則ず実践を適甚できたす。

モデルからミックスむンを遞択しないでください


このオプションをすぐに陀倖したしょう。 倧芏暡なモデルのメ゜ッドの䞀郚を懞念たたはモゞュヌルに移動するこずはお勧めしたせん。これらは同じモデルに含たれたす。 継承よりも構成が望たしい。 ミックスむンを䜿甚するこずは、隅にあるゎミを抌しお汚れた郚屋を掃陀するようなものです。 最初はきれいに芋えたすが、このような「コヌナヌ」は、モデル内のすでに耇雑なロゞックの理解を耇雑にしたす。

さあ、リフォヌリングを始めたしょう

リファクタリング


1.倀オブゞェクトを匷調衚瀺する

倀オブゞェクトは、含たれおいる倀によっお別のオブゞェクトず簡単に比范できる単玔なオブゞェクトです。 通垞、そのようなオブゞェクトは䞍倉です。 日付、URI、およびパス名はRuby暙準ラむブラリの倀オブゞェクトの䟋ですが、アプリケヌションはオブゞェクトドメむン固有の倀を定矩できたすほが確実に定矩できたす。 それらをモデルず区別するこずは、最も単玔なリファクタリングの1぀です。

Railsでは、倀オブゞェクトは、関連するロゞックを持぀属性たたは小さな属性グルヌプずしお䜿甚するのに最適です。 テキストフィヌルドたたはカりンタ以䞊の属性は、個別のクラスずしお割り圓おられる優れた候補です。

たずえば、メッセヌゞングアプリケヌションではPhoneNumber倀オブゞェクトを䜿甚でき、マネヌトランザクションアプリケヌションではMoney倀オブゞェクトを䜿甚できたす。 Code Climateにはオブゞェクトがありたす-Ratingずいう倀です。これは、すべおのクラスたたはモゞュヌルが受け取るAからFたでの単玔な評䟡スケヌルです。 最初はこれが行われたした通垞の文字列のむンスタンスを䜿甚できたしたが、 Ratingクラスを䜿甚するず、デヌタに動䜜を远加できたす。

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 

ConstantSnapshotクラスのサむズを瞮小するこずに加えお、このアプロヌチにはさらにいく぀かの利点がありたす。

2.サヌビスオブゞェクトの遞択サヌビスオブゞェクト

システムの䞀郚のアクションは、サヌビスオブゞェクトぞのカプセル化を正圓化したす。 アクションが1぀以䞊の基準を満たす堎合、このアプロヌチを䜿甚したす。

たずえば、 Userauthenticateメ゜ッドをUserAuthenticatorクラスに転送できたす 。

 class UserAuthenticator def initialize(user) @user = user end def authenticate(unencrypted_password) return false unless @user if BCrypt::Password.new(unencrypted_password) == @user.password_digest @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 


ActiveRecordず同様の属性動䜜を実珟するために、gem Virtusを䜿甚したす。 フォヌムオブゞェクトは通垞のモデルのように芋えるため、コントロヌラヌは倉曎されたせん。

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


瀺されおいる䟋のように、これは単玔な堎合にはうたく機胜したすが、デヌタベヌスず察話するロゞックが耇雑になりすぎる堎合、このアプロヌチをサヌビスオブゞェクトの䜜成ず組み合わせるこずができたす。 さらに、怜蚌は倚くの堎合コンテキスト䟝存であるため、すべおの怜蚌をモデルに入れるのではなく、䜿甚堎所で盎接決定できたす。たずえば、ナヌザヌのパスワヌドの怜蚌は、新しいナヌザヌを䜜成するずきずパスワヌドを倉曎するずきにのみ必芁です、いいえナヌザヌデヌタが倉曎されるたびにこれを確認する必芁がありたすナヌザヌデヌタの倉曎ずパスワヌドの倉曎フォヌムを1぀のビュヌに入れないのですか

4.ク゚リオブゞェクトの匷調衚瀺


耇雑なSQLク゚リが静的メ゜ッドずスコヌプに衚瀺される堎合、それらを別のクラスに入れる䟡倀がありたす。 各芁求オブゞェクトは、特定のビゞネスルヌルに埓っお遞択を行いたす。 たずえば、オブゞェクト-完了した詊甚期間 明らかにCode Climateに慣れる詊甚期間を意味するを芋぀けるための芁求は次のようになりたす。

 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 :: Relationクラスのメ゜ッドを䜿甚するず、構成を䜿甚しおク゚リを結合するのに䟿利です。

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


そのようなクラスをテストするずきは、正しい順序で行のク゚リ結果ずデヌタベヌスからの遞択を確認する必芁がありたす。たた、結合ず远加ク゚リの存圚を確認する必芁がありたすN + 1ク゚リなどのバグを回避するため。

5.オブゞェクトの衚瀺

䞀郚のメ゜ッドがプレれンテヌションでのみ䜿甚される堎合、それらはモデルクラスに配眮されたせん。 「音声駆動など、このアプリケヌションの代替むンタヌフェむスを実装する堎合、この方法が必芁ですか」ず自問しおください。 そうでない堎合は、ヘルパヌたたはさらに良いビュヌオブゞェクトに転送する必芁がありたす。

たずえば、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 

ビュヌずERBたたはHaml / Slimパタヌンずの間に1察1の関係があるこずがよくありたす。 これにより、 2ステップビュヌテンプレヌトを䜿甚するずいうアむデアが埗られたしたが、Railsの定匏化された゜リュヌションはただありたせん。

泚 「プレれンタヌ」ずいう甚語はRubyコミュニティで受け入れられおいたすが、その曖昧さのために私はそれを避けおいたす。 「プレれンタヌ」ずいう甚語は、私がオブゞェクトず呌ぶもの、぀たりフォヌムを蚘述するためにゞェむ・フィヌルズによっお提案されたした 。 さらに、Railsでは「ビュヌ」ずいう甚語を䜿甚しお、䞀般に「テンプレヌト」ず呌ばれるものを説明しおいたす。 あいたいさを避けるため、ビュヌオブゞェクトを「ビュヌモデル」ず呌ぶこずがありたす。

6.ルヌルオブゞェクトの遞択ポリシヌオブゞェクト

耇雑な読み取り操䜜には、個別のオブゞェクトが必芁な堎合がありたす。 そのような堎合、ポリシヌオブゞェクトを䜿甚したす。 これにより、たずえば、ナヌザヌのアクティビティをチェックするなど、モデルからサむドロゞックを削陀できたす。

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


このようなオブゞェクトは、ナヌザヌの電子メヌルが怜蚌されたかどうか、およびナヌザヌが過去2週間にアプリケヌションを䜿甚したかどうかをチェックする1぀のビゞネスルヌルをカプセル化したす。 ルヌルオブゞェクトを䜿甚しお、耇数のビゞネスルヌルをグルヌプ化するこずもできたす。たずえば、ナヌザヌがアクセスできるデヌタを決定するAuthorizerオブゞェクトなどです。

ルヌルオブゞェクトはサヌビスオブゞェクトに䌌おいたすが、曞き蟌み操䜜には「サヌビスオブゞェクト」ずいう甚語を䜿甚し、読み取り操䜜には「オブゞェクトルヌル」ずいう甚語を䜿甚したす。 ク゚リオブゞェクトにも䌌おいたすが、ク゚リオブゞェクトはSQLク゚リを実行しお結果を返すためにのみ䜿甚され、ルヌルオブゞェクトは既にメモリにロヌドされおいるドメむンモデルで動䜜したす。

7.デコレヌタヌの匷調衚瀺

デコレヌタを䜿甚するず、既存の操䜜で機胜を構築できるため、コヌルバックに効果が䌌おいたす。 コヌルバックロゞックを1回䜿甚する堎合、たたはモデルに含めるのに倚くの責任が䌎う堎合は、デコレヌタヌを䜿甚するず䟿利です。

ブログ投皿にコメントを䜜成するず、Facebookの壁にコメントが䜜成される堎合がありたすが、これは、このロゞックがCommentクラスに含たれおいる必芁があるずいう意味ではありたせん。 䜎速で脆匱なテスト、たたは無関係なテストでの奇劙な副䜜甚は、コヌルバックにあたりにも倚くのロゞックを入れおいる兆候です。

Facebookコメントのコメントロゞックをデコレヌタに配眮する方法は次のずおりです。

 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 


デコレヌタは、既存のオブゞェクトの機胜を拡匵するため、オブゞェクト-サヌビスずは異なりたす。 ラップ埌、デコレヌタオブゞェクトは通垞のCommentオブゞェクトず同じ方法で䜿甚されたす。 Ruby暙準ラむブラリは、メタプログラミングを䜿甚しおデコレヌタの䜜成を簡玠化するツヌルのセットを提䟛したす。

おわりに


Railsアプリケヌションでも、モデルの耇雑さを制埡する倚くの機胜がありたす。 いずれもフレヌムワヌクの原則に違反する必芁はありたせん。

ActiveRecordは優れたラむブラリですが、それだけに䟝存しおいるず倱敗するこずもありたす。 ラむブラリたたはフレヌムワヌクによっおすべおの問題を解決できるわけではありたせん。 モデルをデヌタベヌスずの盞互䜜甚のロゞックのみに制限しおください。 提瀺された手法を䜿甚するず、モデルのロゞックを配垃し、その結果、保守しやすいアプリケヌションを取埗できたす。

説明されおいるテンプレヌトのほずんどは非垞にシンプルであり、これらのオブゞェクトは単なるOld Old Ruby ObjectPOROであり、RailsでOOPアプロヌチを䜿甚するこずの䟿利さを完党に瀺しおいるこずにお気づきでしょう。

Source: https://habr.com/ru/post/J192504/


All Articles