Ruby on Rails
Railsベストプラクティス
サーチ…
繰り返さないでください(ドライ)
きれいなコードを維持するために、RailsはDRYの原則に従います。
可能な限り、可能な限り多くのコードを再利用します。複数の場所で同じコードを複製するのではなく、部分的なコードを使用します。これにより、 エラーが減少し、コードがきれいに保たれ、 一度コードを記述してから再利用するという原則が適用されます。また、同じコードの複数の部分を更新するよりも、ある場所でコードを更新する方が簡単で効率的です。したがって、コードをモジュール化して堅牢にします。
あなたのモデルにコードを書いているので、また、コントローラーでコードを書いているだけなので、Fat Model、Skinny ControllerはDRYです。
# Post model
scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) }
# Any controller
def index
....
@unpublished_posts = Post.unpublished
....
end
def others
...
@unpublished_posts = Post.unpublished
...
end
これはまた、内部メソッドが隠され、APIのやり方でパラメータを渡すことによって変更が達成されるAPI駆動構造につながります。
コンベンションオーバーコンフィグレーション
Railsでは、データベースのコントローラ、ビュー、モデルを見ています。
設定の必要性を減らすために、Railsはルールを実装してアプリケーションの操作を容易にします。独自のルールを定義しても構いませんが、最初はRailsが提供する規約に従うことをお勧めします。
これらの規約は、開発をスピードアップし、コードを簡潔かつ読みやすくして、アプリケーション内での簡単なナビゲーションを可能にします。
大会では初心者の入場障壁も低くなります。初心者が知る必要がないだけでなく、無知で恩恵を受けることができるように、Railsにはたくさんの慣習があります。なぜすべてが正しいのかを知らずにすばらしいアプリケーションを作成することは可能です。
例えば
主キーid
持つorders
というデータベーステーブルがある場合、一致するモデルはorder
と呼ばれ、すべてのロジックを処理するコントローラはorders_controller
という名前にorders_controller
ます。ビューは異なるアクションで分割されます。コントローラーにnew
アクションとedit
アクションがある場合は、 new
ビューとedit
ビューもあります。
例えば
アプリケーションを作成するには、単にrails new app_name
実行しrails new app_name
。これにより、Railsアプリケーションのインフラと基盤を構成する約70のファイルとフォルダが生成されます。
それは以下を含む:
- モデル(データベース層)、コントローラ、およびビューを保持するフォルダ
- アプリケーションの単体テストを保持するフォルダ
- JavascriptやCSSファイルなどのWebアセットを保持するフォルダ
- HTTP 400レスポンスのデフォルトファイル(ファイルが見つからない)
- 他の多く
脂肪モデル、スキニーコントローラ
「脂肪モデル、スキニーコントローラ」は、MVCのMとC部分が理想的に一緒に働く方法を指します。つまり、レスポンスに関係のないロジックは、理想的にはテスト可能な方法でモデルに入るべきです。一方、「スキニー」コントローラは、ビューとモデルの間の単なる素晴らしいインタフェースです。
実際には、これにはさまざまなタイプのリファクタリングが必要ですが、すべてが1つのアイデアになります。コントローラーではなく、モデルの応答ではないロジックを移動することで、再利用を促進するだけでなく可能であれば、要求のコンテキスト外でコードをテストすることも可能にしました。
簡単な例を見てみましょう。次のようなコードがあるとします。
def index
@published_posts = Post.where('published_at <= ?', Time.now)
@unpublished_posts = Post.where('published_at IS NULL OR published_at > ?', Time.now)
end
次のように変更することができます:
def index
@published_posts = Post.published
@unpublished_posts = Post.unpublished
end
次に、ロジックをポストモデルに移動すると、次のようになります。
scope :published, ->(timestamp = Time.now) { where('published_at <= ?', timestamp) }
scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) }
default_scopeに注意してください
ActiveRecordにはdefault_scope
含まれており、デフォルトでモデルを自動的にスコープします。
class Post
default_scope ->{ where(published: true).order(created_at: :desc) }
end
上記のコードは、モデルのクエリを実行すると既に公開されている投稿を処理します。
Post.all # will only list published posts
そのスコープは、無害に見えるものの、複数の隠された副作用があり、あなたが望まないかもしれません。
default_scope
とorder
default_scope
でorder
を宣言したので、 Post
呼び出しorder
はデフォルトを上書きするのではなく、追加のオーダーとして追加されます。
Post.order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."created_at" DESC, "posts"."updated_at" DESC
これはおそらくあなたが望む動作ではありません。最初にスコープからorder
を除外することでこれを無効にすることができます
Post.except(:order).order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."updated_at" DESC
default_scope
とモデルの初期化
他のActiveRecord::Relation
同様に、 default_scope
は、初期化されたモデルのデフォルト状態を変更します。
上記の例では、 Post
はデフォルトでwhere(published: true)
設定されているので、 Post
新しいモデルにも設定されています。
Post.new # => <Post published: true>
unscoped
default_scope
はunscoped
最初に呼び出すことで名目上クリアすることができますが、これには副作用もあります。たとえば、STIモデルを考えてみましょう。
class Post < Document
default_scope ->{ where(published: true).order(created_at: :desc) }
end
デフォルトでは、 Post
に対する照会はScopeされ、 'Post'
を含む列がtype
ます。しかし、 unscoped
は独自のdefault_scope
と一緒にこれをクリアするので、 unscoped
を使用する場合はこれも考慮に入れる必要があります。
Post.unscoped.where(type: 'Post').order(updated_at: :desc)
unscoped
およびモデル関連
Post
とUser
関係
class Post < ApplicationRecord
belongs_to :user
default_scope ->{ where(published: true).order(created_at: :desc) }
end
class User < ApplicationRecord
has_many :posts
end
個々のUser
取得すると、関連する投稿を見ることができます。
user = User.find(1)
user.posts
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' AND "posts"."user_id" = ? ORDER BY "posts"."created_at" DESC [["user_id", 1]]
しかし、あなたはposts
関係からdefault_scope
をクリアしたいので、 unscoped
を使う
user.posts.unscoped
SELECT "posts".* FROM "posts"
これにより、 default_scope
と同様にuser_id
条件が消去されuser_id
。
default_scope
使用例
そのすべてにかかわらず、 default_scope
を使用することが正当な理由があります。
複数のサブドメインが同じアプリケーションから配信されるが、分離されたデータがあるマルチテナントシステムを考えてみましょう。この分離を実現する1つの方法はdefault_scope
です。他の場合の短所はここでは不利になります。
class ApplicationRecord < ActiveRecord::Base
def self.inherited(subclass)
super
return unless subclass.superclass == self
return unless subclass.column_names.include? 'tenant_id'
subclass.class_eval do
default_scope ->{ where(tenant_id: Tenant.current_id) }
end
end
end
あなたがする必要があるのは、リクエストの早い段階でTenant.current_id
を設定することだけです。 tenant_id
を含むテーブルは、追加のコードなしで自動的にスコープになります。レコードをインスタンス化すると、作成されたテナントIDが自動的に継承されます。
このユースケースの重要な点は、スコープが要求ごとに1回設定され、変更されないことです。ここでスコープをunscoped
必要がある唯一のケースは、要求スコープ外で実行されるバックグラウンドワーカーなどの特別なケースです。
あなたはそれを必要としない(YAGNI)
あなたが機能について「YAGNI」(あなたはそれを必要としないだろう)と言うことができるなら、あなたはそれを実装しないほうがよいでしょう。シンプルさに焦点を当てることで、多くの開発時間を節約できます。とにかくこのような機能を実装すると、問題が発生する可能性があります。
問題
Overengineering
製品がそれよりも複雑であれば、それは設計されています。通常、これらの「まだ使用されていない」機能は、意図された方法で使用されることはなく、使用されるとリファクタリングする必要があります。時期尚早の最適化、特にパフォーマンスの最適化は、しばしば設計上の決定につながり、将来的に間違っていることが判明します。
コードブロッティング
Code Bloatは不要な複雑なコードを意味します。これは、例えば、抽象化、冗長性、または設計パターンの誤った適用によって生じ得る。コードベースは、理解しにくく、混乱し、維持するのに費用がかかります。
フィーチャークリープ
フィーチャークリープとは、製品のコア機能を超える新しい機能を追加し、不必要に複雑な製品につながることを指します。
長い開発時間
必要な機能を開発するために使用できる時間は、不要な機能を開発するために費やされます。製品の配送に時間がかかります。
ソリューション
キス - それは簡単、愚かなままに
KISSによると、シンプルに設計されていれば、ほとんどのシステムが最適です。シンプリシティは、複雑さを軽減するための主要な設計目標です。これは、例えば、「単一責任原則」に従うことによって達成することができます。
YAGNI - あなたはそれを必要としない
少ないほうがいいですね。すべての機能を考えてください。本当に必要ですか?もしあなたがYAGNIであると思うなら、それを離れてください。それが必要なときに開発する方が良いです。
継続的リファクタリング
製品は着実に改善されています。リファクタリングでは、製品がベストプラクティスに従って実行されていることを確認し、パッチ作業に堕落しないようにすることができます。
ドメインオブジェクト(ファットモデルなし)
「Fat Model、Skinny Controller」は非常に良い第一歩ですが、コードベースが成長し始めるとスケールが上がらなくなります。
モデルの単体責任について考えてみましょう。モデルの単一の責任は何ですか?それはビジネスロジックを保持するのですか?それは非応答関連の論理を保持するのですか?
いいえ、その責任は、パーシスタンス層とその抽象概念を処理することです。
ビジネスロジックは、レスポンスに関連しないロジックや非パーシスタンス関連のロジックと同様に、ドメインオブジェクトに含める必要があります。
ドメインオブジェクトは、問題のドメイン内で1つの責任しか持たないように設計されたクラスです。あなたのクラスで、彼らが解決した問題を「 悲しみの建築 」にしましょう。
実際には、あなたはスキニーのモデル、スキニーのビューとスキニーコントローラに向かって努力する必要があります。ソリューションのアーキテクチャは、選択しているフレームワークの影響を受けてはいけません。
例えば
ストライプ経由でお客様に15%の固定手数料を請求するマーケットプレイスだとします。固定15%手数料を請求すると、注文額に応じて手数料が変わることを意味します。ストライプは2.9%+ 30¢です。
手数料としてお支払いいただくamount*0.15 - (amount*0.029 + 0.30)
です。
このロジックをモデルに書き込まないでください:
# app/models/order.rb
class Order < ActiveRecord::Base
SERVICE_COMMISSION = 0.15
STRIPE_PERCENTAGE_COMMISSION = 0.029
STRIPE_FIXED_COMMISSION = 0.30
...
def commission
amount*SERVICE_COMMISSION - stripe_commission
end
private
def stripe_commission
amount*STRIPE_PERCENTAGE_COMMISSION + STRIPE_FIXED_COMMISSION
end
end
新しい支払い方法と統合するとすぐに、このモデル内でこの機能を拡張することはできません。
また、より多くのビジネスロジックを統合し始めると、 Order
オブジェクトは結束を失い始めます。
永続的な注文の責任から完全に抽象化された手数料の計算で、ドメインオブジェクトを好む:
# app/models/order.rb
class Order < ActiveRecord::Base
...
# No reference to commission calculation
end
# lib/commission.rb
class Commission
SERVICE_COMMISSION = 0.15
def self.calculate(payment_method, model)
model.amount*SERVICE_COMMISSION - payment_commission(payment_method, model)
end
private
def self.payment_commission(payment_method, model)
# There are better ways to implement a static registry,
# this is only for illustration purposes.
Object.const_get("#{payment_method}Commission").calculate(model)
end
end
# lib/stripe_commission.rb
class StripeCommission
STRIPE_PERCENTAGE_COMMISSION = 0.029
STRIPE_FIXED_COMMISSION = 0.30
def self.calculate(model)
model.amount*STRIPE_PERCENTAGE_COMMISSION
+ STRIPE_PERCENTAGE_COMMISSION
end
end
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
@order.commission = Commission.calculate("Stripe", @order)
...
end
end
ドメインオブジェクトを使用すると、次のようなアーキテクチャ上の利点があります。
- ロジックを使用してオブジェクトをインスタンス化するための治具やファクトリは必要ないため、単体テストは非常に簡単です。
- メッセージ
amount
を受け入れるすべてのもので動作します。 - それぞれのドメインオブジェクトを小さく保ち、明確な責任を明確にし、より高い結束性を維持します。
- 追加ではなく、変更による新しい支払い方法で簡単に拡張できます 。
- 各Ruby on Railsアプリケーションで常に増加する
User
オブジェクトを持つ傾向をなくします。
私は個人的にlib
にドメインオブジェクトを置くのが好きです。その場合は、 autoload_paths
に追加することを忘れないでください:
# config/application.rb
config.autoload_paths << Rails.root.join('lib')
また、コマンド/クエリのパターンに従って、より行動指向のドメインオブジェクトを作成することもできます。そのような場合、これらのオブジェクトをapp/commands
に入れるのは、すべてのapp
サブディレクトリが自動ロードパスに自動的に追加されるため、より良い場所になる可能性があります。