Поиск…


Не повторяйте себя (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 автоматически добавляются в путь автозагрузки.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow