รูปแบบการเผยแพร่-สมัครสมาชิกบน Rails: บทแนะนำการใช้งาน

เผยแพร่แล้ว: 2022-03-11

รูปแบบการเผยแพร่-สมัครสมาชิก (หรือเรียกสั้นๆ ว่า pub/sub) เป็นรูปแบบการส่งข้อความ Ruby on Rails ที่ผู้ส่งข้อความ (ผู้เผยแพร่) ไม่ได้ตั้งโปรแกรมข้อความที่จะส่งโดยตรงไปยังผู้รับเฉพาะ (สมาชิก) แต่โปรแกรมเมอร์ "เผยแพร่" ข้อความ (เหตุการณ์) โดยไม่ทราบว่าสมาชิกใด ๆ อาจมีอยู่

ในทำนองเดียวกัน สมาชิกแสดงความสนใจในเหตุการณ์อย่างน้อยหนึ่งเหตุการณ์ และรับเฉพาะข้อความที่น่าสนใจเท่านั้น โดยที่ผู้เผยแพร่ไม่ทราบใดๆ

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

กล่าวอีกนัยหนึ่ง pub-sub เป็นรูปแบบที่ใช้ในการสื่อสารข้อความระหว่างส่วนประกอบต่างๆ ของระบบ โดยที่ส่วนประกอบเหล่านี้ไม่ทราบเกี่ยวกับเอกลักษณ์ของกันและกัน

ในบทแนะนำ Rails นี้ รูปแบบการออกแบบ publish-subscribe ถูกจัดวางในไดอะแกรมนี้

รูปแบบการออกแบบนี้ไม่ใช่เรื่องใหม่ แต่นักพัฒนา 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 ไปใช้