Verwendung von Bedenken in Rails 4

628

Der Standard-Rails 4-Projektgenerator erstellt jetzt das Verzeichnis "Anliegen" unter Controllern und Modellen. Ich habe einige Erklärungen zur Verwendung von Routing-Bedenken gefunden, aber nichts zu Controllern oder Modellen.

Ich bin mir ziemlich sicher, dass es mit dem aktuellen "DCI-Trend" in der Community zu tun hat und würde es gerne ausprobieren.

Die Frage ist, wie soll ich diese Funktion verwenden? Gibt es eine Konvention zum Definieren der Namens- / Klassenhierarchie, damit sie funktioniert? Wie kann ich ein Problem in ein Modell oder einen Controller aufnehmen?

yagooar
quelle

Antworten:

617

Also habe ich es selbst herausgefunden. Es ist eigentlich ein ziemlich einfaches, aber leistungsstarkes Konzept. Dies hat mit der Wiederverwendung von Code wie im folgenden Beispiel zu tun. Grundsätzlich besteht die Idee darin, allgemeine und / oder kontextspezifische Codestücke zu extrahieren, um die Modelle zu bereinigen und zu vermeiden, dass sie zu fett und unordentlich werden.

Als Beispiel werde ich ein bekanntes Muster einfügen, das taggable Muster:

# 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

Wenn Sie dem Produktbeispiel folgen, können Sie Taggable zu jeder gewünschten Klasse hinzufügen und deren Funktionalität freigeben.

Dies wird von DHH ziemlich gut erklärt :

In Rails 4 werden wir Programmierer einladen, Bedenken mit den Standardverzeichnissen app / models / Concern und app / controller / Concern zu verwenden, die automatisch Teil des Ladepfads sind. Zusammen mit dem ActiveSupport :: Concern-Wrapper reicht diese Unterstützung gerade aus, um diesen leichten Factoring-Mechanismus zum Leuchten zu bringen.

yagooar
quelle
11
DCI befasst sich mit einem Kontext, verwendet Rollen als Bezeichner, um ein mentales Modell / einen Anwendungsfall dem Code zuzuordnen, und erfordert die Verwendung von Wrappern (Methoden werden zur Laufzeit direkt an das Objekt gebunden), sodass dies eigentlich nichts mit DCI zu tun hat.
Ciscoheat
2
@yagooar selbst wenn es zur Laufzeit aufgenommen wird, wird es nicht zu DCI. Wenn Sie eine Ruby-DCI-Beispielimplementierung sehen möchten. Schauen Sie sich entweder fulloo.info oder die Beispiele unter github.com/runefs/Moby an oder erfahren Sie, wie Sie mit maroon DCI in Ruby ausführen und was DCI ist runefs.com (Was DCI ist. Ist eine Reihe von Beiträgen, die ich habe gerade erst begonnen)
Rune FS
1
@ RuneFS && Ciscoheat Sie hatten beide Recht. Ich habe gerade die Artikel und Fakten noch einmal analysiert. Und ich war letztes Wochenende auf einer Ruby-Konferenz, auf der es in einem Vortrag um DCI ging, und schließlich verstand ich ein bisschen mehr über seine Philosophie. Der Text wurde so geändert, dass DCI überhaupt nicht erwähnt wird.
Yagooar
9
Es ist erwähnenswert (und wahrscheinlich auch in einem Beispiel enthalten), dass Klassenmethoden in einem speziell benannten Modul ClassMethods definiert werden sollen und dass dieses Modul auch um die Basisklasse ActiveSupport :: Concern erweitert wird.
Febeling
1
Vielen Dank für dieses Beispiel, hauptsächlich b / c Ich war dumm und definierte meine Methoden auf Klassenebene innerhalb des ClassMethods-Moduls mit self. Was auch immer noch, und das funktioniert nicht = P
Ryan Crews
379

Ich habe über die Verwendung von Modellproblemen gelesen, um Fettmodelle zu häuten und Ihre Modellcodes auszutrocknen. Hier ist eine Erklärung mit Beispielen:

1) Trocknen von Modellcodes

Betrachten Sie ein Artikelmodell, ein Ereignismodell und ein Kommentarmodell. Ein Artikel oder eine Veranstaltung hat viele Kommentare. Ein Kommentar gehört entweder zum Artikel oder zum Ereignis.

Traditionell sehen die Modelle folgendermaßen aus:

Kommentar Modell:

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

Artikelmodell:

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

Ereignismodell

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

Wie wir feststellen können, gibt es sowohl für Event als auch für Article einen wichtigen Code. Mit Bedenken können wir diesen gemeinsamen Code in einem separaten Modul Commentable extrahieren.

Erstellen Sie dazu eine commentable.rb-Datei in App / Models / Concerns.

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

Und jetzt sehen Ihre Modelle so aus:

Kommentar Modell:

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

Artikelmodell:

class Article < ActiveRecord::Base
  include Commentable
end

Ereignismodell:

class Event < ActiveRecord::Base
  include Commentable
end

2) Hautfettende Fettmodelle.

Betrachten Sie ein Ereignismodell. Eine Veranstaltung hat viele Teilnehmer und Kommentare.

In der Regel sieht das Ereignismodell folgendermaßen aus

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

Modelle mit vielen Assoziationen und anderen Tendenzen neigen dazu, immer mehr Code anzusammeln und nicht mehr zu verwalten. Bedenken bieten eine Möglichkeit, Fettmodule zu häuten, wodurch sie modularer und verständlicher werden.

Das obige Modell kann mithilfe der folgenden Bedenken überarbeitet werden: Erstellen Sie eine attendable.rbund commentable.rb-Datei im Ordner app / models / ca. / event

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

Und jetzt, wo Sie Bedenken verwenden, reduziert sich Ihr Ereignismodell auf

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

* Bei der Verwendung von Bedenken ist es ratsam, sich für eine domänenbasierte Gruppierung anstatt für eine technische Gruppierung zu entscheiden. Domainbasierte Gruppierung ist wie "Kommentierbar", "Fotofähig", "Teilnahmefähig". Technische Gruppierung bedeutet "ValidationMethods", "FinderMethods" usw.

Aaditi Jain
quelle
6
Bedenken sind also nur eine Möglichkeit, Vererbung oder Schnittstellen oder Mehrfachvererbung zu verwenden? Was ist falsch daran, eine gemeinsame Basisklasse zu erstellen und eine Unterklasse aus dieser gemeinsamen Basisklasse zu erstellen?
Chloe
3
In der Tat @Chloe, ich einige wo rot, eine Rails-App mit einem "Anliegen"
-Verzeichnis
Sie können den Block 'eingeschlossen' verwenden, um alle Ihre Methoden zu definieren. Dazu gehören: Klassenmethoden (mit def self.my_class_method), Instanzmethoden sowie Methodenaufrufe und Direktiven im Klassenbereich. Keine Notwendigkeit fürmodule ClassMethods
A Fader Darkly
1
Das Problem, das ich mit Bedenken habe, ist, dass sie dem Modell Funktionen direkt hinzufügen. Wenn also beispielsweise zwei Probleme beide implementieren add_item, sind Sie fertig. Ich erinnere mich, dass ich dachte, Rails sei kaputt, als einige Validatoren aufhörten zu arbeiten, aber jemand hatte any?ein Problem gelöst. Ich schlage eine andere Lösung vor: Verwenden Sie das Anliegen wie eine Schnittstelle in einer anderen Sprache. Anstatt die Funktionalität zu definieren, wird der Verweis auf eine separate Klasseninstanz definiert, die diese Funktionalität verarbeitet. Dann haben Sie kleinere, ordentlichere Klassen, die eines tun ...
A Fader Darkly
@aaditi_jain: Bitte korrigieren Sie kleine Änderungen, um Missverständnisse zu vermeiden. dh "Erstellen Sie eine Datei" attable.rd "und" commentable.rb "im Ordner" app / models / Concerns / Ereignis "" -> "attable.rd" muss "attableable.rb" sein. Danke
Rubyist
97

Es ist erwähnenswert, dass die Verwendung von Bedenken von vielen als schlechte Idee angesehen wird.

  1. wie dieser Typ
  2. und das hier

Einige Gründe:

  1. Hinter den Kulissen passiert etwas Dunkle Magie - Bedenken sind Patching- includeMethoden, es gibt ein ganzes Abhängigkeitsbehandlungssystem - viel zu viel Komplexität für etwas, das trivial gut ist, altes Ruby-Mixin-Muster.
  2. Ihr Unterricht ist nicht weniger trocken. Wenn Sie 50 öffentliche Methoden in verschiedene Module einfügen und diese einschließen, verfügt Ihre Klasse immer noch über 50 öffentliche Methoden. Sie verbergen lediglich diesen Code-Geruch und legen Ihren Müll in die Schubladen.
  3. Codebase ist mit all diesen Bedenken tatsächlich schwieriger zu navigieren.
  4. Sind Sie sicher, dass alle Mitglieder Ihres Teams das gleiche Verständnis haben, was Bedenken wirklich ersetzen sollte?

Bedenken sind eine einfache Möglichkeit, sich ins Bein zu schießen. Seien Sie vorsichtig mit ihnen.

Dr. Strangelove
quelle
1
Ich weiß, dass SO nicht der beste Ort für diese Diskussion ist, aber welche andere Art von Ruby-Mixin hält Ihren Unterricht trocken? Es scheint, als wären die Gründe Nr. 1 und Nr. 2 in Ihren Argumenten gegensätzlich, es sei denn, Sie sprechen sich nur für ein besseres OO-Design, die Serviceschicht oder etwas anderes aus, das mir fehlt? (Ich bin nicht anderer Meinung - ich
schlage vor
2
Die Verwendung von github.com/AndyObtiva/super_module ist eine Option, die Verwendung guter alter ClassMethods-Muster eine andere. Die Verwendung von mehr Objekten (wie Diensten) zur sauberen Trennung von Anliegen ist definitiv der richtige Weg.
Dr. Strangelove
4
Downvoting, da dies keine Antwort auf die Frage ist. Es ist eine Meinung. Ich bin mir sicher, dass es seine Vorzüge hat, aber es sollte keine Antwort auf eine Frage zu StackOverflow sein.
Adam
2
@ Adam Es ist eine meinungsgebundene Antwort. Stellen Sie sich vor, jemand würde fragen, wie globale Variablen in Schienen verwendet werden sollen, und sicher erwähnen, dass es bessere Möglichkeiten gibt, Dinge zu tun (z. B. Redis.current vs $ redis), die nützliche Informationen für den Themenstarter sein könnten. Softwareentwicklung ist von Natur aus eine disziplinierte Disziplin, an der man nicht vorbei kommt. Tatsächlich sehe ich Meinungen als Antworten und Diskussionen, wobei die Antwort auf Stackoverflow immer die beste ist, und es ist eine gute Sache
Dr.Strangelove
2
Sicher, es zusammen mit Ihrer Antwort auf die Frage zu erwähnen, scheint in Ordnung zu sein. Nichts in Ihrer Antwort beantwortet jedoch die Frage des OP. Wenn Sie nur jemanden warnen möchten, warum er keine Bedenken oder globalen Variablen verwenden sollte, wäre dies ein guter Kommentar, den Sie zu seiner Frage hinzufügen könnten, der jedoch keine gute Antwort liefert.
Adam
56

Dieser Beitrag hat mir geholfen, Bedenken zu verstehen.

# app/models/trader.rb
class Trader
  include Shared::Schedule
end

# app/models/concerns/shared/schedule.rb
module Shared::Schedule
  extend ActiveSupport::Concern
  ...
end
Aminhotob
quelle
1
Diese Antwort erklärt nichts.
46

Ich hatte das Gefühl, dass die meisten Beispiele hier moduleeher die Kraft als den ActiveSupport::ConcernMehrwert demonstrieren module.

Beispiel 1: Lesbarere Module.

Also ohne Bedenken, wie ein typischer sein modulewird.

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

Nach dem Refactoring mit 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

Sie sehen, dass Instanzmethoden, Klassenmethoden und eingeschlossene Blöcke weniger chaotisch sind. Bedenken werden sie angemessen für Sie injizieren. Das ist ein Vorteil der Verwendung ActiveSupport::Concern.


Beispiel 2: Behandeln Sie Modulabhängigkeiten ordnungsgemäß.

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

In diesem Beispiel Barist das Modul, das Hostwirklich benötigt. Aber da Barmuss die Abhängigkeit von Fooder HostKlasse sein include Foo(aber warten, warum Hostwill man das wissen Foo? Kann man das vermeiden?).

So Barfügt Abhängigkeit überall geht es. Und die Reihenfolge der Aufnahme zählt auch hier. Dies erhöht die Komplexität / Abhängigkeit der riesigen Codebasis erheblich.

Nach dem Refactoring mit 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

Jetzt sieht es einfach aus.

Wenn Sie überlegen, warum können wir keine FooAbhängigkeit im BarModul selbst hinzufügen ? Das wird nicht funktionieren, da method_injected_by_foo_to_host_klasses in eine Klasse eingefügt werden muss, die Barnicht auf dem BarModul selbst enthalten ist.

Quelle: Rails ActiveSupport :: Concern

Siva
quelle
Dank dafür. Ich begann mich zu fragen, was ihr Vorteil ist ...
Hari Karam Singh
FWIW Dies ist ungefähr das Kopieren und Einfügen aus den Dokumenten .
Dave Newton
7

In Bedenken machen Sie die Datei filename.rb

Zum Beispiel möchte ich in meiner Anwendung, in der das Attribut create_by existiert, den Wert um 1 und 0 für update_by aktualisieren

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

Wenn Sie Argumente in Aktion übergeben möchten

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

Danach nehmen Sie Folgendes in Ihr Modell auf:

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