สร้างส่วนประกอบ Rails ที่ทันสมัยด้วย Ruby Objects แบบธรรมดา

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

เว็บไซต์ของคุณกำลังได้รับความนิยม และคุณกำลังเติบโตอย่างรวดเร็ว Ruby/Rails เป็นภาษาโปรแกรมที่คุณเลือก ทีมของคุณใหญ่ขึ้น และคุณได้เลิกใช้ "โมเดลอ้วน ตัวควบคุมแบบบาง" ในรูปแบบการออกแบบสำหรับแอป Rails ของคุณ อย่างไรก็ตาม คุณยังคงไม่อยากเลิกใช้ Rails

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

แอปของคุณคุ้มค่าที่จะรีแฟคเตอร์ไหม

มาเริ่มด้วยการดูว่าคุณควรตัดสินใจว่าแอปของคุณเหมาะสมสำหรับการปรับโครงสร้างใหม่หรือไม่

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

  • การทดสอบหน่วยช้า การทดสอบหน่วย PORO มักจะทำงานได้อย่างรวดเร็วด้วยโค้ดที่แยกออกมาอย่างดี ดังนั้นการทดสอบที่ทำงานช้ามักจะเป็นตัวบ่งชี้ถึงการออกแบบที่ไม่ดีและความรับผิดชอบที่มากเกินไป
  • รุ่น FAT หรือตัวควบคุม โมเดลหรือตัวควบคุมที่มีโค้ด (LOC) มากกว่า 200 บรรทัดเป็นตัวเลือกที่ดีสำหรับการปรับโครงสร้างใหม่
  • ฐานรหัสขนาดใหญ่เกินไป หากคุณมี ERB/HTML/HAML ที่มี LOC หรือ Ruby มากกว่า 30,000 ซอร์สโค้ด (ไม่มี GEM ) ที่มี LOC มากกว่า 50,000 ตัว มีโอกาสดีที่คุณควรปรับโครงสร้างใหม่

ลองใช้สิ่งนี้เพื่อดูว่าคุณมีซอร์สโค้ด Ruby กี่บรรทัด:

find app -iname "*.rb" -type f -exec cat {} \;| wc -l

คำสั่งนี้จะค้นหาไฟล์ทั้งหมดที่มีนามสกุล .rb (ไฟล์ ruby) ในโฟลเดอร์ /app และพิมพ์จำนวนบรรทัด โปรดทราบว่าตัวเลขนี้เป็นตัวเลขโดยประมาณเท่านั้น เนื่องจากบรรทัดความคิดเห็นจะรวมอยู่ในผลรวมเหล่านี้

อีกทางเลือกหนึ่งที่แม่นยำและให้ข้อมูลมากขึ้นคือการใช้ stats งาน Rails rake ซึ่งแสดงผลสรุปสั้นๆ ของโค้ด จำนวนคลาส จำนวนเมธอด อัตราส่วนของเมธอดต่อคลาส และอัตราส่วนของบรรทัดของโค้ดต่อเมธอด:

 bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
  • ฉันสามารถแยกรูปแบบที่เกิดซ้ำใน codebase ของฉันได้หรือไม่

การแยกส่วนในการดำเนินการ

เริ่มจากตัวอย่างในโลกแห่งความเป็นจริงกันก่อน

แกล้งทำเป็นว่าเราต้องการเขียนแอปพลิเคชันที่ติดตามเวลาสำหรับนักวิ่ง ที่หน้าหลัก ผู้ใช้สามารถดูเวลาที่เข้าได้

การป้อนแต่ละครั้งจะมีวันที่ ระยะทาง ระยะเวลา และข้อมูล "สถานะ" ที่เกี่ยวข้องเพิ่มเติม (เช่น สภาพอากาศ ประเภทของภูมิประเทศ ฯลฯ) และความเร็วเฉลี่ยที่สามารถคำนวณได้เมื่อจำเป็น

เราต้องการหน้ารายงานที่แสดงความเร็วและระยะทางเฉลี่ยต่อสัปดาห์

หากความเร็วเฉลี่ยของรายการสูงกว่าความเร็วเฉลี่ยโดยรวม เราจะแจ้งผู้ใช้ด้วย SMS (สำหรับตัวอย่างนี้ เราจะใช้ Nexmo RESTful API เพื่อส่ง SMS)

หน้าแรกจะช่วยให้คุณสามารถเลือกระยะทาง วันที่ และเวลาที่ใช้ในการวิ่งจ็อกกิ้งเพื่อสร้างรายการที่คล้ายกันนี้:

เรายังมีหน้า statistics ซึ่งโดยทั่วไปแล้วจะเป็นรายงานประจำสัปดาห์ที่รวมความเร็วและระยะทางเฉลี่ยที่ครอบคลุมต่อสัปดาห์

  • คุณสามารถดูตัวอย่างออนไลน์ได้ที่นี่

รหัส

โครงสร้างของไดเร็กทอรี app มีลักษณะดังนี้:

 ⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb

ฉันจะไม่พูดถึงโมเดล User เพราะมันไม่มีอะไรพิเศษเพราะเราใช้มันกับ Devise เพื่อใช้งานการรับรองความถูกต้อง

สำหรับรูปแบบ Entry จะมีตรรกะทางธุรกิจสำหรับการสมัครของเรา

แต่ละ Entry เป็นของ User

เราตรวจสอบการมีอยู่ของ distance , time_period , date_time และ status แอตทริบิวต์สำหรับแต่ละรายการ

ทุกครั้งที่เราสร้างรายการ เราจะเปรียบเทียบความเร็วเฉลี่ยของผู้ใช้กับค่าเฉลี่ยของผู้ใช้รายอื่นในระบบ และแจ้งผู้ใช้ทาง SMS โดยใช้ Nexmo (เราจะไม่พูดถึงวิธีการใช้ห้องสมุด Nexmo แม้ว่าฉันต้องการ เพื่อสาธิตกรณีที่เราใช้ไลบรารีภายนอก)

  • ตัวอย่างสาระสำคัญ

โปรดสังเกตว่า โมเดล Entry มีมากกว่าตรรกะทางธุรกิจเพียงอย่างเดียว นอกจากนี้ยังจัดการการตรวจสอบและการเรียกกลับบางอย่าง

entries_controller.rb มีการดำเนินการ CRUD หลัก (แต่ไม่มีการอัพเดต) EntriesController#index รับรายการสำหรับผู้ใช้ปัจจุบันและจัดลำดับบันทึกตามวันที่สร้าง ในขณะที่ EntriesController#create สร้างรายการใหม่ ไม่จำเป็นต้องพูดถึงความชัดเจนและความรับผิดชอบของ EntriesController#destroy :

  • ตัวอย่างสาระสำคัญ

ในขณะที่ statistics_controller.rb รับผิดชอบในการคำนวณรายงานรายสัปดาห์ StatisticsController#index รับรายการสำหรับผู้ใช้ที่เข้าสู่ระบบและจัดกลุ่มตามสัปดาห์ โดยใช้เมธอด #group_by ที่มีอยู่ในคลาส Enumerable ใน Rails จากนั้นจึงพยายามตกแต่งผลลัพธ์โดยใช้วิธีการส่วนตัว

  • ตัวอย่างสาระสำคัญ

เราไม่ได้พูดถึงมุมมองมากนักที่นี่ เนื่องจากซอร์สโค้ดอธิบายตนเองได้

ด้านล่างนี้เป็นมุมมองสำหรับแสดงรายการสำหรับผู้ใช้ที่เข้าสู่ระบบ ( index.html.erb ) นี่คือเทมเพลตที่จะใช้เพื่อแสดงผลลัพธ์ของการดำเนินการดัชนี (เมธอด) ในตัวควบคุมรายการ:

  • ตัวอย่างสาระสำคัญ

โปรดทราบว่าเรากำลังใช้ partials render @entries เพื่อดึงโค้ดที่แชร์ออกไปในเทมเพลตบางส่วน _entry.html.erb เพื่อให้เราสามารถเก็บโค้ดของเราให้แห้งและนำกลับมาใช้ใหม่ได้:

  • ตัวอย่างสาระสำคัญ

เช่นเดียวกับ _form บางส่วน แทนที่จะใช้รหัสเดียวกันกับการกระทำ (ใหม่และแก้ไข) เราสร้างรูปแบบบางส่วนที่ใช้ซ้ำได้:

  • ตัวอย่างสาระสำคัญ

สำหรับการดูหน้ารายงานรายสัปดาห์ statistics/index.html.erb จะแสดงสถิติบางส่วนและรายงานประสิทธิภาพรายสัปดาห์ของผู้ใช้โดยการจัดกลุ่มรายการบางส่วน :

  • ตัวอย่างสาระสำคัญ

และสุดท้าย ผู้ช่วยสำหรับรายการ entries_helper.rb มีผู้ช่วยสองคน readable_time_period และ readable_speed ซึ่งจะทำให้แอตทริบิวต์อ่านง่ายยิ่งขึ้น:

  • ตัวอย่างสาระสำคัญ

ไม่มีอะไรแฟนซีเพื่อให้ห่างไกล

พวกคุณส่วนใหญ่จะโต้แย้งว่าการปรับโครงสร้างใหม่นี้ขัดกับหลักการ KISS และจะทำให้ระบบซับซ้อนขึ้น

แอปพลิเคชันนี้จำเป็นต้องมีการปรับโครงสร้างใหม่หรือไม่?

ไม่ แน่นอน แต่เราจะพิจารณาเพื่อจุดประสงค์ในการสาธิตเท่านั้น

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

วงจรชีวิต

เริ่มต้นด้วยการอธิบายโครงสร้างรูปแบบ Rails MVC

โดยปกติแล้ว จะเริ่มต้นโดยเบราว์เซอร์ส่งคำขอ เช่น https://www.toptal.com/jogging/show/1

เว็บเซิร์ฟเวอร์ได้รับคำขอและใช้ routes เพื่อค้นหา controller ที่จะใช้

ผู้ควบคุมทำหน้าที่แยกวิเคราะห์คำขอของผู้ใช้ การส่งข้อมูล คุกกี้ เซสชัน ฯลฯ จากนั้นขอให้ model รับข้อมูล

models คือคลาส Ruby ที่พูดคุยกับฐานข้อมูล จัดเก็บและตรวจสอบข้อมูล ดำเนินการตามตรรกะทางธุรกิจ และดำเนินการอื่นๆ มุมมองคือสิ่งที่ผู้ใช้เห็น: HTML, CSS, XML, Javascript, JSON

หากเราต้องการแสดงลำดับของวงจรชีวิตคำขอของ Rails จะมีลักษณะดังนี้:

Rails แยกวงจรชีวิต MVC

สิ่งที่ฉันต้องการบรรลุคือการเพิ่มสิ่งที่เป็นนามธรรมมากขึ้นโดยใช้วัตถุทับทิมแบบเก่า (PORO) และสร้างรูปแบบดังต่อไปนี้สำหรับการดำเนินการ create/update :

ไดอะแกรมรางสร้างแบบฟอร์ม

และรายการต่อไปนี้สำหรับการดำเนินการ list/show :

แบบสอบถามรายการไดอะแกรม Rails

การเพิ่ม PORO ที่เป็นนามธรรมจะทำให้มั่นใจได้ว่า SRP จะแยกความรับผิดชอบออกจากกันอย่างสมบูรณ์ ซึ่งเป็นสิ่งที่ Rails ไม่ค่อยดีนัก

แนวปฏิบัติ

เพื่อให้บรรลุการออกแบบใหม่ ฉันจะใช้หลักเกณฑ์ที่แสดงด้านล่าง แต่โปรดทราบว่านี่ไม่ใช่กฎที่คุณต้องปฏิบัติตาม T. ให้คิดว่าหลักเกณฑ์เหล่านี้เป็นแนวทางที่ยืดหยุ่นได้ซึ่งจะทำให้การจัดโครงสร้างใหม่ง่ายขึ้น

  • โมเดล ActiveRecord สามารถมีการเชื่อมโยงและค่าคงที่ แต่ไม่มีอย่างอื่น ซึ่งหมายความว่าไม่มีการเรียกกลับ (ใช้วัตถุบริการและเพิ่มการเรียกกลับที่นั่น) และไม่มีการตรวจสอบความถูกต้อง (ใช้วัตถุแบบฟอร์มเพื่อรวมการตั้งชื่อและการตรวจสอบสำหรับแบบจำลอง)
  • ให้คอนโทรลเลอร์เป็นเลเยอร์บาง ๆ และเรียกใช้อ็อบเจ็กต์ Service เสมอ บางท่านอาจถามว่าทำไมใช้คอนโทรลเลอร์เลยเพราะเราต้องการเรียกอ็อบเจ็กต์บริการเพื่อให้มีตรรกะอยู่เรื่อย ๆ? ตัวควบคุมเป็นสถานที่ที่ดีที่จะมีการกำหนดเส้นทาง HTTP การแยกวิเคราะห์พารามิเตอร์ การรับรองความถูกต้อง การเจรจาเนื้อหา การเรียกบริการหรือวัตถุตัวแก้ไขที่ถูกต้อง การตรวจจับข้อยกเว้น การจัดรูปแบบการตอบสนอง และการส่งคืนรหัสสถานะ HTTP ที่ถูกต้อง
  • บริการควรเรียกอ็อบเจ็กต์ Query และไม่ควรเก็บสถานะ ใช้เมธอดของอินสแตนซ์ ไม่ใช่เมธอดของคลาส ควรมีวิธีการสาธารณะน้อยมากเพื่อให้สอดคล้องกับ SRP
  • แบบสอบถามควรทำในวัตถุแบบสอบถาม เมธอดของอ็อบเจ็กต์ Query ควรส่งคืนอ็อบเจ็กต์ แฮช หรืออาร์เรย์ ไม่ใช่การเชื่อมโยง ActiveRecord
  • หลีกเลี่ยงการใช้ Helpers และใช้มัณฑนากรแทน ทำไม? ข้อผิดพลาดทั่วไปของผู้ช่วย Rails ก็คือพวกเขาสามารถเปลี่ยนเป็นฟังก์ชันที่ไม่ใช่ OO กองใหญ่ได้ โดยทั้งหมดจะแชร์เนมสเปซและก้าวข้ามกันและกัน แต่ที่แย่กว่านั้นมากคือไม่มีทางที่ดีที่จะใช้ความหลากหลายใดๆ กับตัวช่วย Rails — ให้การใช้งานที่แตกต่างกันสำหรับบริบทหรือประเภทที่แตกต่างกัน ตัวช่วยแบบ over-ride หรือ sub-classing ฉันคิดว่าคลาสตัวช่วย Rails โดยทั่วไปควรใช้สำหรับวิธีการยูทิลิตี้ ไม่ใช่สำหรับกรณีการใช้งานเฉพาะ เช่น การจัดรูปแบบแอตทริบิวต์ของโมเดลสำหรับตรรกะการนำเสนอทุกประเภท ให้แสงและลมพัดผ่าน
  • หลีกเลี่ยงการใช้ความกังวลและใช้มัณฑนากร/ตัวแทนแทน ทำไม? ท้ายที่สุด ความกังวลดูเหมือนจะเป็นส่วนสำคัญของ Rails และสามารถทำให้โค้ดแห้งเมื่อแชร์ในหลายรุ่น อย่างไรก็ตาม ปัญหาหลักคือข้อกังวลไม่ได้ทำให้โมเดลวัตถุมีความเหนียวแน่นมากขึ้น รหัสได้รับการจัดระเบียบที่ดีขึ้น กล่าวคือ ไม่มีการเปลี่ยนแปลงจริงกับ API ของโมเดล
  • ลองแยก Value Objects ออกจากโมเดล เพื่อให้โค้ดของคุณสะอาดขึ้นและจัดกลุ่มแอตทริบิวต์ที่เกี่ยวข้อง
  • ส่งตัวแปรอินสแตนซ์หนึ่งรายการต่อมุมมองเสมอ

การปรับโครงสร้างใหม่

ก่อนที่เราจะเริ่มต้น ฉันต้องการพูดคุยอีกเรื่องหนึ่ง เมื่อคุณเริ่มการปรับโครงสร้างใหม่ โดยปกติแล้ว คุณจะจบลงด้วยการถามตัวเอง ว่า

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

ฉันจะไม่พูดถึงเรื่องต่างๆ เช่น การย้ายลอจิกจากคอนโทรลเลอร์ไปยังโมเดล เนื่องจากฉันคิดว่าคุณกำลังทำแบบนั้นอยู่แล้ว และคุณสะดวกที่จะใช้ Rails (โดยปกติคือ Skinny Controller และ FAT model)

เพื่อให้บทความนี้กระชับ ฉันจะไม่พูดถึงการทดสอบที่นี่ แต่นั่นไม่ได้หมายความว่าคุณไม่ควรทดสอบ

ตรงกันข้าม คุณควร เริ่มต้นด้วยการทดสอบ เพื่อให้แน่ใจว่าทุกอย่างเรียบร้อยดีก่อนที่จะดำเนินการต่อไป นี่เป็นสิ่ง จำเป็น โดยเฉพาะอย่างยิ่งเมื่อทำการรีแฟคเตอร์

จากนั้น เราสามารถดำเนินการเปลี่ยนแปลงและทำให้แน่ใจว่าการทดสอบทั้งหมดผ่านสำหรับส่วนที่เกี่ยวข้องของโค้ด

การแยกวัตถุมูลค่า

ประการแรก วัตถุมูลค่าคืออะไร?

มาร์ติน ฟาวเลอร์อธิบายว่า:

Value Object เป็นอ็อบเจ็กต์ขนาดเล็ก เช่น เงินหรือออบเจ็กต์ช่วงวันที่ คุณสมบัติหลักของพวกเขาคือพวกเขาติดตามความหมายของค่ามากกว่าความหมายอ้างอิง

บางครั้ง คุณอาจพบกับสถานการณ์ที่แนวคิดสมควรได้รับสิ่งที่เป็นนามธรรม และความเท่าเทียมกันไม่ได้ขึ้นอยู่กับคุณค่า แต่ขึ้นอยู่กับเอกลักษณ์ ตัวอย่างได้แก่ Ruby's Date, URI และ Pathname การแยกไปยังอ็อบเจ็กต์ค่า (หรือโมเดลโดเมน) นั้นสะดวกมาก

รำคาญทำไม?

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

ชัยชนะที่ยิ่งใหญ่อีกอย่างหนึ่งคือการไม่เปลี่ยนรูป ความไม่เปลี่ยนรูปของวัตถุมีความสำคัญมาก เมื่อเราจัดเก็บข้อมูลบางชุด ซึ่งสามารถใช้ในออบเจ็กต์ค่าได้ ปกติฉันไม่ต้องการให้มีการจัดการข้อมูลนั้น

สิ่งนี้มีประโยชน์เมื่อใด

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

ยิ่งไปกว่านั้น ยังมีแนวทางบางอย่างที่ฉันใช้เพื่อช่วยในการตัดสินใจนั้น

หากคุณคิดว่ากลุ่มของเมธอดมีความเกี่ยวข้องกัน กับอ็อบเจกต์ Value พวกมันจะสื่อความหมายได้มากกว่า ความหมายนี้หมายความว่าออบเจกต์ Value ควรแสดงชุดข้อมูลที่แตกต่างกัน ซึ่งนักพัฒนาโดยเฉลี่ยของคุณสามารถอนุมานได้ง่ายๆ โดยดูที่ชื่อของออบเจกต์

วิธีนี้ทำอย่างไร?

วัตถุมูลค่าควรเป็นไปตามกฎพื้นฐานบางประการ:

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

ในตัวอย่างของเรา ฉันจะสร้างออบเจ็กต์ค่า EntryStatus เป็นแอ็ตทริบิวต์ Entry#status_weather และ Entry#status_landform ที่เป็นนามธรรมสำหรับคลาสของตนเอง ซึ่งมีลักษณะดังนี้:

  • ตัวอย่างสาระสำคัญ

หมายเหตุ: นี่เป็นเพียง Plain Old Ruby Object (PORO) ที่ไม่สืบทอดจาก ActiveRecord::Base เราได้กำหนดวิธีการอ่านสำหรับแอตทริบิวต์ของเราและกำหนดไว้เมื่อเริ่มต้น นอกจากนี้เรายังใช้มิกซ์อินที่เปรียบเทียบได้เพื่อเปรียบเทียบอ็อบเจ็กต์โดยใช้เมธอด (<=>)

เราสามารถแก้ไข Entry model เพื่อใช้ value object ที่เราสร้างขึ้น:

  • ตัวอย่างสาระสำคัญ

นอกจากนี้เรายังสามารถแก้ไขวิธีการ EntryController#create เพื่อใช้วัตถุค่าใหม่ตามลำดับ:

  • ตัวอย่างสาระสำคัญ

แยกวัตถุบริการ

ดังนั้นวัตถุบริการคืออะไร?

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

ทำไม? มีประโยชน์อย่างไร?

  • ดีคัปปลิ้ง ออบเจ็กต์บริการช่วยให้คุณแยกระหว่างออบเจ็กต์ได้มากขึ้น
  • ทัศนวิสัย. ออบเจ็กต์บริการ (หากมีชื่อดี) แสดงว่าแอปพลิเคชันทำอะไร ฉันสามารถดูไดเร็กทอรีบริการเพื่อดูว่าแอปพลิเคชันมีความสามารถใดบ้าง
  • แบบจำลองการทำความสะอาดและตัวควบคุม ผู้ควบคุมเปลี่ยนคำขอ (พารามิเตอร์ เซสชัน คุกกี้) เป็นอาร์กิวเมนต์ ส่งต่อไปยังบริการและเปลี่ยนเส้นทางหรือแสดงผลตามการตอบสนองของบริการ ในขณะที่แบบจำลองจัดการกับความสัมพันธ์และความเพียรเท่านั้น การแยกโค้ดจากคอนโทรลเลอร์/โมเดลไปยังอ็อบเจ็กต์บริการจะช่วยสนับสนุน SRP และทำให้โค้ดแยกกันมากขึ้น ความรับผิดชอบของโมเดลจะเป็นการจัดการกับการเชื่อมโยงและการบันทึก/การลบเรกคอร์ดเท่านั้น ในขณะที่ออบเจกต์บริการจะมีความรับผิดชอบเดียว (SRP) สิ่งนี้นำไปสู่การออกแบบที่ดีขึ้นและการทดสอบหน่วยที่ดีขึ้น
  • แห้งและโอบรับการเปลี่ยนแปลง ฉันรักษาวัตถุบริการให้เรียบง่ายและเล็กที่สุดเท่าที่จะทำได้ ฉันเขียนออบเจ็กต์บริการกับออบเจ็กต์บริการอื่นๆ และนำกลับมาใช้ใหม่
  • ทำความสะอาดและเพิ่มความเร็วชุดทดสอบของคุณ บริการต่างๆ ทำได้ง่ายและรวดเร็วในการทดสอบ เนื่องจากเป็นวัตถุ Ruby ขนาดเล็กที่มีจุดเข้าใช้งานเพียงจุดเดียว (วิธีการเรียก) บริการที่ซับซ้อนประกอบด้วยบริการอื่นๆ ดังนั้นคุณจึงสามารถแยกการทดสอบของคุณได้อย่างง่ายดาย นอกจากนี้ การใช้อ็อบเจ็กต์บริการยังช่วยให้จำลอง/สร้างอ็อบเจ็กต์ที่เกี่ยวข้องกับ stub ได้ง่ายขึ้นโดยไม่จำเป็นต้องโหลดสภาพแวดล้อมของรางทั้งหมด
  • โทรได้จากทุกที่ ออบเจ็กต์บริการมีแนวโน้มที่จะถูกเรียกจากคอนโทรลเลอร์ เช่นเดียวกับออบเจ็กต์บริการอื่นๆ งาน DelayedJob / Rescue / Sidekiq งาน Rake คอนโซล ฯลฯ

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

คุณควรแยกวัตถุบริการเมื่อใด

ไม่มีกฎเกณฑ์ที่ยากและรวดเร็วในที่นี้เช่นกัน

โดยปกติ อ็อบเจ็กต์ Service จะดีกว่าสำหรับระบบขนาดกลางถึงขนาดใหญ่ ผู้ที่มีตรรกะเพียงพอนอกเหนือจากการดำเนินการ CRUD มาตรฐาน

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

ต่อไปนี้คือตัวบ่งชี้ว่าเมื่อใดควรใช้ออบเจ็กต์บริการ:

  • การกระทำนั้นซับซ้อน
  • การดำเนินการนี้ครอบคลุมหลายรุ่น
  • การดำเนินการโต้ตอบกับบริการภายนอก
  • การดำเนินการนี้ไม่ใช่ข้อกังวลหลักของโมเดลพื้นฐาน
  • มีหลายวิธีในการดำเนินการ

คุณควรออกแบบ Service Objects อย่างไร?

การออกแบบคลาสสำหรับออบเจกต์บริการนั้นค่อนข้างตรงไปตรงมา เนื่องจากคุณไม่จำเป็นต้องมีอัญมณีพิเศษ ไม่ต้องเรียนรู้ DSL ใหม่ และสามารถพึ่งพาทักษะการออกแบบซอฟต์แวร์ที่คุณมีอยู่แล้วไม่มากก็น้อย

ฉันมักจะใช้แนวทางและข้อตกลงต่อไปนี้เพื่อออกแบบวัตถุที่ให้บริการ:

  • อย่าเก็บสถานะของวัตถุ
  • ใช้เมธอดของอินสแตนซ์ ไม่ใช่เมธอดของคลาส
  • ควรมีวิธีการสาธารณะน้อยมาก (ควรเป็นวิธีการหนึ่งที่สนับสนุน SRP
  • เมธอดควรส่งคืนออบเจ็กต์ผลการค้นหาที่เป็นสื่อสมบูรณ์ ไม่ใช่บูลีน
  • บริการอยู่ภายใต้ไดเร็กทอรี app/services ฉันแนะนำให้คุณใช้ไดเรกทอรีย่อยสำหรับโดเมนที่มีตรรกะทางธุรกิจมาก ตัวอย่างเช่น ไฟล์ app/services/report/generate_weekly.rb จะกำหนด Report::GenerateWeekly ในขณะที่ app/services/report/publish_monthly.rb จะกำหนด Report::PublishMonthly
  • บริการเริ่มต้นด้วยกริยา (และอย่าลงท้ายด้วย Service): ApproveTransaction , SendTestNewsletter , ImportUsersFromCsv
  • บริการตอบสนองต่อวิธีการโทร ฉันพบว่าการใช้กริยาอื่นทำให้ซ้ำซากเล็กน้อย: ApproveTransaction.approve() อ่านไม่ดี นอกจากนี้ เมธอดการโทรยังเป็นเมธอดโดยพฤตินัยสำหรับอ็อบเจ็กต์แลมบ์ดา, procs และเมธอด

หากคุณดูที่ StatisticsController#index คุณจะสังเกตเห็นกลุ่มของวิธีการ ( weeks_to_date_from , weeks_to_date_to , avg_distance ฯลฯ ) ควบคู่ไปกับคอนโทรลเลอร์ ที่ไม่ดีจริงๆ พิจารณาการแตกสาขา หากคุณต้องการสร้างรายงานรายสัปดาห์นอก statistics_controller

ในกรณีของเรา มาสร้าง Report::GenerateWeekly และแยกตรรกะของรายงานออกจาก StatisticsController :

  • ตัวอย่างสาระสำคัญ

ดังนั้น StatisticsController#index จึงดูสะอาดตาขึ้น:

  • ตัวอย่างสาระสำคัญ

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

การ บ้าน: ลองใช้วัตถุ Value สำหรับรายงาน WeeklyReport แทน Struct

แยก Query Objects จาก Controllers

วัตถุแบบสอบถามคืออะไร?

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

คุณควรแยกการสืบค้น SQL/NoSQL ที่ซับซ้อนออกเป็นคลาสของตัวเอง

แต่ละออบเจ็กต์ Query มีหน้าที่ในการส่งคืนชุดผลลัพธ์ตามเกณฑ์ / กฎเกณฑ์ทางธุรกิจ

ในตัวอย่างนี้ เราไม่ได้มีการสืบค้นข้อมูลที่ซับซ้อน ดังนั้นการใช้วัตถุ Query จะไม่มีประสิทธิภาพ อย่างไรก็ตาม เพื่อจุดประสงค์ในการสาธิต ให้แยกการสืบค้นใน Report::GenerateWeekly#call และสร้าง generate_entries_query.rb :

  • ตัวอย่างสาระสำคัญ

และใน Report::GenerateWeekly#call มาแทนที่:

 def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end

กับ:

 def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end

รูปแบบอ็อบเจกต์การสืบค้นช่วยรักษาตรรกะของโมเดลของคุณที่เกี่ยวข้องกับพฤติกรรมของคลาสอย่างเคร่งครัด ในขณะที่ยังทำให้คอนโทรลเลอร์ของคุณบาง เนื่องจากไม่ใช่คลาส Ruby แบบเก่าธรรมดา ออบเจ็กต์การสืบค้นจึงไม่จำเป็นต้องสืบทอดจาก ActiveRecord::Base และไม่ควรรับผิดชอบอะไรมากไปกว่าการดำเนินการค้นหา

แยกสร้างรายการไปยังวัตถุบริการ

ตอนนี้ ให้แยกตรรกะของการสร้างรายการใหม่ไปยังวัตถุบริการใหม่ ลองใช้แบบแผนและสร้าง CreateEntry :

  • ตัวอย่างสาระสำคัญ

และตอนนี้ EntriesController#create ของเรามีดังนี้:

 def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end

ย้ายการตรวจสอบไปยังวัตถุแบบฟอร์ม

ตอนนี้ สิ่งต่างๆ เริ่มมีความน่าสนใจมากขึ้น

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

วัตถุแบบฟอร์มคือวัตถุทับทิมเก่าธรรมดา (PORO) มันรับช่วงต่อจากคอนโทรลเลอร์/อ็อบเจ็กต์บริการทุกที่ที่ต้องการพูดคุยกับฐานข้อมูล

เหตุใดจึงต้องใช้วัตถุแบบฟอร์ม

เมื่อต้องการปรับโครงสร้างแอปของคุณใหม่ คุณควรคำนึงถึงหลักการความรับผิดชอบเดียว (SRP) เสมอ

SRP ช่วยให้คุณตัดสินใจออกแบบได้ดีขึ้นเกี่ยวกับสิ่งที่ชั้นเรียนควรรับผิดชอบ

โมเดลตารางฐานข้อมูลของคุณ (โมเดล ActiveRecord ในบริบทของ Rails) ตัวอย่างเช่น แสดงถึงระเบียนฐานข้อมูลเดียวในโค้ด ดังนั้นจึงไม่มีเหตุผลที่จะต้องเกี่ยวข้องกับสิ่งที่ผู้ใช้ของคุณทำ

นี่คือที่มาของวัตถุแบบฟอร์ม

วัตถุแบบฟอร์มมีหน้าที่แสดงแบบฟอร์มในใบสมัครของคุณ ดังนั้นแต่ละอินพุตจึงสามารถถือเป็นแอตทริบิวต์ในชั้นเรียนได้ สามารถตรวจสอบว่าแอตทริบิวต์เหล่านั้นเป็นไปตามกฎการตรวจสอบ และสามารถส่งข้อมูลที่ "สะอาด" ไปยังตำแหน่งที่ต้องการได้ (เช่น โมเดลฐานข้อมูลของคุณ หรือบางทีเครื่องมือสร้างคำค้นหาของคุณ)

คุณควรใช้วัตถุแบบฟอร์มเมื่อใด

  • เมื่อคุณต้องการแยกการตรวจสอบจากโมเดล Rails
  • เมื่อส่งแบบฟอร์มเดียวสามารถอัปเดตแบบจำลองได้หลายแบบ คุณอาจต้องการสร้างวัตถุแบบฟอร์ม

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

คุณจะสร้างวัตถุแบบฟอร์มได้อย่างไร?

  • สร้างคลาส Ruby ธรรมดา
  • รวม ActiveModel::Model (ใน Rails 3 คุณต้องรวมการตั้งชื่อ การแปลง และการตรวจสอบความถูกต้องแทน)
  • เริ่มใช้คลาสแบบฟอร์มใหม่ของคุณราวกับว่ามันเป็นโมเดล ActiveRecord ปกติ ความแตกต่างที่ใหญ่ที่สุดคือการที่คุณไม่สามารถคงข้อมูลที่จัดเก็บไว้ในวัตถุนี้ได้

โปรดทราบว่าคุณสามารถใช้ Reform Gem ได้ แต่หากใช้ PORO ต่อไป เราจะสร้าง entry_form.rb ซึ่งมีลักษณะดังนี้:

  • ตัวอย่างสาระสำคัญ

และเราจะแก้ไข CreateEntry เพื่อเริ่มใช้ Form object EntryForm :

 class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end

หมายเหตุ: พวกคุณบางคนอาจจะบอกว่าไม่จำเป็นต้องเข้าถึงวัตถุแบบฟอร์มจากวัตถุบริการ และเราสามารถเรียกวัตถุแบบฟอร์มได้โดยตรงจากตัวควบคุม ซึ่งเป็นอาร์กิวเมนต์ที่ถูกต้อง อย่างไรก็ตาม ฉันต้องการให้มีโฟลว์ที่ชัดเจน และนั่นเป็นสาเหตุที่ฉันเรียกออบเจ็กต์ Form จากออบเจ็กต์ Service เสมอ

ย้ายการเรียกกลับไปยัง Service Object

ตามที่เราตกลงกันไว้ก่อนหน้านี้ เราไม่ต้องการให้แบบจำลองของเรามีการตรวจสอบความถูกต้องและการเรียกกลับ เราแยกการตรวจสอบโดยใช้วัตถุแบบฟอร์ม แต่เรายังคงใช้การเรียกกลับบางส่วน ( after_create ในโมเดล Entry compare_speed_and_notify_user )

เหตุใดเราจึงต้องการลบการโทรกลับออกจากโมเดล

นักพัฒนา Rails มักจะเริ่มสังเกตเห็นปัญหาการโทรกลับระหว่างการทดสอบ หากคุณไม่ได้ทดสอบโมเดล ActiveRecord คุณจะเริ่มสังเกตเห็นความเจ็บปวดในภายหลังเมื่อแอปพลิเคชันของคุณเติบโตขึ้น และเนื่องจากต้องใช้ตรรกะมากขึ้นในการโทรหรือหลีกเลี่ยงการโทรกลับ

after_* callbacks ใช้เป็นหลักในการบันทึกหรือคงอยู่ของอ็อบเจ็กต์

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

ในกรณีของเรา เราจะส่ง SMS ถึงผู้ใช้หลังจากที่เราบันทึกรายการ ซึ่งไม่เกี่ยวข้องกับโดเมนของรายการจริงๆ

วิธีง่ายๆ ในการแก้ปัญหาคือการย้ายการเรียกกลับไปยังวัตถุบริการที่เกี่ยวข้อง ท้ายที่สุด การส่ง SMS สำหรับผู้ใช้ปลายทางนั้นเกี่ยวข้องกับ CreateEntry Service Object และไม่เกี่ยวข้องกับตัวแบบรายการ

ในการทำเช่นนั้น เราไม่ต้องตัดทอนเมธอด compare_speed_and_notify_user อีกต่อไปในการทดสอบของเรา เราได้ทำให้เป็นเรื่องง่ายที่จะสร้างรายการโดยไม่ต้องส่ง SMS และเรากำลังติดตามการออกแบบที่ดีเชิงวัตถุโดยทำให้แน่ใจว่าชั้นเรียนของเรามีความรับผิดชอบเดียว (SRP)

ดังนั้นตอนนี้ CreateEntry ของเราจึงมีลักษณะดังนี้:

  • ตัวอย่างสาระสำคัญ

ใช้มัณฑนากรแทนผู้ช่วย

แม้ว่าเราจะสามารถใช้คอลเลกชั่น Draper ของโมเดลการดูและนักตกแต่งได้อย่างง่ายดาย แต่ฉันจะยึด PORO ไว้เพื่อบทความนี้ เช่นเดียวกับที่ฉันเคยทำมา

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

ฉันสามารถใช้ method_missing เพื่อนำไปใช้ได้ แต่ฉันจะใช้ SimpleDelegator ไลบรารีมาตรฐานของ Ruby

รหัสต่อไปนี้แสดงวิธีใช้ SimpleDelegator เพื่อใช้มัณฑนากรพื้นฐานของเรา:

 % app/decorators/base_decorator.rb require 'delegate' class BaseDecorator < SimpleDelegator def initialize(base, view_context) super(base) @object = base @view_context = view_context end private def self.decorates(name) define_method(name) do @object end end def _h @view_context end end

เหตุใดจึงใช้วิธี _h

วิธีนี้ทำหน้าที่เป็นพร็อกซีสำหรับบริบทการดู โดยค่าเริ่มต้น บริบทมุมมองคืออินสแตนซ์ของคลาสมุมมอง คลาสมุมมองเริ่มต้นคือ ActionView::Base คุณสามารถเข้าถึงตัวช่วยดูได้ดังนี้:

 _h.content_tag :div, 'my-div', class: 'my-class'

เพื่อให้สะดวกยิ่งขึ้น เราได้เพิ่มวิธีการ decorate ใน ApplicationHelper :

 module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= "#{object.class}Decorator".constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end

ตอนนี้ เราสามารถย้ายผู้ช่วย EntriesHelper ไปยังนักตกแต่งได้:

 # app/decorators/entry_decorator.rb class EntryDecorator < BaseDecorator decorates :entry def readable_time_period mins = entry.time_period return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60 Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe end def readable_speed "#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe end end

และเราสามารถใช้ readable_time_period และ readable_speed ดังนี้:

 # app/views/entries/_entry.html.erb - <td><%= readable_speed(entry) %> </td> + <td><%= decorate(entry).readable_speed %> </td>
 - <td><%= readable_time_period(entry) %></td> + <td><%= decorate(entry).readable_time_period %></td>

โครงสร้างหลังการปรับโครงสร้างใหม่

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

 app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb

บทสรุป

แม้ว่าเราจะเน้นที่ Rails ในบล็อกโพสต์นี้ แต่ RoR ไม่ได้ขึ้นอยู่กับออบเจกต์บริการที่อธิบายไว้และ PORO อื่นๆ คุณสามารถใช้วิธีนี้กับเว็บเฟรมเวิร์ก อุปกรณ์เคลื่อนที่ หรือแอปคอนโซลใดก็ได้

ด้วยการใช้ MVC เป็นสถาปัตยกรรมของเว็บแอป ทุกอย่างยังคงเชื่อมโยงกันและทำให้คุณทำงานช้าลง เนื่องจากการเปลี่ยนแปลงส่วนใหญ่มีผลกระทบต่อส่วนอื่นๆ ของแอป นอกจากนี้ยังบังคับให้คุณคิดว่าจะวางตรรกะทางธุรกิจไว้ที่ใด - ควรอยู่ในรูปแบบตัวควบคุมหรือมุมมองหรือไม่?

ด้วยการใช้ PORO อย่างง่าย เราได้ย้ายตรรกะทางธุรกิจไปยังโมเดลหรือบริการที่ไม่ได้รับการสืบทอดจาก ActiveRecord ซึ่งเป็นชัยชนะครั้งใหญ่แล้ว ไม่ต้องพูดถึงว่าเรามีโค้ดที่สะอาดกว่า ซึ่งรองรับ SRP และการทดสอบหน่วยที่เร็วขึ้น

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

ฉันหวังว่าฉันจะสาธิตว่าการใช้ Plain Old Ruby Objects และ abstractions อื่นๆ แยกข้อกังวลอย่างไร ทำให้การทดสอบง่ายขึ้น และช่วยสร้างโค้ดที่สะอาดและบำรุงรักษาได้

ที่เกี่ยวข้อง: ประโยชน์ของ Ruby on Rails คืออะไร? หลังจากสองทศวรรษของการเขียนโปรแกรม ฉันใช้ Rails