Cómo usar las preocupaciones en Rails 4

628

El generador de proyectos predeterminado de Rails 4 ahora crea el directorio "preocupaciones" en controladores y modelos. He encontrado algunas explicaciones sobre cómo usar las preocupaciones de enrutamiento, pero nada sobre los controladores o modelos.

Estoy bastante seguro de que tiene que ver con la actual "tendencia de DCI" en la comunidad y me gustaría intentarlo.

La pregunta es, ¿cómo se supone que debo usar esta función? ¿Existe una convención sobre cómo definir la jerarquía de nombres / clases para que funcione? ¿Cómo puedo incluir una inquietud en un modelo o controlador?

yagooar
fuente

Respuestas:

617

Entonces lo descubrí por mí mismo. En realidad es un concepto bastante simple pero poderoso. Tiene que ver con la reutilización del código como en el siguiente ejemplo. Básicamente, la idea es extraer fragmentos de código comunes y / o específicos del contexto para limpiar los modelos y evitar que se vuelvan demasiado gordos y desordenados.

Como ejemplo, pondré un patrón bien conocido, el patrón etiquetable:

# app/models/product.rb
class Product
  include Taggable

  ...
end

# app/models/concerns/taggable.rb
# notice that the file name has to match the module name 
# (applying Rails conventions for autoloading)
module Taggable
  extend ActiveSupport::Concern

  included do
    has_many :taggings, as: :taggable
    has_many :tags, through: :taggings

    class_attribute :tag_limit
  end

  def tags_string
    tags.map(&:name).join(', ')
  end

  def tags_string=(tag_string)
    tag_names = tag_string.to_s.split(', ')

    tag_names.each do |tag_name|
      tags.build(name: tag_name)
    end
  end

  # methods defined here are going to extend the class, not the instance of it
  module ClassMethods

    def tag_limit(value)
      self.tag_limit_value = value
    end

  end

end

Entonces, siguiendo la muestra del Producto, puede agregar Taggable a cualquier clase que desee y compartir su funcionalidad.

DHH lo explica muy bien :

En Rails 4, vamos a invitar a los programadores a utilizar las preocupaciones con los directorios predeterminados de la aplicación / modelos / preocupaciones y la aplicación / controladores / preocupaciones que son automáticamente parte de la ruta de carga. Junto con el envoltorio ActiveSupport :: Concern, es suficiente soporte para hacer que este mecanismo de factorización liviano brille.

yagooar
fuente
11
DCI trata con un contexto, usa roles como identificadores para mapear un modelo mental / caso de uso al código, y no requiere envoltorios (los métodos están vinculados directamente al objeto en tiempo de ejecución), por lo que esto realmente no tiene nada que ver con DCI.
Ciscoheat
2
@yagooar incluso incluirlo en tiempo de ejecución no lo convertiría en DCI. Si desea ver una implementación de ejemplo ruby ​​DCI. Eche un vistazo a fulloo.info o los ejemplos en github.com/runefs/Moby o sobre cómo usar granate para hacer DCI en Ruby y qué DCI es runefs.com (qué es DCI. Es una serie de publicaciones que he comenzó recientemente)
Rune FS
1
@RuneFS && ciscoheat tenían razón ambos. Acabo de analizar los artículos y los hechos nuevamente. Y fui el fin de semana pasado a una conferencia de Ruby donde una charla fue sobre DCI y finalmente entendí un poco más sobre su filosofía. Cambió el texto para que no mencione DCI en absoluto.
yagooar
99
Vale la pena mencionar (y probablemente incluir en un ejemplo) que se supone que los métodos de clase se definen en un módulo ClassMethods especialmente nombrado, y que este módulo se extiende por la clase base sea ActiveSupport :: Concern, también.
Febeling
1
Gracias por este ejemplo, principalmente b / c que estaba siendo tonta y definir mis métodos de nivel de clase en el interior de los ClassMethods módulo con self.whatever todavía, y que no funciona = P
Crews Ryan
379

He estado leyendo sobre el uso de las inquietudes del modelo para adelgazar los modelos de grasa, así como SECAR los códigos de su modelo. Aquí hay una explicación con ejemplos:

1) SECADO de códigos de modelo

Considere un modelo de artículo, un modelo de evento y un modelo de comentario. Un artículo o un evento tiene muchos comentarios. Un comentario pertenece al artículo o al evento.

Tradicionalmente, los modelos pueden verse así:

Modelo de comentario:

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

Modelo de artículo:

class Article < ActiveRecord::Base
  has_many :comments, as: :commentable 

  def find_first_comment
    comments.first(created_at DESC)
  end

  def self.least_commented
   #return the article with least number of comments
  end
end

Modelo de evento

class Event < ActiveRecord::Base
  has_many :comments, as: :commentable 

  def find_first_comment
    comments.first(created_at DESC)
  end

  def self.least_commented
   #returns the event with least number of comments
  end
end

Como podemos notar, hay un código significativo común tanto para Evento como para Artículo. Usando preocupaciones, podemos extraer este código común en un módulo separado Comentario.

Para esto, cree un archivo commentable.rb en app / models / preocupaciones.

module Commentable
  extend ActiveSupport::Concern

  included do
    has_many :comments, as: :commentable
  end

  # for the given article/event returns the first comment
  def find_first_comment
    comments.first(created_at DESC)
  end

  module ClassMethods
    def least_commented
      #returns the article/event which has the least number of comments
    end
  end
end

Y ahora tus modelos se ven así:

Modelo de comentario:

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

Modelo de artículo:

class Article < ActiveRecord::Base
  include Commentable
end

Modelo de evento:

class Event < ActiveRecord::Base
  include Commentable
end

2) Modelos de grasa que aligeran la piel.

Considere un modelo de evento. Un evento tiene muchos asistentes y comentarios.

Por lo general, el modelo de evento podría verse así

class Event < ActiveRecord::Base   
  has_many :comments
  has_many :attenders


  def find_first_comment
    # for the given article/event returns the first comment
  end

  def find_comments_with_word(word)
    # for the given event returns an array of comments which contain the given word
  end 

  def self.least_commented
    # finds the event which has the least number of comments
  end

  def self.most_attended
    # returns the event with most number of attendes
  end

  def has_attendee(attendee_id)
    # returns true if the event has the mentioned attendee
  end
end

Los modelos con muchas asociaciones y, de lo contrario, tienden a acumular más y más código y se vuelven inmanejables. Las preocupaciones proporcionan una manera de personalizar los módulos de grasa, haciéndolos más modularizados y fáciles de entender.

El modelo anterior se puede refactorizar usando preocupaciones como a continuación: Crear una attendable.rby commentable.rbarchivo en modelos / preocupaciones carpeta de eventos app / /

attendable.rb

module Attendable
  extend ActiveSupport::Concern

  included do 
    has_many :attenders
  end

  def has_attender(attender_id)
    # returns true if the event has the mentioned attendee
  end

  module ClassMethods
    def most_attended
      # returns the event with most number of attendes
    end
  end
end

commentable.rb

module Commentable
  extend ActiveSupport::Concern

  included do 
    has_many :comments
  end

  def find_first_comment
    # for the given article/event returns the first comment
  end

  def find_comments_with_word(word)
    # for the given event returns an array of comments which contain the given word
  end

  module ClassMethods
    def least_commented
      # finds the event which has the least number of comments
    end
  end
end

Y ahora usando Preocupaciones, su modelo de Evento se reduce a

class Event < ActiveRecord::Base
  include Commentable
  include Attendable
end

* Durante el uso de las preocupaciones, es recomendable optar por la agrupación basada en 'dominio' en lugar de la agrupación 'técnica'. La agrupación basada en el dominio es como 'Commentable', 'Photoable', 'Attendable'. La agrupación técnica significará 'ValidationMethods', 'FinderMethods', etc.

Aaditi Jain
fuente
66
Entonces, ¿las preocupaciones son solo una forma de usar herencia o interfaces o herencia múltiple? ¿Qué tiene de malo crear una clase base común y subclasificar a partir de esa clase base común?
Chloe
3
De hecho @Chloe, en algún lugar rojo, una aplicación Rails con un directorio de 'preocupaciones' es en realidad una 'preocupación' ...
Ziyan Junaideen
Puede usar el bloque 'incluido' para definir todos sus métodos e incluye: métodos de clase (con def self.my_class_method), métodos de instancia y llamadas a métodos y directivas en el ámbito de la clase. No hay necesidad demodule ClassMethods
A Fader Darkly
1
El problema que tengo con las preocupaciones es que agregan funcionalidad directamente al modelo. Entonces, si dos preocupaciones se implementan add_item, por ejemplo, estás jodido. Recuerdo que pensé que Rails estaba roto cuando algunos validadores dejaron de funcionar, pero alguien lo había implementado any?en una preocupación. Propongo una solución diferente: use la preocupación como una interfaz en un idioma diferente. En lugar de definir la funcionalidad, define la referencia a una instancia de clase separada que maneja esa funcionalidad. Entonces tienes clases más pequeñas y ordenadas que hacen una cosa ...
A Fader Darkly
@aaditi_jain: corrija pequeños cambios para evitar conceptos erróneos. es decir, "Cree un archivo attendable.rd y commentable.rb en la carpeta app / models / preocupaciones / event" -> attendable.rd tiene que ser attendable.rb Gracias
Rubyist
97

Vale la pena mencionar que el uso de preocupaciones es considerado una mala idea por muchos.

  1. como este chico
  2. y éste

Algunos motivos:

  1. Hay algo de magia oscura detrás de escena: la preocupación es el includemétodo de parcheo , hay un sistema completo de manejo de dependencias, demasiada complejidad para algo que es un trivial buen patrón de mezcla Ruby.
  2. Tus clases no son menos secas. Si inserta 50 métodos públicos en varios módulos y los incluye, su clase todavía tiene 50 métodos públicos, es solo que oculta ese olor a código, es como poner la basura en los cajones.
  3. Codebase es realmente más difícil de navegar con todas esas preocupaciones.
  4. ¿Estás seguro de que todos los miembros de tu equipo tienen la misma comprensión de lo que realmente debería sustituir a la preocupación?

Las preocupaciones son una manera fácil de dispararte en la pierna, ten cuidado con ellas.

Dr. Strangelove
fuente
1
Sé que SO no es el mejor lugar para esta discusión, pero ¿qué otro tipo de mezcla Ruby mantiene sus clases secas? Parece que las razones # 1 y # 2 en sus argumentos son contrarias, a menos que solo esté defendiendo un mejor diseño OO, la capa de servicios o algo más que me estoy perdiendo. (No estoy en desacuerdo, ¡estoy sugiriendo que agregar alternativas ayude!)
toobulkeh
2
Usar github.com/AndyObtiva/super_module es una opción, usar buenos patrones antiguos de ClassMethods es otra. Y usar más objetos (como servicios) para separar limpiamente las preocupaciones es definitivamente el camino a seguir.
Dr.Strangelove
44
Votación negativa porque esta no es una respuesta a la pregunta. Es una opinion. Es una opinión que estoy seguro de que tiene sus méritos, pero no debería ser una respuesta a una pregunta sobre StackOverflow.
Adam
2
@ Adam Es una respuesta obstinada. Imagine que alguien pregunta cómo usar variables globales en los rieles, seguramente mencionaría que hay mejores maneras de hacer las cosas (es decir, Redis.current vs $ redis) ¿podría ser información útil para comenzar el tema? El desarrollo de software es inherentemente una disciplina obstinada, no hay forma de evitarlo. De hecho, veo las opiniones como respuestas y discusiones sobre qué respuesta es la mejor todo el tiempo en stackoverflow, y es algo bueno
Dr.Strangelove
2
Claro, mencionarlo junto con su respuesta a la pregunta parece estar bien. Sin embargo, nada en su respuesta en realidad responde a la pregunta del OP. Si todo lo que desea hacer es advertir a alguien por qué no deberían usar las preocupaciones o las variables globales, eso sería un buen comentario que podría agregar a su pregunta, pero en realidad no es una buena respuesta.
Adam
46

Sentí que la mayoría de los ejemplos aquí demostraban el poder en modulelugar de cómo ActiveSupport::Concernagrega valor module.

Ejemplo 1: módulos más legibles.

Entonces, sin preocupaciones, esto moduleserá lo típico .

module M
  def self.included(base)
    base.extend ClassMethods
    base.class_eval do
      scope :disabled, -> { where(disabled: true) }
    end
  end

  def instance_method
    ...
  end

  module ClassMethods
    ...
  end
end

Después de refactorizar con ActiveSupport::Concern.

require 'active_support/concern'

module M
  extend ActiveSupport::Concern

  included do
    scope :disabled, -> { where(disabled: true) }
  end

  class_methods do
    ...
  end

  def instance_method
    ...
  end
end

Verá que los métodos de instancia, los métodos de clase y el bloque incluido son menos desordenados. Las preocupaciones los inyectarán de manera apropiada para usted. Esa es una ventaja de usar ActiveSupport::Concern.


Ejemplo 2: manejar las dependencias del módulo con gracia.

module Foo
  def self.included(base)
    base.class_eval do
      def self.method_injected_by_foo_to_host_klass
        ...
      end
    end
  end
end

module Bar
  def self.included(base)
    base.method_injected_by_foo_to_host_klass
  end
end

class Host
  include Foo # We need to include this dependency for Bar
  include Bar # Bar is the module that Host really needs
end

En este ejemplo Bares el módulo que Hostrealmente necesita. Pero como Bartiene dependencia con Foola Hostclase tiene que hacerlo include Foo(pero espere ¿por qué Hostquiere saber Foo? ¿Se puede evitar?).

Entonces Baragrega dependencia donde quiera que vaya. Y el orden de inclusión también es importante aquí. Esto agrega mucha complejidad / dependencia a la enorme base de código.

Después de refactorizar con ActiveSupport::Concern

require 'active_support/concern'

module Foo
  extend ActiveSupport::Concern
  included do
    def self.method_injected_by_foo_to_host_klass
      ...
    end
  end
end

module Bar
  extend ActiveSupport::Concern
  include Foo

  included do
    self.method_injected_by_foo_to_host_klass
  end
end

class Host
  include Bar # It works, now Bar takes care of its dependencies
end

Ahora se ve simple.

Si está pensando, ¿por qué no podemos agregar Foodependencia en el Barpropio módulo? Eso no funcionará, ya method_injected_by_foo_to_host_klassque debe inyectarse en una clase que no está incluida Baren el Barmódulo.

Fuente: Rails ActiveSupport :: Preocupación

Siva
fuente
gracias por eso. Estaba empezando a preguntarme cuál es su ventaja ...
Hari Karam Singh
FWIW esto es más o menos copiar y pegar de los documentos .
Dave Newton
7

En preocupaciones, haga el archivo filename.rb

Por ejemplo, quiero en mi aplicación donde el atributo create_by existe, actualice su valor en 1 y 0 para updated_by

module TestConcern 
  extend ActiveSupport::Concern

  def checkattributes   
    if self.has_attribute?(:created_by)
      self.update_attributes(created_by: 1)
    end
    if self.has_attribute?(:updated_by)
      self.update_attributes(updated_by: 0)
    end
  end

end

Si quieres pasar argumentos en acción

included do
   before_action only: [:create] do
     blaablaa(options)
   end
end

después de eso incluye en tu modelo así:

class Role < ActiveRecord::Base
  include TestConcern
end
Sajjad Murtaza
fuente