รูปแบบการเผยแพร่-สมัครสมาชิกบน Rails: บทแนะนำการใช้งาน
เผยแพร่แล้ว: 2022-03-11รูปแบบการเผยแพร่-สมัครสมาชิก (หรือเรียกสั้นๆ ว่า pub/sub) เป็นรูปแบบการส่งข้อความ Ruby on Rails ที่ผู้ส่งข้อความ (ผู้เผยแพร่) ไม่ได้ตั้งโปรแกรมข้อความที่จะส่งโดยตรงไปยังผู้รับเฉพาะ (สมาชิก) แต่โปรแกรมเมอร์ "เผยแพร่" ข้อความ (เหตุการณ์) โดยไม่ทราบว่าสมาชิกใด ๆ อาจมีอยู่
ในทำนองเดียวกัน สมาชิกแสดงความสนใจในเหตุการณ์อย่างน้อยหนึ่งเหตุการณ์ และรับเฉพาะข้อความที่น่าสนใจเท่านั้น โดยที่ผู้เผยแพร่ไม่ทราบใดๆ
เพื่อให้บรรลุสิ่งนี้ ตัวกลางที่เรียกว่า "นายหน้าข้อความ" หรือ "รถบัสเหตุการณ์" ได้รับข้อความที่เผยแพร่แล้วส่งต่อไปยังสมาชิกที่ลงทะเบียนเพื่อรับข้อความ
กล่าวอีกนัยหนึ่ง pub-sub เป็นรูปแบบที่ใช้ในการสื่อสารข้อความระหว่างส่วนประกอบต่างๆ ของระบบ โดยที่ส่วนประกอบเหล่านี้ไม่ทราบเกี่ยวกับเอกลักษณ์ของกันและกัน
รูปแบบการออกแบบนี้ไม่ใช่เรื่องใหม่ แต่นักพัฒนา Rails มักไม่ได้ใช้รูปแบบนี้ มีเครื่องมือมากมายที่ช่วยรวมรูปแบบการออกแบบนี้เข้ากับฐานโค้ดของคุณ เช่น:
- Wisper (ซึ่งโดยส่วนตัวแล้วฉันชอบและจะพูดถึงมันต่อไป)
- อีเว้นท์บัส
- EventBGBus (ทางแยกของ EventBus)
- RabbitMQ
- Redis
เครื่องมือทั้งหมดเหล่านี้มีการใช้งาน pub-sub ที่แตกต่างกัน แต่เครื่องมือเหล่านี้ทั้งหมดมีข้อดีหลักเหมือนกันสำหรับแอปพลิเคชัน Rails
ข้อดีของการนำ Pub-Sub ไปใช้
ลดรุ่น/ตัวควบคุมบวม
เป็นแนวทางปฏิบัติทั่วไป แต่ไม่ใช่แนวทางปฏิบัติที่ดีที่สุดที่จะมีโมเดลหรือตัวควบคุมที่มีไขมันบางตัวในแอปพลิเคชัน Rails ของคุณ
รูปแบบผับ/ย่อย สามารถ ช่วยย่อยสลายแบบจำลองหรือตัวควบคุมไขมันได้อย่างง่ายดาย
โทรกลับน้อยลง
การเรียกกลับที่เชื่อมโยงกันจำนวนมากระหว่างแบบจำลองนั้นเป็นกลิ่นของรหัสที่รู้จักกันดี และค่อย ๆ จับคู่แบบจำลองเข้าด้วยกัน ทำให้ยากต่อการบำรุงรักษาหรือขยาย
ตัวอย่างเช่น Post
model อาจมีลักษณะดังนี้:
# app/models/post.rb class Post # ... field: content, type: String # ... after_create :create_feed, :notify_followers # ... def create_feed Feed.create!(self) end def notify_followers User::NotifyFollowers.call(self) end end
และตัวควบคุม Post
อาจมีลักษณะดังนี้:
# app/controllers/api/v1/posts_controller.rb class Api::V1::PostsController < Api::V1::ApiController # ... def create @post = current_user.posts.build(post_params) if @post.save render_created(@post) else render_unprocessable_entity(@post.errors) end end # ... end
อย่างที่คุณเห็น โมเดล Post
มีการเรียกกลับที่เชื่อมโยงโมเดลกับทั้งโมเดล Feed
และ User::NotifyFollowers
บริการหรือข้อกังวลอย่างแน่นหนา โดยใช้รูปแบบ pub/sub ใดๆ โค้ดก่อนหน้าอาจถูก re-factored ให้มีลักษณะดังนี้ ซึ่งใช้ Wisper:
# app/models/post.rb class Post # ... field: content, type: String # ... # no callbacks in the models! end
ผู้จัดพิมพ์ เผยแพร่เหตุการณ์ด้วยวัตถุ payload ของเหตุการณ์ที่อาจจำเป็น
# app/controllers/api/v1/posts_controller.rb # corresponds to the publisher in the previous figure class Api::V1::PostsController < Api::V1::ApiController include Wisper::Publisher # ... def create @post = current_user.posts.build(post_params) if @post.save # Publish event about post creation for any interested listeners publish(:post_create, @post) render_created(@post) else # Publish event about post error for any interested listeners publish(:post_errors, @post) render_unprocessable_entity(@post.errors) end end # ... end
สมาชิกสมัคร เฉพาะกิจกรรมที่พวกเขาต้องการตอบกลับ
# app/listener/feed_listener.rb class FeedListener def post_create(post) Feed.create!(post) end end
# app/listener/user_listener.rb class UserListener def post_create(post) User::NotifyFollowers.call(self) end end
Event Bus ลงทะเบียนสมาชิกที่แตกต่างกันในระบบ
# config/initializers/wisper.rb Wisper.subscribe(FeedListener.new) Wisper.subscribe(UserListener.new)
ในตัวอย่างนี้ รูปแบบ pub-sub กำจัดการเรียกกลับในโมเดล Post
โดยสิ้นเชิง และช่วยให้โมเดลทำงานแยกจากกันโดยมีความรู้ขั้นต่ำเกี่ยวกับกันและกัน ทำให้มั่นใจได้ว่าการมีเพศสัมพันธ์จะหลวม การขยายพฤติกรรมไปสู่การดำเนินการเพิ่มเติมเป็นเพียงเรื่องของการเชื่อมโยงกับเหตุการณ์ที่ต้องการ
หลักการความรับผิดชอบเดียว (SRP)
หลักการความรับผิดชอบเดียวมีประโยชน์มากสำหรับการรักษาฐานโค้ดที่สะอาด ปัญหาในการยึดมั่นคือบางครั้งความรับผิดชอบของชั้นเรียนไม่ชัดเจนเท่าที่ควร นี่เป็นเรื่องปกติโดยเฉพาะอย่างยิ่งเมื่อพูดถึง MVC (เช่น Rails)
โมเดล ควรจัดการกับความพากเพียร ความสัมพันธ์ และอื่นๆ ไม่มาก
ผู้ควบคุม ควรจัดการคำขอของผู้ใช้และเป็นผู้ปิดล้อมตรรกะทางธุรกิจ (Service Objects)
Service Objects ควรสรุปหนึ่งในความรับผิดชอบของตรรกะทางธุรกิจ จัดเตรียมจุดเริ่มต้นสำหรับบริการภายนอก หรือทำหน้าที่เป็นทางเลือกแทนข้อกังวลของแบบจำลอง
ด้วยพลังในการลด coupling รูปแบบการออกแบบ pub-sub สามารถรวมเข้ากับวัตถุบริการความรับผิดชอบเดียว (SRSO) เพื่อช่วยห่อหุ้มตรรกะทางธุรกิจ และห้ามตรรกะทางธุรกิจไม่ให้คืบคลานเข้าไปในแบบจำลองหรือตัวควบคุม ซึ่งช่วยให้ฐานโค้ดสะอาด อ่านได้ บำรุงรักษา และปรับขนาดได้
ต่อไปนี้คือตัวอย่างของตรรกะทางธุรกิจที่ซับซ้อนซึ่งนำมาใช้โดยใช้รูปแบบ pub/sub และออบเจ็กต์บริการ:
สำนักพิมพ์
# app/service/financial/order_review.rb class Financial::OrderReview include Wisper::Publisher # ... def self.call(order) if order.approved? publish(:order_create, order) else publish(:order_decline, order) end end # ...
สมาชิก
# app/listener/client_listener.rb class ClientListener def order_create(order) # can implement transaction using different service objects Client::Charge.call(order) Inventory::UpdateStock.call(order) end def order_decline(order) Client::NotifyDeclinedOrder(order) end end
ด้วยการใช้รูปแบบการเผยแพร่-สมัครสมาชิก ฐานโค้ดจะได้รับการจัดระเบียบเป็น SRSO เกือบจะโดยอัตโนมัติ นอกจากนี้ การนำโค้ดไปใช้สำหรับเวิร์กโฟลว์ที่ซับซ้อนสามารถจัดระเบียบได้อย่างง่ายดายตามเหตุการณ์ โดยไม่สูญเสียความสามารถในการอ่าน การบำรุงรักษา หรือความสามารถในการปรับขนาด
การทดสอบ
การสลายตัวของโมเดลและตัวควบคุมที่มีไขมัน และมี SRSO จำนวนมาก การทดสอบฐานโค้ดจะกลายเป็นกระบวนการที่ง่ายขึ้นมาก โดยเฉพาะอย่างยิ่งกรณีนี้เมื่อพูดถึงการทดสอบการรวมและการสื่อสารระหว่างโมดูล การทดสอบควรตรวจสอบให้แน่ใจว่ามีการเผยแพร่และรับกิจกรรมอย่างถูกต้อง

Wisper มีอัญมณีทดสอบที่เพิ่มตัวจับคู่ RSpec เพื่อทำให้การทดสอบส่วนประกอบต่างๆ ง่ายขึ้น
ในสองตัวอย่างก่อนหน้า ( ตัวอย่าง Post
และ ตัวอย่าง Order
) การทดสอบควรมีดังต่อไปนี้:
สำนักพิมพ์
# spec/service/financial/order_review.rb describe Financial::OrderReview do it 'publishes :order_create' do @order = Fabricate(:order, approved: true) expect { Financial::OrderReview.call(@order) }.to broadcast(:order_create) end it 'publishes :order_decline' do @order = Fabricate(:order, approved: false) expect { Financial::OrderReview.call(@order) }.to broadcast(:order_decline) end end
สมาชิก
# spec/listeners/feed_listener_spec.rb describe FeedListener do it 'receives :post_create event on PostController#create' do expect(FeedListner).to receive(:post_create).with(Post.last) post '/post', { content: 'Some post content' }, request_headers end end
อย่างไรก็ตาม มีข้อจำกัดบางประการในการทดสอบเหตุการณ์ที่เผยแพร่เมื่อผู้จัดพิมพ์เป็นผู้ควบคุม
หากคุณต้องการก้าวไปอีกขั้น การทดสอบเพย์โหลดด้วยจะช่วยรักษาฐานโค้ดที่ดียิ่งขึ้น
อย่างที่คุณเห็น การทดสอบรูปแบบการออกแบบ pub-sub นั้นง่ายมาก เป็นเพียงเรื่องของการทำให้มั่นใจว่ามีการเผยแพร่และรับเหตุการณ์ต่างๆ อย่างถูกต้อง
ผลงาน
นี่เป็นข้อ ได้ เปรียบที่เป็นไปได้มากกว่า รูปแบบการออกแบบ publish-subscribe นั้นไม่มีผลกระทบสำคัญต่อประสิทธิภาพของโค้ด อย่างไรก็ตาม เช่นเดียวกับเครื่องมือใดๆ ที่คุณใช้ในโค้ด เครื่องมือสำหรับการนำ pub/sub ไปใช้งาน อาจส่งผลกระทบอย่างมากต่อประสิทธิภาพ บางครั้งอาจส่งผลเสีย แต่บางครั้งอาจส่งผลดีมาก
อย่างแรก ตัวอย่างของผลกระทบที่ไม่พึงประสงค์: Redis คือ “แคชคีย์-ค่าขั้นสูงและการจัดเก็บ มักถูกเรียกว่าเซิร์ฟเวอร์โครงสร้างข้อมูล” เครื่องมือยอดนิยมนี้รองรับรูปแบบผับ/ย่อยและมีความเสถียรมาก อย่างไรก็ตาม หากใช้บนเซิร์ฟเวอร์ระยะไกล (ไม่ใช่เซิร์ฟเวอร์เดียวกันกับที่ปรับใช้แอปพลิเคชัน Rails) จะส่งผลให้สูญเสียประสิทธิภาพอย่างมากเนื่องจากค่าใช้จ่ายของเครือข่าย
ในทางกลับกัน Wisper มีอะแดปเตอร์หลายตัวสำหรับการจัดการเหตุการณ์แบบอะซิงโครนัส เช่น wisper-celluloid, wisper-sidekiq และ wisper-activejob เครื่องมือเหล่านี้รองรับเหตุการณ์แบบอะซิงโครนัสและการประมวลผลแบบเธรด ซึ่งหากนำไปใช้อย่างเหมาะสมสามารถเพิ่มประสิทธิภาพของแอพพลิเคชั่นได้อย่างมาก
บรรทัดล่าง
หากคุณกำลังตั้งเป้าไปที่ประสิทธิภาพที่เพิ่มขึ้น รูปแบบผับ/ย่อยสามารถช่วยให้คุณบรรลุเป้าหมายได้ แต่แม้ว่าคุณจะไม่พบการเพิ่มประสิทธิภาพด้วยรูปแบบการออกแบบ Rails นี้ แต่ก็ยังช่วยจัดระเบียบโค้ดและทำให้สามารถบำรุงรักษาได้มากขึ้น ท้ายที่สุดแล้ว ใครจะกังวลเกี่ยวกับประสิทธิภาพของโค้ดที่ไม่สามารถรักษาไว้ได้หรือใช้งานไม่ได้ตั้งแต่แรก
ข้อเสียของการใช้งาน Pub-Sub
เช่นเดียวกับทุกสิ่ง มีข้อเสียบางประการที่อาจเกิดขึ้นกับรูปแบบผับย่อยเช่นกัน
ข้อต่อหลวม (ข้อต่อแบบยืดหยุ่นไม่ได้)
จุดแข็งของรูปแบบผับ/ย่อยที่ใหญ่ที่สุดก็คือจุดอ่อนที่ยิ่งใหญ่ที่สุดเช่นกัน โครงสร้างของข้อมูลที่เผยแพร่ (ส่วนของข้อมูลเหตุการณ์) จะต้องมีการกำหนดไว้อย่างชัดเจน และไม่ยืดหยุ่นอย่างรวดเร็ว ในการปรับเปลี่ยนโครงสร้างข้อมูลของเพย์โหลดที่เผยแพร่ จำเป็นต้องทราบเกี่ยวกับผู้สมัครสมาชิกทั้งหมด และแก้ไขด้วย หรือตรวจสอบให้แน่ใจว่าการปรับเปลี่ยนนั้นเข้ากันได้กับเวอร์ชันเก่ากว่า ทำให้การปรับโครงสร้างโค้ดผู้เผยแพร่โฆษณาทำได้ยากขึ้นมาก
หากคุณต้องการหลีกเลี่ยงสิ่งนี้ คุณจะต้องระมัดระวังเป็นพิเศษในการกำหนดเพย์โหลดของผู้เผยแพร่ แน่นอน หากคุณมีชุดทดสอบที่ยอดเยี่ยม ซึ่งทดสอบส่วนของข้อมูลเช่นเดียวกับที่กล่าวไว้ก่อนหน้านี้ คุณไม่ต้องกังวลมากว่าระบบจะหยุดทำงานหลังจากที่คุณเปลี่ยนชื่อข้อมูลหรือกิจกรรมของผู้เผยแพร่
ความเสถียรของการรับส่งข้อความ
ผู้จัดพิมพ์ไม่มีความรู้เกี่ยวกับสถานะของสมาชิกและในทางกลับกัน การใช้เครื่องมือ Pub/sub แบบง่ายๆ อาจเป็นไปไม่ได้ที่จะรับรองความเสถียรของบัสรับส่งข้อความ และเพื่อให้แน่ใจว่าข้อความที่เผยแพร่ทั้งหมดอยู่ในคิวและนำส่งอย่างถูกต้อง
จำนวนข้อความที่มีการแลกเปลี่ยนกันมากขึ้นนำไปสู่ความไม่เสถียรในระบบเมื่อใช้เครื่องมือง่ายๆ และอาจเป็นไปไม่ได้ที่จะรับรองการส่งมอบให้กับสมาชิกทุกคนหากไม่มีโปรโตคอลที่ซับซ้อนกว่านี้ คุณอาจพิจารณาใช้บริการต่างๆ เช่น RabbitMQ, PubNub, Pusher, CloudAMQP, IronMQ หรือทางเลือกอื่นๆ ทั้งนี้ขึ้นอยู่กับจำนวนข้อความที่แลกเปลี่ยนและพารามิเตอร์ประสิทธิภาพที่คุณต้องการ ทางเลือกเหล่านี้มีฟังก์ชันพิเศษ และมีความเสถียรมากกว่า Wisper สำหรับระบบที่ซับซ้อนมากขึ้น อย่างไรก็ตาม พวกเขายังต้องการงานพิเศษบางอย่างในการดำเนินการ คุณสามารถอ่านเพิ่มเติมเกี่ยวกับวิธีการทำงานของนายหน้าข้อความได้ที่นี่
วนรอบเหตุการณ์ที่ไม่มีที่สิ้นสุด
เมื่อระบบขับเคลื่อนโดยเหตุการณ์โดยสมบูรณ์ คุณควรระมัดระวังเป็นพิเศษไม่ให้มีเหตุการณ์วนซ้ำ ลูปเหล่านี้เหมือนกับลูปอนันต์ที่สามารถเกิดขึ้นได้ในโค้ด อย่างไรก็ตาม การตรวจจับล่วงหน้านั้นยากกว่า และสามารถทำให้ระบบของคุณหยุดนิ่งได้ สามารถเกิดขึ้นได้โดยไม่ต้องแจ้งให้ทราบเมื่อมีกิจกรรมมากมายที่เผยแพร่และสมัครรับข้อมูลทั่วทั้งระบบ
สรุปบทช่วยสอน Rails
รูปแบบการเผยแพร่-สมัครรับข้อมูลไม่ใช่สัญลักษณ์แสดงหัวข้อย่อยสีเงินสำหรับปัญหา Rails และกลิ่นโค้ดทั้งหมดของคุณ แต่เป็นรูปแบบการออกแบบที่ดีมากที่ช่วยในการแยกส่วนประกอบระบบต่างๆ ออก และทำให้บำรุงรักษา อ่านง่ายขึ้น และปรับขนาดได้มากขึ้น
เมื่อรวมกับออบเจ็กต์บริการความรับผิดชอบเดียว (SRSO) pub-sub ยังสามารถช่วยในการห่อหุ้มตรรกะทางธุรกิจและป้องกันข้อกังวลทางธุรกิจที่แตกต่างกันจากการคืบคลานเข้าสู่แบบจำลองหรือตัวควบคุม
ประสิทธิภาพที่เพิ่มขึ้นหลังจากใช้รูปแบบนี้ขึ้นอยู่กับเครื่องมือพื้นฐานที่ใช้เป็นส่วนใหญ่ แต่ประสิทธิภาพที่เพิ่มขึ้นสามารถปรับปรุงได้อย่างมากในบางกรณี และโดยส่วนใหญ่แล้วจะไม่ส่งผลเสียต่อประสิทธิภาพอย่างแน่นอน
อย่างไรก็ตาม การใช้รูปแบบ Pub-sub ควรศึกษาและวางแผนอย่างรอบคอบ เนื่องจากด้วยพลังอันยิ่งใหญ่ของ coupling ที่หลวม ความรับผิดชอบที่ยิ่งใหญ่ในการบำรุงรักษาและการปรับโครงสร้างส่วนประกอบที่ประกอบเข้าด้วยกันอย่างหลวม ๆ
เนื่องจากเหตุการณ์ไม่สามารถควบคุมได้ง่าย ไลบรารีผับ/ย่อยแบบธรรมดาจึงอาจไม่รับประกันความเสถียรของตัวรับส่งข้อความ
และสุดท้าย อันตรายจากการแนะนำวนซ้ำของเหตุการณ์ที่ไม่มีที่สิ้นสุดซึ่งไม่มีใครสังเกตเห็นจนกว่าจะสายเกินไป
ฉันใช้รูปแบบนี้มาเกือบปีแล้ว และมันยากสำหรับฉันที่จะจินตนาการถึงการเขียนโค้ดโดยปราศจากมัน สำหรับฉัน มันคือกาวที่ทำให้งานเบื้องหลัง วัตถุบริการ ข้อกังวล ผู้ควบคุม และโมเดลทั้งหมดสื่อสารกันได้อย่างหมดจดและทำงานร่วมกันอย่างมีเสน่ห์
ฉันหวังว่าคุณจะได้เรียนรู้มากเท่ากับที่ฉันได้เรียนรู้จากการตรวจสอบโค้ดนี้ และคุณรู้สึกได้รับแรงบันดาลใจที่จะให้โอกาสรูปแบบการเผยแพร่-สมัครสมาชิก เพื่อทำให้แอปพลิเคชัน Rails ของคุณยอดเยี่ยม
สุดท้ายนี้ ขอขอบคุณ @krisleech สำหรับงานที่ยอดเยี่ยมของเขาในการนำ Wisper ไปใช้