Ruby on Rails
Mejores Prácticas de Rieles
Buscar..
No te repitas (SECO)
Para ayudar a mantener el código limpio, Rails sigue el principio de DRY.
Implica siempre que sea posible, reutilizar tanto código como sea posible en lugar de duplicar código similar en múltiples lugares (por ejemplo, usar parciales). Esto reduce los errores , mantiene su código limpio y aplica el principio de escribir código una vez y luego reutilizarlo. También es más fácil y más eficiente actualizar el código en un solo lugar que actualizar varias partes del mismo código. Haciendo así tu código más modular y robusto.
También Fat Model, Skinny Controller está SECO, porque usted escribe el código en su modelo y en el controlador solo hace la llamada, como:
# 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
Esto también ayuda a conducir a una estructura basada en API donde los métodos internos están ocultos y los cambios se logran a través de pasar parámetros en forma de API.
Convención sobre configuración
En Rails, te encuentras mirando controladores, vistas y modelos para tu base de datos.
Para reducir la necesidad de una configuración pesada, Rails implementa reglas para facilitar el trabajo con la aplicación. Puede definir sus propias reglas, pero para el principio (y para más adelante) es una buena idea atenerse a las convenciones que ofrece Rails.
Estas convenciones acelerarán el desarrollo, mantendrán su código conciso y legible, y le permitirán una fácil navegación dentro de su aplicación.
Las convenciones también reducen las barreras de entrada para los principiantes. Hay tantas convenciones en Rails que un principiante ni siquiera necesita conocer, pero que solo pueden beneficiarse de la ignorancia. Es posible crear grandes aplicaciones sin saber por qué todo es como es.
Por ejemplo
Si tiene una tabla de base de datos llamada orders
con el id
clave principal, el modelo coincidente se llama order
y el controlador que maneja toda la lógica se denomina orders_controller
. La vista se divide en diferentes acciones: si el controlador tiene una acción new
y de edit
, también hay una vista new
y de edit
.
Por ejemplo
Para crear una aplicación, simplemente ejecuta rails new app_name
. Esto generará aproximadamente 70 archivos y carpetas que conforman la infraestructura y la base de su aplicación Rails.
Incluye:
- Carpetas para guardar sus modelos (capa de base de datos), controladores y vistas
- Carpetas para realizar pruebas unitarias para su aplicación.
- Carpetas para guardar sus activos web como archivos Javascript y CSS
- Archivos predeterminados para respuestas HTTP 400 (es decir, archivo no encontrado)
- Muchos otros
Modelo gordo, flaco controlador
"Modelo gordo, controlador delgado" se refiere a cómo las partes M y C de MVC trabajan idealmente juntas. Es decir, cualquier lógica no relacionada con la respuesta debe ir en el modelo, idealmente en un método agradable y comprobable. Mientras tanto, el controlador "delgado" es simplemente una interfaz agradable entre la vista y el modelo.
En la práctica, esto puede requerir un rango de diferentes tipos de refactorización, pero todo se reduce a una idea: al mover cualquier lógica que no sea sobre la respuesta al modelo (en lugar del controlador), no solo ha promovido la reutilización siempre que sea posible, pero también ha hecho posible probar su código fuera del contexto de una solicitud.
Veamos un ejemplo simple. Digamos que tienes un código como este:
def index
@published_posts = Post.where('published_at <= ?', Time.now)
@unpublished_posts = Post.where('published_at IS NULL OR published_at > ?', Time.now)
end
Puedes cambiarlo a esto:
def index
@published_posts = Post.published
@unpublished_posts = Post.unpublished
end
Luego, puede mover la lógica a su modelo de publicación, donde podría verse así:
scope :published, ->(timestamp = Time.now) { where('published_at <= ?', timestamp) }
scope :unpublished, ->(timestamp = Time.now) { where('published_at IS NULL OR published_at > ?', timestamp) }
Cuidado con default_scope
ActiveRecord incluye default_scope
, para default_scope
automáticamente un modelo de forma predeterminada.
class Post
default_scope ->{ where(published: true).order(created_at: :desc) }
end
El código anterior servirá publicaciones que ya están publicadas cuando realiza cualquier consulta en el modelo.
Post.all # will only list published posts
Ese alcance, aunque tiene un aspecto inocuo, tiene múltiples efectos secundarios ocultos que tal vez no desee.
default_scope
y order
Dado que declaró un order
en el default_scope
, el order
llamada en Post
se agregará como pedidos adicionales en lugar de anular el valor predeterminado.
Post.order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."created_at" DESC, "posts"."updated_at" DESC
Probablemente este no sea el comportamiento que querías; puede anular esto excluyendo el order
del alcance primero
Post.except(:order).order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."updated_at" DESC
default_scope
y modelo de inicialización
Al igual que con cualquier otro ActiveRecord::Relation
, default_scope
alterará el estado predeterminado de los modelos que se inicializaron a partir de él.
En el ejemplo anterior, la Post
tiene where(published: true)
establece de forma predeterminada, por lo que los nuevos modelos de la Post
también lo tendrán configurado.
Post.new # => <Post published: true>
unscoped
default_scope
se puede borrar nominalmente llamando primero sin unscoped
, pero esto también tiene efectos secundarios. Tomemos, por ejemplo, un modelo de STI:
class Post < Document
default_scope ->{ where(published: true).order(created_at: :desc) }
end
De forma predeterminada, las consultas contra Post
tendrán un alcance para type
columnas que contengan 'Post'
. Sin embargo, unscoped
lo unscoped
junto con su propio default_scope
, por lo que si usa unscoped
también debe recordar tenerlo en cuenta.
Post.unscoped.where(type: 'Post').order(updated_at: :desc)
unscoped
Asociaciones y Modelo
Considera una relación entre Post
y User
class Post < ApplicationRecord
belongs_to :user
default_scope ->{ where(published: true).order(created_at: :desc) }
end
class User < ApplicationRecord
has_many :posts
end
Al obtener un User
individual, puede ver las publicaciones relacionadas con él:
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]]
Pero desea borrar el default_scope
de la relación de posts
, por lo que utiliza sin unscoped
user.posts.unscoped
SELECT "posts".* FROM "posts"
Esto elimina la condición user_id
así como el default_scope
.
Un ejemplo de caso de uso para default_scope
A pesar de todo eso, hay situaciones en las que el uso de default_scope
es justificable.
Considere un sistema de múltiples inquilinos donde se sirven múltiples subdominios desde la misma aplicación pero con datos aislados. Una forma de lograr este aislamiento es a través de default_scope
. Las desventajas en otros casos se convierten en ventajas aquí.
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
Todo lo que necesita hacer es establecer Tenant.current_id
en algo al principio de la solicitud, y cualquier tabla que contenga tenant_id
se convertirá automáticamente en un ámbito sin ningún código adicional. Los registros de creación de instancias heredarán automáticamente la identificación del inquilino con la que se crearon.
Lo importante de este caso de uso es que el alcance se establece una vez por solicitud y no cambia. Los únicos casos que necesitará sin unscoped
aquí son casos especiales, como los trabajadores en segundo plano que se ejecutan fuera del alcance de una solicitud.
No lo vas a necesitar (YAGNI)
Si puede decir "YAGNI" (No lo va a necesitar) sobre una característica, es mejor que no la implemente. Puede ahorrarse mucho tiempo de desarrollo al centrarse en la simplicidad. Implementar tales características de todas formas puede llevar a problemas:
Problemas
Sobreingeniería
Si un producto es más complicado de lo que tiene que ser, está sobre diseñado. Por lo general, estas características "aún no utilizadas" nunca se utilizarán de la forma prevista en que fueron escritas y deben ser refactorizadas si alguna vez se usan. Las optimizaciones prematuras, especialmente las optimizaciones de rendimiento, a menudo conducen a decisiones de diseño que resultarán incorrectas en el futuro.
Código Inflado
Código Bloat significa código complicado innecesario. Esto puede ocurrir, por ejemplo, por abstracción, redundancia o aplicación incorrecta de patrones de diseño. El código base se vuelve difícil de entender, confuso y costoso de mantener.
Característica de arrastramiento
Feature Creep se refiere a agregar nuevas funciones que van más allá de la funcionalidad central del producto y conducen a una complejidad innecesariamente alta del producto.
Largo tiempo de desarrollo
El tiempo que podría usarse para desarrollar las características necesarias se emplea para desarrollar características innecesarias. El producto tarda más en entregarse.
Soluciones
KISS - Que sea simple, estúpido.
Según KISS, la mayoría de los sistemas funcionan mejor si están diseñados de manera simple. La simplicidad debe ser un objetivo principal de diseño para reducir la complejidad. Se puede lograr siguiendo el "Principio de Responsabilidad Única", por ejemplo.
YAGNI - No lo vas a necesitar
Menos es más. Piense en todas las características, ¿es realmente necesario? Si puedes pensar en alguna forma de que sea YAGNI, déjalo. Es mejor desarrollarlo cuando es necesario.
Refactorización continua
El producto se está mejorando constantemente. Con la refactorización, podemos asegurarnos de que el producto se está realizando de acuerdo con las mejores prácticas y no se degenerará en un trabajo de parche.
Objetos de dominio (no más modelos de grasa)
"Fat Model, Skinny Controller" es un muy buen primer paso, pero no se escala bien una vez que tu base de código comienza a crecer.
Pensemos en la Responsabilidad Única de los modelos. ¿Cuál es la única responsabilidad de los modelos? ¿Es para mantener la lógica empresarial? ¿Es para mantener la lógica no relacionada con la respuesta?
No. Su responsabilidad es manejar la capa de persistencia y su abstracción.
La lógica de negocios, así como cualquier lógica no relacionada con la respuesta y la lógica no relacionada con la persistencia, deben incluirse en los objetos de dominio.
Los objetos de dominio son clases diseñadas para tener una sola responsabilidad en el dominio del problema. Deja que tus clases " griten su arquitectura " para los problemas que resuelven.
En la práctica, debes esforzarte por conseguir modelos delgados, vistas delgadas y controladores delgados. La arquitectura de su solución no debe verse influida por el marco que elija.
Por ejemplo
Digamos que usted es un mercado que cobra una comisión fija del 15% a sus clientes a través de Stripe. Si cobra una comisión fija del 15%, eso significa que su comisión varía según el monto del pedido porque Stripe cobra 2.9% + 30 ¢.
El monto que cobra como comisión debe ser: amount*0.15 - (amount*0.029 + 0.30)
.
No escriba esta lógica en el modelo:
# 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
Tan pronto como se integre con un nuevo método de pago, no podrá escalar esta funcionalidad dentro de este modelo.
Además, tan pronto como empiece a integrar más lógica de negocios, su objeto Order
comenzará a perder cohesión .
Prefiera los objetos de dominio, con el cálculo de la comisión totalmente abstraído de la responsabilidad de las órdenes persistentes:
# 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
El uso de objetos de dominio tiene las siguientes ventajas arquitectónicas:
- es extremadamente fácil de realizar una prueba unitaria, ya que no se requieren accesorios ni fábricas para instanciar los objetos con la lógica.
- Funciona con todo lo que acepta el
amount
mensaje. - mantiene cada objeto de dominio pequeño, con responsabilidades claramente definidas y con mayor cohesión.
- Se escala fácilmente con nuevos métodos de pago por adición, no por modificación .
- detiene la tendencia a tener un objeto de
User
cada vez mayor en cada aplicación de Ruby on Rails.
Personalmente me gusta poner objetos de dominio en lib
. Si lo hace, recuerde agregarlo a autoload_paths
:
# config/application.rb
config.autoload_paths << Rails.root.join('lib')
También puede preferir crear objetos de dominio más orientados a la acción, siguiendo el patrón de Comando / Consulta. En tal caso, colocar estos objetos en la app/commands
podría ser un lugar mejor, ya que todos app
subdirectorios de la app
se agregan automáticamente a la ruta de carga automática.