ถ้าคุณเคยใช้ Ruby on Rails น่าจะเคยเจอ concept ของ concerns ตอน jumpstart Rails project ใหม่ จะได้ directory app/controllers/concerns และ app/models/concerns แต concerns คืออะไร? และทำไมคนใน Rails community บางคนพูดเล่นแย่เกี่ยวกับมัน?
ภาพรวมสั้นๆ
Rails Concern คือ module ใดๆ ที่ extend ActiveSupport::Concern module คุณอาจถาม — concerns ต่างจาก modules ยังไง? ความต่างหลักคือ Rails concerns ให้คุณทำ "magic" นิดหน่อยได้ เช่น:
# app/models/concerns/trashable.rb
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
end
end
เห็นคำว่า included มันคือ Rails carbohydrates เกิดบน Ruby module ActiveSupport::Concern ทำให้คุณวางโค้ดที่ต้องการให้ evaluate ภายใน included block ได้ เช่น อยากแยก trashing logic ออกจาก model included ให้ทำอย่างเราทำ และ include concern ใน model เช่น:
class Song < ApplicationRecord
include Trashable
has_many :authors
# ...
end
Pretty handy และ naive ณ จุดนี้ใช่ไหม? Model ลดน้ำหนักลงนิดหน่อย และ trashing นำกลับมาใช้ซ้ำใน models อื่นๆ ไม่ได้แค่ Song model เท่านั้น แต่สิ่งๆ สามารถซับซ้อนได้ มาลงลึกกัน
ตัวอย่างคลาสสิกของ Mixin
ก่อนจะลงลึก concerns เพิ่ม อธิบายอีกมุมมอง เมื่อเห็น include SomeModule หรือ extend AnotherModule เรียกว่า mixins Mixin คือชุดโค้ดที่เพิ่มเข้า class อื่นๆ ได้ และจาก Ruby documentation module คือ collection ของ methods และ constants ดังนั้นเราทำคือ include modules ที่มี methods และ constants เข้า class ต่างๆ ให้ใช้งานได้
นี่คือสิ่งที่ทำกับ concern Trashable แยก logic ทั่วไปรอบ trashing model object ไป module นี้ include ในที่อื่นได้ ดังนั้น mixin คือ design pattern ใช้ไม่ได้แค่ Ruby และ Rails แต่ทุกที่ที่ใช้ คน either ชอบคิดว่าดี หรือเกลียดคิดว่าควบคุมไม่ออก
เข้าใจดีกว่าจะไปดู pros และ cons ของการใช้ พอจะเข้าใจเมื่อไหร่หรือควรใช้ concerns
I Have It All (ฉันมีทุกอย่าง)
ตอนตัดสินใจ extract ไป concern เช่น Trashable คุณเข้าถึง functionality ทั้งหมดของที่ Trashable ถูก include นี่ให้ great power แต่เช่น Richard Schneeman กล่าวในบล็อก — "with great power comes great ability to make complicated code" เขาหมายถึง complicate code ที่คุณอาจ rely on บางอย่างที่ ควร อยู่ใน concerns
ดู Trashable อีกครั้ง:
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
end
end
Logic ของ concern rely บนว่า field trashed มีอยู่ที่ include concern ไป ไม่ใช่เรื่องใหญ่ นี่คือสิ่งที่เราอยากได้ แต่สิ่งที่เห็นเกิดคือ คนถูกล่อลวงให้ดึง stuff อื่นจาก model เข้า concern มาดูภาพว่าสามารถเกิดได้ยังไง สมมติ model Song มี method featured_authors อีก:
class Song < ApplicationRecord
include Trashable
has_many :authors
def featured_authors
authors.where(featured: true)
end
# ...
end
class Album < ApplicationRecord
include Trashable
has_many :authors
def featured_authors
authors.where(featured: true)
end
# ...
end
เพื่อ illustrate ให้ดีกว่า เพิ่ม model Album ที่ include Trashable เช่นกัน สมมติอยาก notify featured authors ของ song และ album เวลา trash คนมักถูกล่อลวงวาง logic นี้ใน concern:
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
notify(featured_authors)
end
def notify(authors)
# ...
end
end
ตรงนี้เริ่มซับซ้อนนิดหน่อย เนื่องจาก trashing logic อยู่นอก Song model อาจถูกล่อลวงวาง notifying ใน Trashable concern ที่นี่เกิดบางอย่าง "ผิด" featured_authors ถูกดึงมาจาก Song model โอเค ผ่าน pull request review และ CI checks
แล้วสองสามเดือนต่อมา requirement ใหม่ ต้องเปลี่ยนวิธี present featured_authors สำหรับ songs เช่น อยากแสดง featured authors จาก Europe เท่านั้น นิสัยนักพัฒนาจะหาที่ define featured_authors และแก้ไข:
class Song < ApplicationRecord
include Trashable
has_many :authors
def featured_authors
authors.where(featured: true).where(region: 'Europe')
end
# ...
end
class Album < ApplicationRecord
include Trashable
has_many :authors
# ...
end
ทำงานได้ดีที่แสดง authors แต่หลัง deploy production ชาวต่างประเทศจะไม่ได้รับ notify เกี่ยวกับเพลงของพวกเขาอีก Mistakes แบบนี้ง่ายเวลาใช้ concerns ตัวอย่างข้างบน simple และ artificial แต่ใน "the wild" สับสนมาก
สิ่งที่ risky คือ concern (mixin) รู้มากเกินไปเกี่ยวกับ model ที่ถูก include เรียกว่า circular dependency Song และ Album depend on Trashable สำหรับ trashing Trashable depend on ทั้งสองสำหรับ definition ของ featured_authors เหมือนกันสำหรับ field trashed ต้องมีในทั้งสอง model เพื่อให้ Trashable concern ทำงาน
นี่คือเหตุผล no-concern club against และ pro-concern club for ผมบอกว่า version แรกของ Trashable คือที่ผมเลือกใน codebase มาดูวิธีทำ version สองกับ notifying ให้ดีกว่า
Where Do Y'all Come From (มาจากไหนทั้งหลาย)
กลับมาที่ Trashable กับ notifying ต้องทำบางอย่าง อีกอย่างที่เกิดเวลาใช้ concerns คือ over-DRY things ลองทำดูกับ existing models โดยสร้าง concern อีกอัน (อดทนฟังนะ):
module Authorable
has_many :authors
def featured_authors
authors.where(featured: true)
end
end
Song และ Album จะกลายเป็น:
class Song < ApplicationRecord
include Trashable
include Authorable
# ...
end
class Album < ApplicationRecord
include Trashable
include Authorable
# ...
end
DRY ทุกอย่าง แต่ requirement featured authors จาก Europe ไม่ fulfilled ทำให้แย่ลง ตอนนี้ Trashable concern และ models depend on Authorable What the hell? คำถามเดียวกันตอนผม deal กับ concerns ก่อนหน้า ยาก track down ว่า methods มาจากไหน
Solution ของผมคือเก็บ featured_authors ใกล้ models ให้มากที่สุด notify method ควร ไม่ เป็นส่วนของ Trashable concern เลย แต่ละ model ควรจัดการเอง โดยเฉพาะถ้า notify subgroups ต่างกัน มาดูวิธีทำให้เจ็บน้อยกว่า:
# Concerns
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
end
end
module Authorable
has_many :authors
# Other useful methods that relate to authors across models.
# If there are none, ditch the concern.
end
# Models
class Song < ApplicationRecord
include Trashable
include Authorable
def featured_authors
authors.where(featured: true).where(region: 'Europe')
end
# ...
end
class Album < ApplicationRecord
include Trashable
include Authorable
def featured_authors
authors.where(featured: true)
end
# ...
end
Concerns แบบนี้ manageable และไม่ซับซ้อนเกินไป ข้าม notify functionality ที่อธิบายไว้ เพราะอาจเป็น topic อีกวัน
The Final Boss (บอสสุดท้าย)
สำหรับ Basecamp ผู้สร้าง Rails concerns referencing other concerns เห็นว่า perfectly fine ตาม DHH illustrated ใน tweet ไป:
ดู code screenshot คุณ either แก้มน่าตกใน awe หรือ appall ผมรู้สึกไม่มี in-between ถ้าได้โอกาส edit โค้ดนี้ จะ envision เป็น "Final Concern Boss Fight" แต่冗談แยก น่าสนใจคือมี comments บอกว่า concern ไหน depend on concern ไหน ดู:
# ...
include Subscribable # Depends on Readable
include Eventable # Depends on Recordables
# ...
Comments แบบนี้ช่วยได้ แต่ยัง setup สำหรับทำบางอย่าง sketchy โดยเฉพาะถ้าคุณใหม่ใน codebase การใหม่และไม่ aware ของ "gotchas" ทั้งหมด ส่งคุณลง concern downward spiral แน่นอน
อย่างนี้คือ DHH แชร์ใน comment ภายใน discussion Tweet ตอบกลับถามว่า folks ที่ทำงานกับ codebase นี้ interact กับ concerns แบบนี้ยังไง DHH ตอบว่า ไม่มี docs มากทีม rarely hire ทีมเข้าใจดี
แต่มี experienced team ที่รู้ codebase ดีเป็น argument สำหรับใช้ concerns weird และไม่แข็งแรง ผมคิดมันเป็น feeling มากกว่า คุณสบายกับ multiple inheritances ที่ modules ให้ไหม หรือชอบ composition? ตัดสินใจเอง
สรุป
เห็นแล้ว concerns คือ modules ที่ให้ syntax sugar เล็กน้อย extract และ DRY โค้ด ถ้ามี tools 有用กว่านี้ อาจไม่ควร reach out concerns ทันที Behavior เช่น handling file attachments และ trashing logic ที่โชว์ในตัวอย่างอาจเป็น good candidates extract ไป modules (concerns)
หวังว่าเห็น possible good และ bad things เวลา deal กับ concerns และ modules โดยรวม จำได้ว่า no code is perfect และสุดท้าย คุณเรียนรู้ได้ยังไงว่าอะไรดีอะไรแย่สำหรับคุณถ้าไม่ลองและอาจ fail หรือ succeed?
No solution is perfect หวังว่าเข้าใจ Rails concerns way of doing things ในบล็อกโพสต์นี้ Как всегда use your judgment และระวัง pros และ cons
Until the next one, cheers!
P.S. หากอยากอ่าน Ruby Magic posts ทันทีที่เผยแพร่ สมัครสมาชิก Ruby Magic newsletter และไม่พลาดโพสต์เด็ดๆ!