สร้างส่วนประกอบ 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 จะมีลักษณะดังนี้:
สิ่งที่ฉันต้องการบรรลุคือการเพิ่มสิ่งที่เป็นนามธรรมมากขึ้นโดยใช้วัตถุทับทิมแบบเก่า (PORO) และสร้างรูปแบบดังต่อไปนี้สำหรับการดำเนินการ create/update
:
และรายการต่อไปนี้สำหรับการดำเนินการ list/show
:
การเพิ่ม 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 อื่นๆ แยกข้อกังวลอย่างไร ทำให้การทดสอบง่ายขึ้น และช่วยสร้างโค้ดที่สะอาดและบำรุงรักษาได้