Ruby on Rails
Рекомендации по Rails
Поиск…
Не повторяйте себя (DRY)
Чтобы поддерживать чистый код, 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 существует так много соглашений, что новичка даже не нужно знать, но может просто извлечь выгоду из невежества. Можно создавать отличные приложения, не зная, почему все так, как есть.
Например
Если у вас есть таблица базы данных, называемая orders
с id
первичного ключа, соответствующая модель называется order
а контроллер, который обрабатывает всю логику, называется orders_controller
. Вид разделяется на разные действия: если у контроллера есть new
действие и действие edit
, есть также new
и edit
представление.
Например
Чтобы создать приложение, вы просто запускаете rails new app_name
. Это создаст примерно 70 файлов и папок, которые составляют инфраструктуру и основу для вашего приложения Rails.
Это включает:
- Папки для хранения ваших моделей (уровень базы данных), контроллеров и представлений
- Папки для проведения модульных тестов для вашего приложения
- Папки для хранения ваших сетевых ресурсов, таких как файлы Javascript и CSS
- Файлы по умолчанию для ответов HTTP 400 (т. Е. Файл не найден)
- Многие другие
Жирная модель, тощий контроллер
«Fat Model, Skinny Controller» относится к тому, как M и C части MVC идеально работают вместе. А именно, любая логика, не связанная с ответом, должна идти в модели, в идеале, в хорошем, проверяемом методе. Между тем, «худой» контроллер - просто приятный интерфейс между представлением и моделью.
На практике это может потребовать множество различных типов рефакторинга, но все сводится к одной идее: путем перемещения любой логики, которая не связана с ответом модели (вместо контроллера), не только вы продвигаете повторное использование где это возможно, но вы также смогли проверить свой код вне контекста запроса.
Давайте посмотрим на простой пример. Скажем, у вас есть такой код:
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
Поскольку вы объявили order
в default_scope
, вызывающий order
в Post
будет добавлен в качестве дополнительных заказов вместо переопределения значения по умолчанию.
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
, но это также имеет побочные эффекты. Возьмем, к примеру, модель ИППП:
class Post < Document
default_scope ->{ where(published: true).order(created_at: :desc) }
end
По умолчанию запросы к Post
будут областями для type
столбцов, содержащих 'Post'
. Но 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]]
Но вы хотите очистить default_scope
от отношения posts
, так что вы используете unscoped
user.posts.unscoped
SELECT "posts".* FROM "posts"
Это уничтожает условие user_id
а также default_scope
.
Пример использования для default_scope
Несмотря на все это, существуют ситуации, когда использование default_scope
является оправданным.
Рассмотрим систему с несколькими арендаторами, где несколько поддоменов обслуживаются из одного приложения, но с изолированными данными. Одним из способов достижения этой изоляции является использование 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
, автоматически станет областью без дополнительного кода. Мгновенные записи автоматически наследуют идентификатор арендатора, с которым они были созданы.
Важное значение в этом случае состоит в том, что область задается один раз для каждого запроса и не изменяется. Единственные случаи , вы должны unscoped
здесь особые случаи , такие как фон работники , которые работают за пределами области видимости запроса.
Вам это не понадобится (YAGNI)
Если вы можете сказать «YAGNI» (вам это не понадобится) о какой-либо функции, лучше не применять ее. Может быть много времени на разработку, сфокусированное на простоте. Реализация таких функций в любом случае может привести к проблемам:
Проблемы
переустройства
Если продукт сложнее, чем он должен быть, он переработан. Обычно эти «еще не используемые» функции никогда не будут использоваться по-своему, они были написаны и должны быть реорганизованы, если они когда-либо будут использоваться. Преждевременные оптимизации, особенно оптимизация производительности, часто приводят к проектным решениям, которые в будущем будут ошибочными.
Код Bloat
Code Bloat означает ненужный сложный код. Это может происходить, например, путем абстракции, избыточности или неправильного применения шаблонов проектирования. База кода становится трудно понять, запутанной и дорогой в обслуживании.
Функция ползучести
Функция Creep относится к добавлению новых функций, которые выходят за рамки основных функций продукта и приводят к излишне высокой сложности продукта.
Длительное время разработки
Время, которое может быть использовано для разработки необходимых функций, расходуется на разработку ненужных функций. Продукт требует больше времени для доставки.
Решения
KISS - Держите его простым, глупым
Согласно KISS, большинство систем работают лучше всего, если они разработаны просто. Простота должна быть основной целью проектирования, чтобы уменьшить сложность. Это может быть достигнуто, например, с помощью «Принципа единой ответственности».
YAGNI - Вам это не понадобится
Меньше - больше. Подумайте о каждой функции, действительно ли это необходимо? Если вы можете думать о том, что это YAGNI, оставьте это. Лучше развивать его, когда это необходимо.
Непрерывный рефакторинг
Продукт постоянно совершенствуется. С рефакторингом мы можем убедиться, что продукт выполняется в соответствии с лучшей практикой и не вырождается в работу патча.
Объекты домена (больше нет моделей жира)
«Fat Model, Skinny Controller» - очень хороший первый шаг, но он плохо масштабируется, как только ваша кодовая база начинает расти.
Давайте подумаем о единой ответственности моделей. Какова единственная ответственность моделей? Это держать бизнес-логику? Должна ли она придерживаться логики, не связанной с ответом?
Нет. Его ответственность заключается в том, чтобы обрабатывать слой сохранения и его абстракцию.
Бизнес-логика, а также любая логика, не связанная с ответом, и логика, не связанная с сохранением, должны идти в объектах домена.
Объекты домена - это классы, предназначенные для одной ответственности в области проблемы. Пусть ваши классы « Кричат их архитектуру » для проблем, которые они решают.
На практике вы должны стремиться к тощим моделям, тощим представлениям и тощим контроллерам. Структура вашего решения не должна зависеть от структуры, которую вы выбираете.
Например
Предположим, вы являетесь рынком, который взимает фиксированную комиссию в размере 15% для ваших клиентов через Stripe. Если вы взимаете фиксированную комиссию в размере 15%, это означает, что ваша комиссия изменяется в зависимости от суммы заказа, потому что сборы Strip 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
сообщений. - сохраняет каждый объект домена небольшим, с четко определенными обязанностями и с более высокой связностью.
- легко масштабируется с помощью новых способов оплаты путем добавления, а не изменения .
- прекращает тенденцию иметь постоянно растущий объект
User
в каждом приложении Ruby on Rails.
Я лично хотел бы поместить объекты домена в lib
. Если вы это сделаете, не забудьте добавить его в autoload_paths
:
# config/application.rb
config.autoload_paths << Rails.root.join('lib')
Вы также можете предпочесть создавать объекты домена более ориентированными на действия, следуя шаблону Command / Query. В таком случае размещение этих объектов в app/commands
может быть лучше, поскольку все подкаталоги app
автоматически добавляются в путь автозагрузки.