วิธีใช้ข้อกังวลใน Rails 4


628

ตอนนี้ตัวสร้างโปรเจ็กต์ Rails 4 เริ่มต้นจะสร้างไดเรกทอรี "กังวล" ภายใต้ตัวควบคุมและรุ่น ฉันพบคำอธิบายบางอย่างเกี่ยวกับวิธีใช้ความกังวลในการกำหนดเส้นทาง แต่ไม่มีอะไรเกี่ยวกับตัวควบคุมหรือรุ่น

ฉันค่อนข้างแน่ใจว่าเกี่ยวข้องกับ "กระแส DCI" ในชุมชนปัจจุบันและต้องการลองดู

คำถามคือฉันควรใช้คุณสมบัตินี้อย่างไรมีการประชุมเกี่ยวกับวิธีกำหนดลำดับชั้นการตั้งชื่อ / คลาสเพื่อให้ทำงานได้หรือไม่ ฉันจะรวมข้อกังวลในแบบจำลองหรือตัวควบคุมได้อย่างไร

คำตอบ:


617

ดังนั้นฉันพบมันด้วยตัวเอง จริงๆแล้วมันเป็นแนวคิดที่เรียบง่าย แต่ทรงพลัง มันจะทำอย่างไรกับการใช้รหัสซ้ำในตัวอย่างด้านล่าง โดยทั่วไปความคิดคือการแยกโค้ดทั่วไปและ / หรือบริบทเฉพาะเพื่อทำความสะอาดโมเดลและหลีกเลี่ยงการทำให้อ้วนและยุ่งเกินไป

เป็นตัวอย่างฉันจะใส่หนึ่งรูปแบบที่รู้จักกันดีรูปแบบแท็ก:

# 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

ดังนั้นหลังจากตัวอย่างผลิตภัณฑ์คุณสามารถเพิ่ม Taggable ให้กับคลาสที่คุณต้องการและใช้งานร่วมกันได้

นี่เป็นคำอธิบายที่ค่อนข้างดีโดยDHH :

ใน Rails 4 เราจะเชิญโปรแกรมเมอร์ให้ใช้ข้อกังวลกับแอพเริ่มต้น / รุ่น / ข้อกังวลและแอพ / ตัวควบคุม / ไดเรกทอรีที่เกี่ยวข้องซึ่งเป็นส่วนหนึ่งของเส้นทางการโหลดโดยอัตโนมัติ เมื่อใช้ร่วมกับ ActiveSupport :: wrapper ที่เกี่ยวข้องมันสนับสนุนได้เพียงพอที่จะทำให้กลไกแฟตตอริ่งที่มีน้ำหนักเบาเปล่งประกาย


11
DCI เกี่ยวข้องกับบริบทใช้บทบาทเป็นตัวระบุในการแมปแบบจำลองทางจิต / กรณีการใช้รหัสและไม่ต้องใช้ wrappers ที่จะใช้
ciscoheat

2
@yagooar แม้การรวมไว้ที่รันไทม์จะไม่ทำให้ DCI หากคุณต้องการดูตัวอย่างการนำทับทิม DCI มาใช้ ลองดูที่fulloo.infoหรือตัวอย่างที่github.com/runefs/Mobyหรือวิธีใช้ maroon เพื่อทำ DCI ใน Ruby และอะไรคือ DCI runefs.com (DCI คืออะไรคือชุดของโพสต์ที่ฉันได้ เพิ่งเริ่มเร็ว ๆ นี้)
Rune FS

1
@RuneFS && ciscoheat คุณทั้งคู่ถูกต้อง ฉันเพิ่งวิเคราะห์บทความและข้อเท็จจริงอีกครั้ง และฉันไปสัปดาห์สุดท้ายที่การประชุม Ruby ที่หนึ่งพูดคุยเกี่ยวกับ DCI และในที่สุดฉันก็เข้าใจปรัชญาอีกเล็กน้อย เปลี่ยนข้อความดังนั้นจึงไม่พูดถึง DCI เลย
yagooar

9
เป็นมูลค่าการกล่าวขวัญ (และอาจรวมถึงในตัวอย่าง) ว่าวิธีการเรียนควรจะกำหนดไว้ในโมดูลชื่อ ClassMethods และโมดูลนี้จะขยายโดยชั้นฐานเป็น ActiveSupport :: ความกังวลเช่นกัน
febeling

1
ขอบคุณสำหรับตัวอย่างนี้ส่วนใหญ่ b / c ฉันเป็นใบ้และกำหนดวิธีการเรียนระดับของฉันในโมดูล ClassMethods ด้วยตนเองสิ่งที่ยังคงและที่ไม่ทำงาน = P
Ryan Crews

379

ฉันได้อ่านเกี่ยวกับการใช้แบบจำลองที่เกี่ยวข้องกับแบบจำลองไขมันในผิวหนังรวมถึง DRY ที่เป็นไปตามรหัสรุ่น นี่คือคำอธิบายพร้อมตัวอย่าง:

1) DRYING ค่ารหัสรุ่น

พิจารณาโมเดลบทความโมเดลเหตุการณ์และโมเดลข้อคิดเห็น บทความหรือเหตุการณ์มีความคิดเห็นมากมาย ความคิดเห็นเป็นของบทความหรือเหตุการณ์

ตามเนื้อผ้ารุ่นอาจมีลักษณะเช่นนี้

รูปแบบความคิดเห็น:

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

รูปแบบบทความ:

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

รูปแบบกิจกรรม

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

ดังที่เราสามารถสังเกตเห็นได้ว่ามีส่วนสำคัญของรหัสทั่วไปสำหรับทั้งเหตุการณ์และบทความ ด้วยความกังวลเราสามารถแยกรหัสทั่วไปนี้ในโมดูลที่แยกต่างหากแสดงความคิดเห็นได้

สำหรับสิ่งนี้ให้สร้างไฟล์ commentable.rb ในแอพ / รุ่น / ข้อกังวล

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

และตอนนี้แบบจำลองของคุณมีลักษณะเช่นนี้:

รูปแบบความคิดเห็น:

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

รูปแบบบทความ:

class Article < ActiveRecord::Base
  include Commentable
end

รูปแบบกิจกรรม:

class Event < ActiveRecord::Base
  include Commentable
end

2) แบบจำลองการสลายไขมันด้วยผิวหนัง

พิจารณาโมเดลเหตุการณ์ เหตุการณ์มีผู้เข้าร่วมและความคิดเห็นมากมาย

โดยทั่วไปโมเดลเหตุการณ์อาจมีลักษณะเช่นนี้

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

แบบจำลองที่มีการเชื่อมโยงมากมายและมีแนวโน้มที่จะสะสมรหัสมากขึ้นเรื่อย ๆ และไม่สามารถจัดการได้ ข้อกังวลมีวิธีในการทำให้โมดูลไขมันปรับสภาพผิวทำให้พวกมันเป็นโมดูลมากขึ้นและเข้าใจได้ง่ายขึ้น

โมเดลดังกล่าวสามารถปรับเปลี่ยนได้โดยใช้ข้อกังวลดังต่อไปนี้: สร้างattendable.rbและcommentable.rbไฟล์ในโฟลเดอร์แอพ / รุ่น / ข้อกังวล / เหตุการณ์

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

และเมื่อใช้ความกังวลโมเดลกิจกรรมของคุณจะลดลง

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

* ในขณะที่ใช้ความกังวลมันแนะนำให้ไปสำหรับการจัดกลุ่มตาม 'โดเมน' มากกว่าการจัดกลุ่ม 'ทางเทคนิค' การจัดกลุ่มตามโดเมนคือ 'ความเห็นได้', 'ภาพถ่ายได้', 'เข้าร่วมได้' การจัดกลุ่มทางเทคนิคจะหมายถึง 'ValidationMethods', 'FinderMethods' ฯลฯ


6
ดังนั้นความกังวลเป็นเพียงวิธีการใช้การสืบทอดหรือส่วนต่อประสานหรือการรับมรดกหลาย ๆ เกิดอะไรขึ้นกับการสร้างคลาสพื้นฐานทั่วไปและคลาสย่อยจากคลาสพื้นฐานทั่วไปนั้น
Chloe

3
แท้จริง @Chloe ฉันบางที่สีแดงแอป Rails ที่มีไดเรกทอรี 'กังวล' จริง ๆ แล้วเป็น 'กังวล' ...
Ziyan Junaideen

คุณสามารถใช้บล็อก 'รวม' เพื่อกำหนดวิธีการทั้งหมดของคุณและรวมถึง: วิธีการเรียน (ด้วยdef self.my_class_method), วิธีการเช่นและการเรียกวิธีการและคำสั่งในขอบเขตชั้นเรียน ไม่จำเป็นสำหรับmodule ClassMethods
เฟดเดอร์ Darkly

1
ปัญหาที่ฉันมีเกี่ยวกับความกังวลก็คือพวกเขาเพิ่มฟังก์ชั่นโดยตรงกับรูปแบบ ดังนั้นหากทั้งสองเกี่ยวข้องกับการใช้งานทั้งสองอย่างadd_itemคุณจะถูกเมา ฉันจำได้ว่า Rails พังเมื่อผู้ตรวจสอบบางรายหยุดทำงาน แต่มีคนดำเนินการตามany?ที่กังวล ฉันเสนอวิธีแก้ไขปัญหาที่แตกต่าง: ใช้ความกังวลเช่นอินเทอร์เฟซในภาษาอื่น แทนที่จะกำหนดฟังก์ชันการทำงานจะกำหนดการอ้างอิงไปยังอินสแตนซ์คลาสแยกต่างหากที่จัดการฟังก์ชันการทำงานนั้น ถ้างั้นคุณก็จะมีชั้นเรียนขนาดเล็กลงที่ทำสิ่งหนึ่ง ...
เฟดเดอร์ดาร์ก

@aaditi_jain: โปรดแก้ไขการเปลี่ยนแปลงเล็กน้อยเพื่อหลีกเลี่ยงความเข้าใจผิด เช่น "สร้างไฟล์ accessable.rd และ commentable.rb ในโฟลเดอร์ app / models / problems / event" -> ต้องมีไฟล์ connectable.rd ขอบคุณ
Rubyistist

97

เป็นเรื่องที่ควรกล่าวถึงว่าการใช้ข้อกังวลนั้นถือเป็นความคิดที่ไม่ดีโดยหลายคน

  1. ชอบผู้ชายคนนี้
  2. และอันนี้

ด้วยเหตุผลบางอย่าง

  1. มีความมืดเกิดขึ้นเบื้องหลัง - ความกังวลคือincludeวิธีการแพทช์มีระบบการจัดการที่พึ่งพาทั้งหมด - มีความซับซ้อนมากเกินไปสำหรับบางสิ่งที่เป็นรูปแบบทับทิมมิกซ์เก่าที่ดี
  2. ชั้นเรียนของคุณไม่แห้ง หากคุณใช้วิธีสาธารณะ 50 วิธีในโมดูลที่หลากหลายและรวมไว้ในชั้นเรียนของคุณยังคงมีวิธีสาธารณะ 50 วิธีเพียงแค่คุณซ่อนกลิ่นรหัสนั้นจัดเรียงขยะในลิ้นชัก
  3. จริง ๆ แล้ว Codebase นั้นยากที่จะสำรวจความกังวลเหล่านั้นทั้งหมด
  4. คุณแน่ใจหรือไม่ว่าสมาชิกทุกคนในทีมของคุณมีความเข้าใจเหมือนกันว่าอะไรควรนำมาแทนที่ข้อกังวล

ข้อกังวลเป็นวิธีง่าย ๆ ในการยิงตัวเองที่ขาระวังตัวด้วย


1
ฉันรู้ว่า SO ไม่ใช่สถานที่ที่ดีที่สุดสำหรับการสนทนานี้ แต่ Ruby mixin ประเภทอื่น ๆ ที่ทำให้ชั้นเรียนของคุณแห้ง ดูเหมือนเหตุผลที่ # 1 และ # 2 ในข้อโต้แย้งของคุณเป็นตัวนับเว้นแต่คุณเพียงแค่ทำให้การออกแบบ OO ที่ดีขึ้นชั้นบริการหรืออะไรที่ฉันขาดหายไป? (ฉันไม่เห็นด้วย - ฉันแนะนำให้เพิ่มทางเลือกช่วย!)
toobulkeh

2
การใช้github.com/AndyObtiva/super_moduleเป็นตัวเลือกหนึ่งโดยใช้รูปแบบ ClassMethods เก่าที่ดีเป็นอีกตัวเลือกหนึ่ง และการใช้วัตถุมากขึ้น (เช่นบริการ) เพื่อแยกข้อกังวลออกจากกันอย่างหมดจดเป็นวิธีที่จะไปอย่างแน่นอน
Dr.Strangelove

4
Downvoting เพราะนี่ไม่ใช่คำตอบสำหรับคำถาม มันเป็นความเห็น เป็นความเห็นที่ฉันแน่ใจว่ามีข้อดี แต่ไม่ควรตอบคำถามใน StackOverflow
Adam

2
@Adam มันเป็นคำตอบที่มีความเห็น ลองนึกภาพใครบางคนจะถามวิธีการใช้ตัวแปรทั่วโลกในทางรถไฟแน่นอนว่ามีวิธีที่ดีกว่าในการทำสิ่งต่าง ๆ (เช่น Redis.current เทียบกับ $ redis) อาจเป็นข้อมูลที่มีประโยชน์สำหรับผู้เริ่มต้นหัวข้อ? การพัฒนาซอฟต์แวร์นั้นเป็นระเบียบวินัยที่มีความคิดเห็นโดยปริยาย ในความเป็นจริงฉันเห็นความคิดเห็นว่าเป็นคำตอบและการอภิปรายว่าคำตอบใดดีที่สุดตลอดเวลาใน stackoverflow และเป็นสิ่งที่ดี
Dr.Strangelove

2
แน่นอนว่าการพูดถึงมันพร้อมกับคำตอบของคุณสำหรับคำถามนั้นดูดี ไม่มีคำตอบของคุณในการตอบคำถามของ OP จริง ๆ หากสิ่งที่คุณต้องการทำคือเตือนใครบางคนว่าทำไมพวกเขาไม่ควรใช้ความกังวลหรือตัวแปรทั่วโลกที่จะทำให้ความคิดเห็นที่ดีที่คุณสามารถเพิ่มคำถามของพวกเขา แต่มันไม่ได้ทำให้คำตอบที่ดีจริงๆ
อดัม

56

โพสต์นี้ช่วยให้ฉันเข้าใจข้อกังวล

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

# app/models/concerns/shared/schedule.rb
module Shared::Schedule
  extend ActiveSupport::Concern
  ...
end

1
รุ่นเก็บถาวรทางอินเทอร์เน็ต: web.archive.org/web/20130712014326/http://blog.andywaite.com/
MZB

1
คำตอบนี้ไม่ได้อธิบายอะไรเลย

46

ผมก็รู้สึกว่าส่วนใหญ่ของตัวอย่างที่นี่แสดงให้เห็นถึงพลังของmoduleมากกว่าวิธีการเพิ่มมูลค่าให้กับActiveSupport::Concernmodule

ตัวอย่างที่ 1:โมดูลที่อ่านได้มากขึ้น

ดังนั้นโดยไม่ต้องกังวลว่าจะเป็นเรื่องปกติmoduleอย่างไร

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

หลังจาก refactoring 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

คุณจะเห็นวิธีการอินสแตนซ์วิธีการเรียนและบล็อกรวมจะยุ่งน้อย ความกังวลจะทำให้พวกเขาอย่างเหมาะสมสำหรับคุณ ActiveSupport::Concernนั่นเป็นหนึ่งในประโยชน์ของการใช้


ตัวอย่างที่ 2:จัดการการพึ่งพาโมดูลอย่างสง่างาม

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

ในตัวอย่างนี้Barเป็นโมดูลที่Hostต้องการจริงๆ แต่เนื่องจากBarมีการพึ่งพากับระดับต้องFooHostinclude Foo ( แต่ทำไมรอไม่Hostอยากรู้เกี่ยวกับFooมันสามารถหลีกเลี่ยงได้?)

ดังนั้น Barเพิ่มการพึ่งพาทุกที่ที่มันไป และลำดับของการรวมก็มีความสำคัญเช่นกัน สิ่งนี้จะเพิ่มความซับซ้อน / การพึ่งพาฐานรหัสจำนวนมาก

หลังจากปรับสภาพด้วย 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

ตอนนี้มันดูเรียบง่าย

หากคุณกำลังคิดว่าทำไมเราไม่สามารถเพิ่มการFooพึ่งพาในBarโมดูลเองได้? ที่จะไม่ทำงานเนื่องจากmethod_injected_by_foo_to_host_klassต้องถูกฉีดในชั้นเรียนที่รวมถึงBarไม่ได้อยู่ในBarโมดูลตัวเอง

ที่มา: Rails ActiveSupport :: Concern


ขอบคุณสำหรับสิ่งนั้น ฉันเริ่มสงสัยว่าข้อได้เปรียบของพวกเขาคืออะไร ...
Hari Karam Singh

FWIW นี้จะประมาณคัดลอกวางจากเอกสาร
Dave Newton

7

ในความกังวลให้ไฟล์ชื่อไฟล์. rb

ตัวอย่างเช่นฉันต้องการในแอปพลิเคชันของฉันที่ attribute create_by มีอยู่อัปเดตที่นั่นค่า 1 และ 0 สำหรับ 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

หากคุณต้องการที่จะผ่านข้อโต้แย้งในการดำเนินการ

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

หลังจากนั้นรวมไว้ในแบบจำลองของคุณเช่นนี้:

class Role < ActiveRecord::Base
  include TestConcern
end
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.