Jak korzystać z obaw w Railsach 4


628

Domyślny generator projektów Rails 4 tworzy teraz katalog „dotyczy” w kontrolerach i modelach. Znalazłem kilka wyjaśnień na temat korzystania z problemów z routingiem, ale nic na temat kontrolerów lub modeli.

Jestem pewien, że ma to związek z obecnym „trendem DCI” w społeczności i chciałbym spróbować.

Pytanie brzmi: jak mam korzystać z tej funkcji, czy istnieje konwencja dotycząca sposobu definiowania hierarchii nazw / klas, aby działała? Jak mogę uwzględnić problem w modelu lub sterowniku?

Odpowiedzi:


617

Więc sam to odkryłem. To właściwie dość prosta, ale potężna koncepcja. Ma to związek z ponownym użyciem kodu, jak w poniższym przykładzie. Zasadniczo chodzi o to, aby wyodrębnić typowe i / lub kontekstowe fragmenty kodu, aby oczyścić modele i uniknąć ich nadmiernego zużycia i bałaganu.

Jako przykład podam jeden dobrze znany wzorzec, znakowalny wzór:

# 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

Tak więc po przykładzie produktu możesz dodać Taggable do dowolnej klasy i udostępnić jego funkcjonalność.

Jest to dość dobrze wyjaśnione przez DHH :

W Railsach 4 zaprosimy programistów do używania problemów z domyślnymi katalogami app / models / koncernów / app / kontrolerów / koncernów, które są automatycznie częścią ścieżki ładowania. W połączeniu z otoką ActiveSupport :: Concern jest to wystarczające wsparcie, aby ten lekki mechanizm faktoringowy zabłysnął.


11
DCI zajmuje się kontekstem, używa ról jako identyfikatorów do mapowania modelu mentalnego / przypadku użycia do kodu i nie wymaga używania opakowań (metody są powiązane bezpośrednio z obiektem w czasie wykonywania), więc to naprawdę nie ma nic wspólnego z DCI.
ciscoheat

2
@yagooar, nawet włączenie go w czasie wykonywania, nie sprawi, że będzie to DCI. Jeśli chcesz zobaczyć przykładową implementację Rubiego DCI. Rzuć okiem na fulloo.info lub przykłady na github.com/runefs/Moby lub jak używać bordowego do robienia DCI w Ruby i czym jest DCI runefs.com (Czym jest DCI. To seria postów, które mam właśnie zaczął niedawno)
Rune FS

1
@RuneFS && ciscoheat mieliście rację. Właśnie ponownie przeanalizowałem artykuły i fakty. W zeszły weekend poszedłem na konferencję Ruby, gdzie jedna rozmowa dotyczyła DCI i wreszcie zrozumiałem trochę więcej o jej filozofii. Zmieniono tekst, aby w ogóle nie wspominał o DCI.
yagooar 10.03.13

9
Warto wspomnieć (i prawdopodobnie zawierać w przykładzie), że metody klas powinny być zdefiniowane w specjalnie nazwanym module ClassMethods i że moduł ten jest rozszerzony o klasę bazową również ActiveSupport :: Concern.
febeling

1
Dziękuję za ten przykład, głównie b / c Byłem głupi i definiowałem swoje metody na poziomie klasy w module ClassMethods z self. Cokolwiek jeszcze, i to nie działa = P
Ryan Crews

379

Czytałem o korzystaniu z modeli z modelami do modelowania tłuszczowych modeli, a także do OSUSZANIA kodów modeli. Oto wyjaśnienie z przykładami:

1) SUSZENIE kodów modeli

Rozważ model artykułu, model zdarzenia i model komentarza. Artykuł lub wydarzenie ma wiele komentarzy. Komentarz należy do artykułu lub wydarzenia.

Tradycyjnie modele mogą wyglądać następująco:

Model komentarza:

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

Model artykułu:

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

Model zdarzenia

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

Jak możemy zauważyć, znaczna część kodu jest wspólna zarówno dla zdarzenia, jak i artykułu. Korzystając z obaw, możemy wyodrębnić ten wspólny kod w osobnym module Komentowalny.

W tym celu utwórz plik commentable.rb w app / models / koncernach.

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

A teraz twoje modele wyglądają tak:

Model komentarza:

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

Model artykułu:

class Article < ActiveRecord::Base
  include Commentable
end

Model zdarzenia:

class Event < ActiveRecord::Base
  include Commentable
end

2) Modele tłuszczów wyróżniające skórę.

Rozważ model zdarzenia. Wydarzenie ma wielu uczestników i komentarzy.

Zazwyczaj model zdarzenia może wyglądać tak

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

Modele o wielu skojarzeniach i poza tym mają tendencję do gromadzenia coraz większej ilości kodu i stają się niemożliwe do zarządzania. Obawy stanowią sposób na zminiaturyzowanie modułów tłuszczu, dzięki czemu są one bardziej modułowe i łatwe do zrozumienia.

Powyższy model można refaktoryzować, korzystając z poniższych obaw: Utwórz a attendable.rbicommentable.rb plik folderze app / models / koncern / event

attable.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

A teraz używając Obawy, Twój model wydarzenia zmniejsza się do

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

* Podczas korzystania zaleca się grupowanie oparte na „domenie”, a nie „techniczne”. Grupowanie w oparciu o domeny jest podobne do „Komentowalne”, „Photoable”, „Attendable”. Grupowanie techniczne oznacza „ValidationMethods”, „FinderMethods itp


6
Czy zatem obawy są sposobem na wykorzystanie dziedziczenia, interfejsów lub wielokrotnego dziedziczenia? Co jest złego w tworzeniu wspólnej klasy podstawowej i podklas z tej wspólnej klasy podstawowej?
Chloe,

3
Rzeczywiście @Chloe, ja tam, gdzie czerwony, aplikacja Rails z katalogiem „dotyczy” jest w rzeczywistości „problemem” ...
Ziyan Junaideen,

Możesz użyć bloku „włączone”, aby zdefiniować wszystkie swoje metody i obejmuje: metody klas (z def self.my_class_method), metody instancji oraz wywołania metod i dyrektywy w zakresie klasy. Nie ma potrzebymodule ClassMethods
Fader Darkly

1
Problem, który mam z obawami, polega na tym, że dodają funkcjonalność bezpośrednio do modelu. Więc jeśli dwa problemy dotyczą obu narzędzi add_item, na przykład, masz problemy . Pamiętam, że Railsy zostały zerwane, gdy niektóre weryfikatory przestały działać, ale ktoś wdrożył any?problem. Proponuję inne rozwiązanie: użyj troski jak interfejsu w innym języku. Zamiast definiować funkcjonalność, definiuje odwołanie do osobnego wystąpienia klasy, który obsługuje tę funkcjonalność. Potem masz mniejsze, zgrabniejsze klasy, które robią jedną rzecz ...
Fader Darkly

@aaditi_jain: Proszę poprawić małą zmianę, aby uniknąć nieporozumień. tzn. „Utwórz plik attable.rd i commentable.rb w folderze app / models / koncern / event” -> attable.rd musi być dostępny. rb Dzięki
Rubyist

97

Warto wspomnieć, że używanie obaw jest uważane przez wielu za zły pomysł.

  1. jak ten facet
  2. i ten

Pewne powody:

  1. Za kulisami dzieje się jakaś mroczna magia - Zaniepokojenie to includemetoda łatania , istnieje cały system obsługi zależności - zdecydowanie zbyt duża złożoność dla czegoś, co jest trywialnym, dobrym, starym wzorcem mieszania Ruby.
  2. Twoje zajęcia są nie mniej suche. Jeśli umieścisz 50 metod publicznych w różnych modułach i dodasz je, twoja klasa nadal ma 50 metod publicznych, po prostu ukrywasz ten zapach kodu, w pewien sposób wkładasz śmieci do szuflad.
  3. Codebase jest w rzeczywistości trudniejszy w nawigacji z wszystkimi tymi obawami.
  4. Czy jesteś pewien, że wszyscy członkowie twojego zespołu mają takie samo zrozumienie, co naprawdę powinno zastąpić troskę?

Obawy są łatwym sposobem na zastrzelenie się w nogę, bądź z nimi ostrożny.


1
Wiem, że SO nie jest najlepszym miejscem do dyskusji, ale jaki inny rodzaj Rubinowego mixinu utrzymuje twoje zajęcia w suchości? Wygląda na to, że powody nr 1 i nr 2 w twoich argumentach są sprzeczne, chyba że popierasz lepszy projekt OO, warstwę usług lub coś innego, czego mi brakuje? (Nie zgadzam się - sugeruję, że dodanie alternatyw pomaga!)
toobulkeh

2
Korzystanie z github.com/AndyObtiva/super_module to jedna opcja, używanie starych dobrych wzorców ClassMethods to kolejna opcja. Korzystanie z większej liczby obiektów (takich jak usługi) w celu czystego rozdzielenia problemów jest zdecydowanie najlepszym rozwiązaniem.
Dr.Strangelove

4
Głosowanie w dół, ponieważ nie jest to odpowiedź na pytanie. To jest opinia. Jest to opinia, która z pewnością ma swoje zalety, ale nie powinna być odpowiedzią na pytanie na StackOverflow.
Adam

2
@Adam To uparta opinia. Wyobraź sobie, że ktoś zapytałby, jak używać zmiennych globalnych w szynach, z pewnością wspomina, że ​​istnieją lepsze sposoby robienia rzeczy (np. Redis.current vs $ redis) mogą być użytecznymi informacjami na początek tematu? Tworzenie oprogramowania jest z natury dyscypliną, której nie można obejść. W rzeczywistości postrzegam opinie jako odpowiedzi i dyskusje, która odpowiedź jest najlepsza przez cały czas w
przepełnieniu stosu

2
Jasne, wymienienie go wraz z odpowiedzią na pytanie wydaje się w porządku. Nic w twojej odpowiedzi nie odpowiada jednak na pytanie PO. Jeśli wszystko, co chcesz zrobić, to ostrzec kogoś, dlaczego nie powinien używać obaw lub zmiennych globalnych, to byłby dobry komentarz, który możesz dodać do jego pytania, ale tak naprawdę nie jest to dobra odpowiedź.
Adam


46

Czułem, że większość przykładów pokazuje siłę, modulea nie ActiveSupport::Concernwartość dodaną module.

Przykład 1: Bardziej czytelne moduły.

Więc bez obaw, jak typowy modulebędzie.

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

Po refaktoryzacji za pomocą 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

Widzisz metody instancji, metody klas i dołączony blok są mniej nieporządne. Obawy wstrzykną je odpowiednio dla ciebie. To jedna z zalet używania ActiveSupport::Concern.


Przykład 2: Obsługuj zależności modułu z wdziękiem.

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

W tym przykładzie Barjest moduł, który Hostnaprawdę potrzebuje. Ale skoro Barzależność Foood Hostklasy musi include Foo(ale poczekaj, dlaczego Hostchce wiedzieć Foo? Czy można tego uniknąć?).

BarDodaje więc zależność wszędzie. I kolejność włączenia liczy również tutaj. To dodaje wiele złożoności / zależności do ogromnej bazy kodu.

Po refaktoryzacji za pomocą 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

Teraz wygląda to prosto.

Jeśli zastanawiasz się, dlaczego nie możemy dodać Foozależności w Barsamym module? To nie zadziała, ponieważ method_injected_by_foo_to_host_klassmusi zostać wprowadzone do klasy, która Barnie obejmuje Barsamego modułu.

Źródło: Rails ActiveSupport :: Koncern


Dziękuję za to. Zaczynałem się zastanawiać, jaka jest ich przewaga ...
Hari Karam Singh

FWIW to z grubsza kopiuj-wklej z dokumentów .
Dave Newton

7

W przypadku wątpliwości wykonaj plik nazwa_pliku.rb

Na przykład chcę w mojej aplikacji, w której istnieje atrybut create_by, zaktualizuj tam wartość o 1, a 0 dla 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

Jeśli chcesz przekazać argumenty w akcji

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

potem dołącz do swojego modelu w ten sposób:

class Role < ActiveRecord::Base
  include TestConcern
end
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.