Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

RubyonRailsのモデルパターンとアンチパターン

Ruby on Railsのパターンとアンチパターンシリーズの2番目の投稿にようこそ。前回のブログ投稿では、一般的なパターンとアンチパターンについて説明しました。また、Railsの世界で最も有名なパターンとアンチパターンのいくつかについても触れました。このブログ投稿では、Railsモデルのアンチパターンとパターンをいくつか紹介します。

モデルに苦労している場合は、このブログ投稿が最適です。モデルをダイエットするプロセスをすばやく実行し、移行を作成するときに避けるべきいくつかのことをしっかりと完了します。すぐに飛び込みましょう。

脂肪 太りすぎのモデル

Railsアプリケーションを開発する場合、それが本格的なRails WebサイトであろうとAPIであろうと、人々はほとんどのロジックをモデルに格納する傾向があります。前回のブログ投稿では、Songの例がありました。 多くのことを行ったクラス。モデルに多くのことを保持すると、単一責任原則(SRP)に違反します。

見てみましょう。

class Song < ApplicationRecord
  belongs_to :album
  belongs_to :artist
  belongs_to :publisher
 
  has_one :text
  has_many :downloads
 
  validates :artist_id, presence: true
  validates :publisher_id, presence: true
 
  after_update :alert_artist_followers
  after_update :alert_publisher
 
  def alert_artist_followers
    return if unreleased?
 
    artist.followers.each { |follower| follower.notify(self) }
  end
 
  def alert_publisher
    PublisherMailer.song_email(publisher, self).deliver_now
  end
 
  def includes_profanities?
    text.scan_for_profanities.any?
  end
 
  def user_downloaded?(user)
    user.library.has_song?(self)
  end
 
  def find_published_from_artist_with_albums
    ...
  end
 
  def find_published_with_albums
    ...
  end
 
  def to_wav
    ...
  end
 
  def to_mp3
    ...
  end
 
  def to_flac
    ...
  end
end

このようなモデルの問題は、曲に関連するさまざまなロジックのダンプグラウンドになることです。メソッドは、時間の経過とともに1つずつゆっくりと追加されると、積み重なっていきます。

モデル内のコードを小さなモジュールに分割することを提案しましたが、そうすることで、コードをある場所から別の場所に移動するだけです。それでも、コードを移動すると、コードをより適切に整理し、読みやすさを低下させた肥満モデルを回避できます。

一部の人々はRailsの懸念を使用することに頼り、ロジックがモデル間で再利用できることに気づきます。私は以前にそれについて書きました、そして、何人かの人々はそれを愛しました、他はそうではありませんでした。とにかく、懸念のある話はモジュールに似ています。どこにでも含めることができるモジュールにコードを移動しているだけであることに注意してください。

もう1つの方法は、小さなクラスを作成し、必要に応じてそれらを呼び出すことです。たとえば、曲変換コードを別のクラスに抽出できます。

class SongConverter
  attr_reader :song
 
  def initialize(song)
    @song = song
  end
 
  def to_wav
    ...
  end
 
  def to_mp3
    ...
  end
 
  def to_flac
    ...
  end
end
 
class Song
  ...
 
  def converter
    SongConverter.new(self)
  end
 
  ...
end

これで、SongConverterができました。 これは、曲を別の形式に変換することを目的としています。独自のテストと変換に関する将来のロジックを持つことができます。また、曲をMP3に変換する場合は、次の操作を実行できます。

@song.converter.to_mp3

私には、これはモジュールや懸念事項を使用するよりも少し明確に見えます。継承よりもコンポジションを使うほうが好きだからかもしれません。私はそれをより直感的で読みやすいと考えています。どちらに進むかを決める前に、両方のケースを確認することをお勧めします。または、必要に応じて両方を選択できます。誰もあなたを止めません。

SQLパスタパルメザン

実生活でおいしいパスタが好きではない人はいますか?一方、コードパスタに関しては、ほとんど誰もファンではありません。そして、正当な理由があります。 Railsmodelsでは、Active Recordの使用状況をすばやくスパゲッティに変えて、コードベース全体を渦巻くことができます。どうすればこれを回避できますか?

それらの長いクエリがスパゲッティの列に変わるのを防ぐように思われるいくつかのアイデアがあります。まず、データベース関連のコードがどこにでもある可能性があるかを見てみましょう。 Songに戻りましょう モデル。具体的には、そこから何かをフェッチしようとするときまで。

class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = Song.where(status: :published)
                .where(artist_id: artist_id)
                .order(:title)
 
    ...
  end
end
 
class SongController < ApplicationController
  def index
    @songs = Song.where(status: :published)
                 .order(:release_date)
 
    ...
  end
end
 
class SongRefreshJob < ApplicationJob
  def perform
    songs = Song.where(status: :published)
 
    ...
  end
end

上記の例では、Songの3つのユースケースがあります。 モデルが照会されています。 SongReporterService内 曲に関するデータを報告するために使用され、具体的なアーティストから公開された曲を取得しようとします。次に、SongControllerで 、公開された曲を取得し、リリース日までに注文します。最後に、SongRefreshJobで 公開された曲だけを取得し、それらを使って何かをします。

これはすべて問題ありませんが、ステータス名を突然releasedに変更した場合はどうなりますか。 または、曲のフェッチ方法に他の変更を加えますか?すべてのオカレンスを個別に編集する必要があります。また、上記のコードはDRYではありません。アプリケーション全体で繰り返されます。これであなたをがっかりさせないでください。幸い、この問題には解決策があります。

Railsスコープを使用できます このコードを乾かします。スコープを使用すると、関連付けやオブジェクトで呼び出すことができる、一般的に使用されるクエリを定義できます。これにより、コードが読みやすくなり、変更が容易になります。しかし、おそらく最も重要なことは、スコープを使用すると、joinsなどの他のActiveRecordメソッドをチェーンできることです。 、where 、等。コードがスコープでどのように見えるか見てみましょう。

class Song < ApplicationRecord
  ...
 
  scope :published, ->            { where(published: true) }
  scope :by_artist, ->(artist_id) { where(artist_id: artist_id) }
  scope :sorted_by_title,         { order(:title) }
  scope :sorted_by_release_date,  { order(:release_date) }
 
  ...
end
 
class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = Song.published.by_artist(artist_id).sorted_by_title
 
    ...
  end
end
 
class SongController < ApplicationController
  def index
    @songs = Song.published.sorted_by_release_date
 
    ...
  end
end
 
class SongRefreshJob < ApplicationJob
  def perform
    songs = Song.published
 
    ...
  end
end

どうぞ。繰り返しコードを切り取ってモデルに入れることができました。しかし、特に太ったモデルや神オブジェクトの場合と診断された場合、これが常にうまくいくとは限りません。モデルにメソッドと責任を追加することは、それほど素晴らしいアイデアではないかもしれません。

ここでの私のアドバイスは、スコープの使用を最小限に抑え、そこにある一般的なクエリのみを抽出することです。私たちの場合、おそらくwhere(published: true) どこでも使用されるので、完璧なスコープになります。他のSQL関連のコードについては、リポジトリパターンと呼ばれるものを使用できます。それが何であるかを調べましょう。

リポジトリパターン

これから紹介するのは、ドメイン駆動型設計書で定義されている1:1リポジトリパターンではありません。私たちとRailsリポジトリパターンの背後にある考え方は、データベースロジックをビジネスロジックから分離することです。 Active Recordの代わりに生のSQL呼び出しを実行するリポジトリクラスを完全に作成することもできますが、本当に必要な場合を除いて、そのようなものはお勧めしません。

私たちにできることは、SongRepositoryを作成することです そこにデータベースロジックを配置します。

class SongRepository
  class << self
    def find(id)
      Song.find(id)
    rescue ActiveRecord::RecordNotFound => e
      raise RecordNotFoundError, e
    end
 
    def destroy(id)
      find(id).destroy
    end
 
    def recently_published_by_artist(artist_id)
      Song.where(published: true)
          .where(artist_id: artist_id)
          .order(:release_date)
    end
  end
end
 
class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = SongRepository.recently_published_by_artist(artist_id)
 
    ...
  end
end
 
class SongController < ApplicationController
  def destroy
    ...
 
    SongRepository.destroy(params[:id])
 
    ...
  end
end

ここで行ったことは、クエリロジックをテスト可能なクラスに分離したことです。また、モデルはスコープとロジックに関係しなくなりました。コントローラーとモデルは薄く、みんな幸せです。右?さて、そこにすべての重い引っ張りをしているActiveRecordがまだあります。このシナリオでは、findを使用します 、これは以下を生成します:

SELECT "songs".* FROM "songs" WHERE "songs"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]

「正しい」方法は、これらすべてをSongRepository内で定義することです。 。私が言ったように、私はそれをお勧めしません。あなたはそれを必要とせず、あなたは完全なコントロールを持ちたいです。 Active Recordから離れるユースケースは、ActiveRecordでは簡単にサポートされないSQL内の複雑なトリックが必要になることです。

生のSQLとActiveRecordについて話すと、私も1つのトピックを取り上げる必要があります。移行のトピックとそれらを適切に行う方法。飛び込みましょう。

移行—誰が気にしますか?

移行を作成するときに、そこにあるコードはアプリケーションの他の部分ほど良くないはずだという議論をよく耳にします。そして、その議論は私にはよく合いません。人々はこの言い訳を使用して、移行時に臭いコードを設定する傾向があります。これは、一度だけ実行されて忘れられるためです。数人で作業していて、全員が常に同期していない場合は、これが当てはまるかもしれません。

現実はしばしば異なります。このアプリケーションは、さまざまなアプリケーションパーツで何が起こるかを知らない多くの人々が使用できます。また、疑わしい1回限りのコードをそこに置くと、データベースの状態が破損したり、移行がおかしくなったりするために、誰かの開発環境が数時間中断する可能性があります。これがアンチパターンかどうかはわかりませんが、注意する必要があります。

移行を他の人にとってより便利にする方法は?プロジェクトの全員が移行しやすいリストを見てみましょう。

常にダウンメソッドを提供するようにしてください

何かがいつロールバックされるかはわかりません。移行を元に戻せない場合は、必ずActiveRecord::IrreversibleMigrationを上げてください。 そのような例外:

def down
  raise ActiveRecord::IrreversibleMigration
end

移行でアクティブレコードを回避するようにしてください

ここでの考え方は、移行を実行する必要があるときのデータベースの状態を除いて、外部の依存関係を最小限に抑えることです。したがって、1日を台無しにする(または保存する)アクティブレコードの検証はありません。プレーンSQLが残っています。たとえば、特定のアーティストのすべての曲を公開する移行を作成しましょう。

class UpdateArtistsSongsToPublished < ActiveRecord::Migration[6.0]
  def up
    execute <<-SQL
      UPDATE songs
      SET published = true
      WHERE artist_id = 46
    SQL
  end
 
  def down
    execute <<-SQL
      UPDATE songs
      SET published = false
      WHERE artist_id = 46
    SQL
  end
end

Songが非常に必要な場合 モデルの場合、移行内でそれを定義することをお勧めします。そうすれば、app/models内の実際のActiveRecordモデルの潜在的な変更から移行を防弾することができます。 。しかし、これはすべてうまくてダンディですか?次のポイントに行きましょう。

データ移行からスキーマ移行を分離する

移行に関するRailsガイドを読むと、次のようになります。

移行はActiveRecordの機能であり、データベーススキーマを進化させることができます。 時間とともに。移行では、純粋なSQLでスキーマの変更を記述するのではなく、RubyDSLを使用してテーブルへの変更を記述できます。

ガイドの要約では、データベーステーブルの実際のデータの編集については言及されておらず、構造についてのみ言及されています。したがって、2番目のポイントで曲を更新するために定期的な移行を使用したという事実は完全には正しくありません。

プロジェクトで同様のことを定期的に行う必要がある場合は、data_migrateの使用を検討してください。 宝石。これは、データの移行をスキーマの移行から分離するための優れた方法です。前の例を簡単に書き直すことができます。データ移行を生成するには、次のようにします。

bin/rails generate data_migration update_artists_songs_to_published

次に、そこに移行ロジックを追加します:

class UpdateArtistsSongsToPublished < ActiveRecord::Migration[6.0]
  def up
    execute <<-SQL
      UPDATE songs
      SET published = true
      WHERE artist_id = 46
    SQL
  end
 
  def down
    execute <<-SQL
      UPDATE songs
      SET published = false
      WHERE artist_id = 46
    SQL
  end
end

このようにして、すべてのスキーマ移行をdb/migrate内に保持します。 ディレクトリと、db/data内のデータを処理するすべての移行 ディレクトリ。

最終的な考え

モデルを扱い、Railsで読みやすくすることは常に苦労しています。うまくいけば、このブログ投稿で、考えられる落とし穴と一般的な問題の解決策を確認できます。モデルのアンチパターンとパターンのリストは、この投稿では完全ではありませんが、これらは私が最近見つけた最も注目すべきものです。

より多くのRailsパターンとアンチパターンに興味がある場合は、シリーズの次の記事にご期待ください。今後の投稿では、RailsMVCのビューとコントローラー側の一般的な問題と解決策について説明します。

次回まで、乾杯!

P.S。 Ruby Magicの投稿をマスコミから離れたらすぐに読みたい場合は、Ruby Magicニュースレターを購読して、投稿を1つも見逃さないでください。


  1. RubyonRailsでスコープを使用する方法

    Railsのスコープとは何ですか?なぜそれが役立つのですか? まあ… スコープは、scopeを使用してRailsモデル内で定義するカスタムクエリです。 メソッド。 すべてのスコープには2つの引数があります : コードでこのスコープを呼び出すために使用する名前。 クエリを実装するラムダ。 このように見えます : class Fruit < ApplicationRecord scope :with_juice, -> { where(juice > 0) } end スコープを呼び出した結果、ActiveRecord::Relationを取得します オブジ

  2. Ruby on Railsとは何ですか?なぜそれが役立つのですか?

    Ruby on Rails(RoRの場合もある)は、最も人気のあるオープンソースのWebアプリケーションフレームワークです。 Rubyプログラミング言語で構築されています。 Railsを使用すると、単純なものから複雑なものまで、アプリケーションの構築に役立ちます。Railsで実行できることには制限がありません。 フレームワークとは何ですか? フレームワークは、ソフトウェアを作成するときに使用する特定の構造を提供するコード、ツール、およびユーティリティのコレクションです。 この構造により、コードがより整理されます。 正しく使うことを学ぶと、作業が簡単になります。 レールは正確に何を