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.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow