Rails Service Objects: คู่มือที่ครอบคลุม
เผยแพร่แล้ว: 2022-03-11Ruby on Rails มาพร้อมกับทุกสิ่งที่คุณต้องการเพื่อสร้างต้นแบบแอปพลิเคชันของคุณอย่างรวดเร็ว แต่เมื่อ Codebase ของคุณเริ่มเติบโต คุณจะเจอสถานการณ์ที่รูปแบบเดิมๆ ของ Fat Model, Skinny Controller แตกสลาย เมื่อตรรกะทางธุรกิจของคุณไม่พอดีกับโมเดลหรือตัวควบคุม นั่นคือเวลาที่ออบเจ็กต์บริการเข้ามา และให้เราแยกการดำเนินการทางธุรกิจทั้งหมดออกเป็นออบเจ็กต์ Ruby ของตัวเอง
ในบทความนี้ ฉันจะอธิบายเมื่อจำเป็นต้องใช้ออบเจ็กต์บริการ วิธีการเขียนวัตถุบริการที่สะอาดและจัดกลุ่มเข้าด้วยกันเพื่อความมีสติของผู้มีส่วนร่วม กฎเกณฑ์ที่เข้มงวดที่ฉันกำหนดให้กับออบเจ็กต์บริการเพื่อผูกเข้ากับตรรกะทางธุรกิจของฉันโดยตรง และวิธีที่จะไม่เปลี่ยนอ็อบเจ็กต์บริการของคุณให้กลายเป็นพื้นที่ทิ้งขยะสำหรับโค้ดทั้งหมดที่คุณไม่รู้ว่าต้องทำอย่างไร
เหตุใดฉันจึงต้องการออบเจ็กต์บริการ
ลองทำสิ่งนี้: คุณจะทำอย่างไรเมื่อแอปพลิเคชันของคุณต้องการทวีตข้อความจาก params[:message]
?
หากคุณเคยใช้ vanilla Rails มาก่อน คุณอาจเคยทำสิ่งนี้:
class TweetController < ApplicationController def create send_tweet(params[:message]) end private def send_tweet(tweet) client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['TWITTER_CONSUMER_KEY'] config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET'] config.access_token = ENV['TWITTER_ACCESS_TOKEN'] config.access_token_secret = ENV['TWITTER_ACCESS_SECRET'] end client.update(tweet) end end
ปัญหาคือคุณได้เพิ่มอย่างน้อยสิบบรรทัดในคอนโทรลเลอร์ของคุณ แต่จริงๆ แล้วมันไม่ได้อยู่ที่นั้น แล้วถ้าคุณต้องการใช้ฟังก์ชันเดียวกันนี้ในคอนโทรลเลอร์อื่นล่ะ คุณย้ายสิ่งนี้ไปสู่ข้อกังวลหรือไม่? เดี๋ยวก่อน แต่รหัสนี้ไม่อยู่ในคอนโทรลเลอร์เลย เหตุใด Twitter API จึงไม่สามารถมาพร้อมกับออบเจ็กต์ที่เตรียมไว้เพียงชิ้นเดียวให้ฉันโทรได้
ครั้งแรกที่ฉันทำสิ่งนี้ ฉันรู้สึกเหมือนทำอะไรสกปรก ตัวควบคุม Rails แบบลีนที่สวยงามของฉันก่อนหน้านี้เริ่มอ้วนขึ้น และฉันไม่รู้ว่าต้องทำอย่างไร ในที่สุด ฉันก็ซ่อมคอนโทรลเลอร์ด้วยวัตถุบริการ
ก่อนที่คุณจะเริ่มอ่านบทความนี้ ลองสมมุติว่า:
- แอปพลิเคชั่นนี้จัดการบัญชี Twitter
- The Rails Way หมายถึง “วิธีการทำสิ่งต่าง ๆ ของ Ruby on Rails แบบธรรมดา” และหนังสือเล่มนี้ไม่มีอยู่จริง
- ฉันเป็นผู้เชี่ยวชาญของ Rails… ซึ่งฉันบอกทุกวันว่าฉันเป็น แต่ฉันมีปัญหาในการเชื่อ ดังนั้นลองแสร้งทำเป็นว่าฉันเป็นคนหนึ่งจริงๆ
วัตถุบริการคืออะไร?
ออบเจ็กต์บริการคือออบเจ็กต์ Old Ruby Objects (PORO) แบบธรรมดาที่ออกแบบมาเพื่อเรียกใช้การดำเนินการเดียวในตรรกะของโดเมนของคุณและทำได้ดี ลองพิจารณาตัวอย่างด้านบน: วิธีการของเรามีเหตุผลที่จะทำสิ่งเดียวอยู่แล้ว นั่นคือการสร้างทวีต จะเกิดอะไรขึ้นถ้าตรรกะนี้ถูกห่อหุ้มไว้ภายในคลาส Ruby เดียวที่เราสามารถสร้างอินสแตนซ์และเรียกใช้เมธอดได้ สิ่งที่ต้องการ:
tweet_creator = TweetCreator.new(params[:message]) tweet_creator.send_tweet # Later on in the article, we'll add syntactic sugar and shorten the above to: TweetCreator.call(params[:message])
นี้มันสวยมาก; วัตถุบริการ TweetCreator
ของเราที่สร้างขึ้นเมื่อสามารถเรียกได้จากทุกที่และจะทำสิ่งหนึ่งได้เป็นอย่างดี
การสร้างวัตถุบริการ
ขั้นแรก มาสร้าง TweetCreator
ใหม่ในโฟลเดอร์ใหม่ชื่อ app/services
:
$ mkdir app/services && touch app/services/tweet_creator.rb
และทิ้งตรรกะทั้งหมดของเราไว้ในคลาส Ruby ใหม่:
# app/services/tweet_creator.rb class TweetCreator def initialize(message) @message = message end def send_tweet client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['TWITTER_CONSUMER_KEY'] config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET'] config.access_token = ENV['TWITTER_ACCESS_TOKEN'] config.access_token_secret = ENV['TWITTER_ACCESS_SECRET'] end client.update(@message) end end
จากนั้นคุณสามารถเรียก TweetCreator.new(params[:message]).send_tweet
ได้ทุกที่ในแอปของคุณ และมันจะใช้งานได้ Rails จะโหลดวัตถุนี้อย่างน่าอัศจรรย์เพราะจะโหลดทุกอย่างโดยอัตโนมัติภายใต้ app/
ตรวจสอบสิ่งนี้โดยเรียกใช้:
$ rails c Running via Spring preloader in process 12417 Loading development environment (Rails 5.1.5) > puts ActiveSupport::Dependencies.autoload_paths ... /Users/gilani/Sandbox/nazdeeq/app/services
ต้องการทราบข้อมูลเพิ่มเติมเกี่ยวกับวิธีการ autoload
หรือไม่ อ่านคู่มือการโหลดอัตโนมัติและโหลดซ้ำค่าคงที่
การเพิ่มน้ำตาลซินแทคติกเพื่อสร้างออบเจกต์บริการ Rails ให้น้อยลง
ฟังนะ รู้สึกดีในทางทฤษฎี แต่ TweetCreator.new(params[:message]).send_tweet
เป็นเพียงคำพูดหนึ่งคำเท่านั้น ใช้คำซ้ำซากมากเกินไป… เหมือน HTML (ba-dum tiss! ) จริงๆ แล้ว ทำไมคนถึงใช้ HTML เมื่อ HAML อยู่ใกล้ๆ หรือแม้แต่สลิม ฉันเดาว่าเป็นบทความอื่นสำหรับเวลาอื่น กลับไปที่งานในมือ:
TweetCreator
เป็นชื่อคลาสสั้นๆ ที่ดี แต่ความยุ่งยากพิเศษในการสร้างอินสแตนซ์ของอ็อบเจกต์และการเรียกเมธอดนั้นยาวเกินไป! หาก Ruby มีลำดับความสำคัญในการเรียกบางสิ่งและให้เรียกใช้งานตัวเองทันทีด้วยพารามิเตอร์ที่กำหนด... โอ้ เดี๋ยวก่อน! มันคือ Proc#call
Proccall
เรียกใช้บล็อกโดยตั้งค่าพารามิเตอร์ของบล็อกเป็นค่าในพารามิเตอร์โดยใช้สิ่งที่ใกล้เคียงกับวิธีการเรียกความหมาย ส่งคืนค่าของนิพจน์สุดท้ายที่ประเมินในบล็อกaproc = Proc.new {|scalar, values| values.map {|value| valuescalar } } aproc.call(9, 1, 2, 3) #=> [9, 18, 27] aproc[9, 1, 2, 3] #=> [9, 18, 27] aproc.(9, 1, 2, 3) #=> [9, 18, 27] aproc.yield(9, 1, 2, 3) #=> [9, 18, 27]
เอกสาร
หากสิ่งนี้ทำให้คุณสับสน ให้ฉันอธิบาย proc
สามารถ call
-ed เพื่อดำเนินการเองด้วยพารามิเตอร์ที่กำหนด ซึ่งหมายความว่าหาก TweetCreator
เป็น proc
เราสามารถเรียกมันด้วย TweetCreator.call(message)
และผลลัพธ์จะเทียบเท่ากับ TweetCreator.new(params[:message]).call
ซึ่งดูค่อนข้างคล้ายกับ TweetCreator.new(params[:message]).send_tweet
รุ่นเก่าของเรา TweetCreator.new(params[:message]).send_tweet
มาทำให้วัตถุบริการของเรามีพฤติกรรมเหมือน proc
กันเถอะ!
อันดับแรก เนื่องจากเราอาจต้องการใช้พฤติกรรมนี้ซ้ำกับออบเจ็กต์บริการทั้งหมดของเรา มายืมจาก Rails Way และสร้างคลาสที่เรียกว่า ApplicationService
:
# app/services/application_service.rb class ApplicationService def self.call(*args, &block) new(*args, &block).call end end
คุณเห็นสิ่งที่ฉันทำที่นั่นไหม ฉันได้เพิ่มเมธอดของคลาสที่เรียกว่า call
ซึ่งสร้างอินสแตนซ์ใหม่ของคลาสด้วยอาร์กิวเมนต์หรือบล็อกที่คุณส่งผ่าน และเรียกใช้การ call
บนอินสแตนซ์ ตรงตามที่เราต้องการ! สิ่งสุดท้ายที่ต้องทำคือการเปลี่ยนชื่อเมธอดจากคลาส TweetCreator
เป็น call
และให้คลาสสืบทอดจาก ApplicationService
:
# app/services/tweet_creator.rb class TweetCreator < ApplicationService attr_reader :message def initialize(message) @message = message end def call client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['TWITTER_CONSUMER_KEY'] config.consumer_secret = ENV['TWITTER_CONSUMER_SECRET'] config.access_token = ENV['TWITTER_ACCESS_TOKEN'] config.access_token_secret = ENV['TWITTER_ACCESS_SECRET'] end client.update(@message) end end
และสุดท้าย เรามาปิดท้ายกันด้วยการเรียกวัตถุบริการของเราในตัวควบคุม:
class TweetController < ApplicationController def create TweetCreator.call(params[:message]) end end
การจัดกลุ่มออบเจ็กต์บริการที่คล้ายกันเพื่อความมีสติ
ตัวอย่างข้างต้นมีวัตถุบริการเพียงชิ้นเดียว แต่ในโลกแห่งความเป็นจริง สิ่งต่าง ๆ อาจซับซ้อนกว่านั้น ตัวอย่างเช่น จะเกิดอะไรขึ้นถ้าคุณมีบริการหลายร้อยรายการ และครึ่งหนึ่งเกี่ยวข้องกับการดำเนินธุรกิจ เช่น การมีบริการ Follower
ที่ติดตามบัญชี Twitter อื่น พูดตามตรง ฉันจะเป็นบ้าตายถ้าโฟลเดอร์หนึ่งมีไฟล์ที่ดูไม่ซ้ำกัน 200 ไฟล์ ยังดีที่มีรูปแบบอื่นจาก Rails Way ที่เราสามารถคัดลอกได้—ฉันหมายถึง ใช้เป็นแรงบันดาลใจ: เนมสเปซ
สมมติว่าเราได้รับมอบหมายให้สร้างวัตถุบริการที่ติดตามโปรไฟล์ Twitter อื่นๆ
ลองดูชื่อของวัตถุบริการก่อนหน้าของเรา: TweetCreator
ดูเหมือนว่าบุคคลหรืออย่างน้อยที่สุดก็มีบทบาทในองค์กร คนที่สร้างทวีต ฉันชอบตั้งชื่อออบเจ็กต์บริการของฉันราวกับว่ามันเป็นเพียงแค่: บทบาทในองค์กร ตามแบบแผนนี้ ฉันจะเรียกวัตถุใหม่ของฉัน: ProfileFollower
ตอนนี้ เนื่องจากฉันเป็นผู้ปกครองสูงสุดของแอปนี้ ฉันจะสร้างตำแหน่งผู้บริหารในลำดับชั้นบริการของฉัน และมอบหมายความรับผิดชอบสำหรับบริการทั้งสองนี้ไปยังตำแหน่งนั้น ฉันจะเรียกตำแหน่งผู้บริหารใหม่นี้ว่า TwitterManager
เนื่องจากตัวจัดการนี้ไม่ได้ทำอะไรนอกจากจัดการ มาสร้างโมดูลและซ้อนอ็อบเจ็กต์บริการของเราภายใต้โมดูลนี้ โครงสร้างโฟลเดอร์ของเราจะมีลักษณะดังนี้:
services ├── application_service.rb └── twitter_manager ├── profile_follower.rb └── tweet_creator.rb
และวัตถุบริการของเรา:
# services/twitter_manager/tweet_creator.rb module TwitterManager class TweetCreator < ApplicationService ... end end
# services/twitter_manager/profile_follower.rb module TwitterManager class ProfileFollower < ApplicationService ... end end
และการโทรของเราจะกลายเป็น TwitterManager::TweetCreator.call(arg)
และ TwitterManager::ProfileManager.call(arg)

Service Objects เพื่อจัดการการทำงานของฐานข้อมูล
ตัวอย่างข้างต้นทำการเรียก API แต่วัตถุบริการยังสามารถใช้ได้เมื่อมีการเรียกทั้งหมดไปยังฐานข้อมูลของคุณแทนที่จะเป็น API ซึ่งจะเป็นประโยชน์อย่างยิ่งหากการดำเนินการทางธุรกิจบางอย่างต้องการการอัปเดตฐานข้อมูลหลายรายการในธุรกรรม ตัวอย่างเช่น รหัสตัวอย่างนี้จะใช้บริการเพื่อบันทึกการแลกเปลี่ยนสกุลเงินที่เกิดขึ้น
module MoneyManager # exchange currency from one amount to another class CurrencyExchanger < ApplicationService ... def call ActiveRecord::Base.transaction do # transfer the original currency to the exchange's account outgoing_tx = CurrencyTransferrer.call( from: the_user_account, to: the_exchange_account, amount: the_amount, currency: original_currency ) # get the exchange rate rate = ExchangeRateGetter.call( from: original_currency, to: new_currency ) # transfer the new currency back to the user's account incoming_tx = CurrencyTransferrer.call( from: the_exchange_account, to: the_user_account, amount: the_amount * rate, currency: new_currency ) # record the exchange happening ExchangeRecorder.call( outgoing_tx: outgoing_tx, incoming_tx: incoming_tx ) end end end # record the transfer of money from one account to another in money_accounts class CurrencyTransferrer < ApplicationService ... end # record an exchange event in the money_exchanges table class ExchangeRecorder < ApplicationService ... end # get the exchange rate from an API class ExchangeRateGetter < ApplicationService ... end end
ฉันจะส่งคืนอะไรจากวัตถุบริการของฉัน
เราได้พูดถึงวิธีการ call
วัตถุบริการของเราแล้ว แต่วัตถุควรส่งคืนอะไร มีสามวิธีในการเข้าถึงสิ่งนี้:
- คืน
true
หรือfalse
- ส่งคืนค่า
- คืน Enum
คืน true
หรือ false
อันนี้ง่าย: หากการกระทำทำงานตามที่ตั้งใจไว้ ให้คืนค่า true
; มิฉะนั้นให้คืนค่า false
:
def call ... return true if client.update(@message) false end
คืนมูลค่า
หากวัตถุบริการของคุณดึงข้อมูลจากที่ใดที่หนึ่ง คุณอาจต้องการคืนค่านั้น:
def call ... return false unless exchange_rate exchange_rate end
ตอบกลับด้วย Enum
หากออบเจ็กต์บริการของคุณซับซ้อนกว่านี้เล็กน้อย และคุณต้องการจัดการกับสถานการณ์ที่แตกต่างกัน คุณสามารถเพิ่ม enums เพื่อควบคุมการไหลของบริการของคุณ:
class ExchangeRecorder < ApplicationService RETURNS = [ SUCCESS = :success, FAILURE = :failure, PARTIAL_SUCCESS = :partial_success ] def call foo = do_something return SUCCESS if foo.success? return FAILURE if foo.failure? PARTIAL_SUCCESS end private def do_something end end
จากนั้นในแอปของคุณ คุณสามารถใช้:
case ExchangeRecorder.call when ExchangeRecorder::SUCCESS foo when ExchangeRecorder::FAILURE bar when ExchangeRecorder::PARTIAL_SUCCESS baz end
ฉันไม่ควรใส่ Service Objects ใน lib/services
แทน app/services
หรือไม่
นี้เป็นอัตนัย ความคิดเห็นของผู้คนแตกต่างกันว่าจะวางวัตถุบริการไว้ที่ไหน บางคนใส่ไว้ใน lib/services
ในขณะที่บางคนสร้าง app/services
ตกอยู่ค่ายหลัง. คู่มือเริ่มต้นใช้งานของ Rails อธิบายโฟลเดอร์ lib/
เป็นที่สำหรับวาง “โมดูลเพิ่มเติมสำหรับแอปพลิเคชันของคุณ”
ในความเห็นที่ต่ำต้อยของฉัน "โมดูลแบบขยาย" หมายถึงโมดูลที่ไม่ห่อหุ้มตรรกะของโดเมนหลักและโดยทั่วไปสามารถใช้ในโครงการต่างๆ ในคำพูดที่ชาญฉลาดของคำตอบ Stack Overflow แบบสุ่ม ให้ใส่โค้ดในนั้นที่ "สามารถกลายเป็นอัญมณีของตัวเองได้"
Service Objects เป็นแนวคิดที่ดีหรือไม่?
ขึ้นอยู่กับกรณีการใช้งานของคุณ ฟังนะ ความจริงที่ว่าคุณกำลังอ่านบทความนี้อยู่ในขณะนี้ บ่งบอกว่าคุณกำลังพยายามเขียนโค้ดที่ไม่ได้อยู่ในโมเดลหรือคอนโทรลเลอร์ ฉันเพิ่งอ่านบทความนี้เกี่ยวกับวิธีที่ออบเจ็กต์บริการเป็นรูปแบบการต่อต้าน ผู้เขียนมีความคิดเห็นของเขา แต่ฉันไม่เห็นด้วยด้วยความเคารพ
เพียงเพราะบุคคลอื่นใช้วัตถุบริการมากเกินไปไม่ได้หมายความว่าสิ่งเหล่านั้นไม่ดีโดยเนื้อแท้ ในการเริ่มต้นของฉัน Nazdeeq เราใช้ออบเจ็กต์บริการและโมเดลที่ไม่ใช่ ActiveRecord แต่ความแตกต่างระหว่างสิ่งที่ปรากฏแก่ฉันคือ: ฉันเก็บ การดำเนินการทางธุรกิจ ทั้งหมดไว้ในออบเจกต์บริการ ในขณะที่ยังคงรักษาทรัพยากรที่ไม่ต้องการความคงอยู่จริง ๆ ในโมเดลที่ไม่ใช่ ActiveRecord ท้ายที่สุดแล้ว คุณเป็นผู้ตัดสินใจว่ารูปแบบใดที่เหมาะกับคุณ
อย่างไรก็ตาม ฉันคิดว่าวัตถุที่ให้บริการโดยทั่วไปเป็นความคิดที่ดีหรือไม่ อย่างแน่นอน! พวกเขาจัดระเบียบโค้ดของฉันอย่างเป็นระเบียบ และสิ่งที่ทำให้ฉันมั่นใจในการใช้ PORO ก็คือ Ruby ชอบวัตถุ ไม่จริง รูบี้ ชอบ สิ่งของ มันบ้า บ้ามาก แต่ฉันชอบมัน! กรณีตรงประเด็น:
> 5.is_a? Object # => true > 5.class # => Integer > class Integer ?> def woot ?> 'woot woot' ?> end ?> end # => :woot > 5.woot # => "woot woot"
ดู? 5
เป็นวัตถุอย่างแท้จริง
ในหลายภาษา ตัวเลขและประเภทดั้งเดิมอื่นๆ ไม่ใช่วัตถุ Ruby ติดตามอิทธิพลของภาษา Smalltalk โดยให้วิธีการและตัวแปรอินสแตนซ์ทุกประเภท สิ่งนี้ทำให้การใช้ Ruby เป็นเรื่องง่าย เนื่องจากกฎที่ใช้กับวัตถุมีผลกับ Ruby ทั้งหมด Ruby-lang.org
เมื่อใดที่ฉันไม่ควรใช้วัตถุบริการ
อันนี้ง่าย ฉันมีกฎเหล่านี้:
- รหัสของคุณจัดการกับการกำหนดเส้นทาง พารามิเตอร์ หรือทำสิ่งอื่นๆ ของตัวควบคุมหรือไม่
ถ้าใช่ อย่าใช้วัตถุบริการ เพราะรหัสของคุณอยู่ในตัวควบคุม - คุณกำลังพยายามแบ่งปันรหัสของคุณในตัวควบคุมอื่นหรือไม่?
ในกรณีนี้ อย่าใช้วัตถุบริการ—ใช้ข้อกังวล - รหัสของคุณเป็นเหมือนโมเดลที่ไม่ต้องการความคงอยู่หรือไม่?
ถ้าเป็นเช่นนั้น อย่าใช้วัตถุบริการ ใช้โมเดลที่ไม่ใช่ ActiveRecord แทน - รหัสของคุณเป็นการดำเนินการทางธุรกิจที่เฉพาะเจาะจงหรือไม่? (เช่น "นำถังขยะออกไป" "สร้าง PDF โดยใช้ข้อความนี้" หรือ "คำนวณภาษีศุลกากรโดยใช้กฎที่ซับซ้อนเหล่านี้")
ในกรณีนี้ ให้ใช้วัตถุบริการ รหัสนั้นอาจไม่เหมาะสมกับคอนโทรลเลอร์หรือรุ่นของคุณ
แน่นอนว่านี่เป็นกฎ ของฉัน ดังนั้นคุณสามารถปรับเปลี่ยนให้เข้ากับกรณีการใช้งานของคุณเองได้ สิ่งเหล่านี้ทำงานได้ดีสำหรับฉัน แต่ระยะของคุณอาจแตกต่างกันไป
กฎการเขียน Good Service Objects
ฉันมีกฎสี่ข้อสำหรับการสร้างวัตถุบริการ สิ่งเหล่านี้ไม่ได้เขียนด้วยหิน และถ้าคุณต้องการทำลายมัน จริงๆ คุณก็ทำได้ แต่ฉันอาจจะขอให้คุณเปลี่ยนมันในการตรวจสอบโค้ด เว้นแต่ว่าเหตุผลของคุณจะดี
กฎข้อที่ 1: วิธีสาธารณะเพียงหนึ่งวิธีต่อออบเจ็กต์บริการ
ออบเจ็กต์บริการเป็นการดำเนินการทางธุรกิจ เดียว คุณสามารถเปลี่ยนชื่อวิธีการสาธารณะของคุณได้หากต้องการ ฉันชอบใช้ call
แต่ codebase ของ Gitlab CE เรียกมันว่า execute
และคนอื่นอาจใช้ perform
ใช้อะไรก็ได้ที่คุณต้องการ—คุณสามารถเรียกมันว่า nermin
สำหรับทุกอย่างที่ฉันใส่ใจ อย่าสร้างวิธีการสาธารณะสองวิธีสำหรับวัตถุบริการเดียว แบ่งมันออกเป็นสองวัตถุถ้าคุณต้องการ
กฎข้อที่ 2: ตั้งชื่อวัตถุบริการเหมือนบทบาทใบ้ที่บริษัท
ออบเจ็กต์บริการเป็นการดำเนินการ ทางธุรกิจ เดียว ลองนึกภาพถ้าคุณจ้างคนหนึ่งที่บริษัทเพื่อทำงานนั้น คุณจะเรียกพวกเขาว่าอะไร? หากงานของพวกเขาคือการสร้างทวีต ให้เรียกพวกเขาว่า TweetCreator
หากงานของพวกเขาคืออ่านทวีตที่เฉพาะเจาะจง ให้เรียกพวกเขาว่า TweetReader
กฎข้อที่ 3: อย่าสร้างวัตถุทั่วไปเพื่อดำเนินการหลายอย่าง
ออบเจ็กต์บริการ เป็นการดำเนินการ ทางธุรกิจเดียว ฉันแบ่งการทำงานออกเป็นสองส่วน: TweetReader
และ ProfileFollower
สิ่งที่ฉันไม่ได้ทำคือสร้างอ็อบเจ็กต์ทั่วไปชื่อ TwitterHandler
และถ่ายโอนฟังก์ชัน API ทั้งหมดในนั้นทิ้ง กรุณาอย่าทำเช่นนี้ สิ่งนี้ขัดกับความคิด "การดำเนินการทางธุรกิจ" และทำให้วัตถุบริการดูเหมือน Twitter Fairy หากคุณต้องการแบ่งปันรหัสระหว่างวัตถุทางธุรกิจ เพียงแค่สร้างวัตถุหรือโมดูล BaseTwitterManager
และผสมผสานสิ่งนั้นเข้ากับวัตถุบริการของคุณ
กฎข้อที่ 4: จัดการข้อยกเว้นภายในวัตถุบริการ
เป็นครั้งที่สิบหก: อ อบเจ็กต์บริการเป็นการดำเนินการทางธุรกิจเดียว ฉันไม่สามารถพูดได้เพียงพอ หากคุณมีคนที่อ่านทวีต พวกเขาจะให้ทวีตกับคุณหรือพูดว่า “ทวีตนี้ไม่มีอยู่จริง” ในทำนองเดียวกัน อย่าปล่อยให้วัตถุบริการตื่นตระหนก กระโดดขึ้นไปบนโต๊ะทำงานของตัวควบคุม และบอกให้หยุดการทำงานทั้งหมดเพราะ “เกิดข้อผิดพลาด!” เพียงคืนค่า false
และปล่อยให้ตัวควบคุมดำเนินการต่อไปจากที่นั่น
เครดิตและขั้นตอนต่อไป
บทความนี้จะเกิดขึ้นไม่ได้หากไม่มีชุมชนนักพัฒนา Ruby ที่ Toptal ถ้าฉันพบปัญหา ชุมชนเป็นกลุ่มวิศวกรที่มีความสามารถที่เป็นประโยชน์มากที่สุดที่ฉันเคยพบ
หากคุณกำลังใช้ออบเจกต์บริการ คุณอาจพบว่าตัวเองสงสัยว่าจะบังคับคำตอบบางอย่างขณะทำการทดสอบได้อย่างไร ฉันแนะนำให้อ่านบทความนี้เกี่ยวกับวิธีการสร้างวัตถุบริการจำลองใน Rspec ที่จะส่งคืนผลลัพธ์ที่คุณต้องการเสมอโดยไม่ต้องกดวัตถุบริการ!
หากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับกลเม็ดของ Ruby ฉันขอแนะนำให้สร้าง Ruby DSL: คู่มือการเขียนโปรแกรมเมตาขั้นสูงโดยเพื่อน Toptaler Mate Solymosi เขาแยกแยะว่าไฟล์ routes.rb
ไม่รู้สึกเหมือน Ruby และช่วยคุณสร้าง DSL ของคุณเอง