Code quality แบบใช้ความคุ้นเคย
Chakrit Likitkhajorn
Posted on March 7, 2021
หลายๆ ครั้งผมพบว่าโปรแกรมเมอร์มักจะตัดสินคุณภาพของโค้ดจากปัจจัยเพียงแค่ว่า Pattern ที่เขาใช้ตรงกับที่เราใช้มั้ย
เช่น ถ้าสมมติเราทำโค้ด MVC อาจจะมีทีมนึงกำหนดกฎไว้แบบนี้
- Controller ห้ามมี Logic
- ใน View ให้เข้าถึง Model แล้วถ้าอยากแสดงอะไรพิเศษให้เพิ่มใน Model
- Model เป็นที่รวม Logic ส่วนมาก รวมถึงการเข้าฐานข้อมูล
- ให้ใช้ Dependency Injection ในการทำให้ Unit test ได้
ทีนี้เวลาเราไปเจอโค้ดของอีกทีมที่ใช้ Pattern อีกอย่างกับที่เราคุ้นชิน เช่น
- หากต้องการแสดงอะไรพิเศษให้สร้าง Presenter หรือ ViewModel ขึ้นมา
- Controller มี Logic ในการตรวจสอบ Request ได้
- ให้ใช้ Stub ในการทำให้ Unit test ได้
เราก็อาจจะคิดว่าโค้ดนั้นห่วยเพราะมันไม่ถูกต้องตามที่เราเรียนมาหรือคุ้นชินมา
ผมมองว่าการใช้ความคุ้นชินแบบนี้เป็นตัวชี้วัดว่าโค้ดมีคุณภาพมั้ยที่ไม่ดีมากๆ โค้ดหลายตัวมีคุณภาพที่ดีโดยไม่จำเป็นต้องใช้ Design Pattern ที่เรารู้จัก ตัวเราเองก็ไม่สามารถไปอ่านหรือทำความเข้าใจทุกๆ Design Pattern บนโลกนี้ได้
ผมมีโอกาสอ่านโค้ดของ Candidate ทำงานกับ Legacy code มาเยอะ มีโอกาสเขียนโปรแกรมมาหลากหลายภาษา แต่ละภาษาแต่ละโค้ดเองก็มีทั้งสไตล์, Framework, Design decision, Best practices ที่แตกต่างกัน
ด้วยประสบการณ์แบบนี้ ผมไม่อยู่ในฐานะที่สามารถบอกได้ว่าโค้ดไหนแย่หรือดีจากเพียงแค่เพราะมันคุ้นตามั้ย แค่เปลี่ยนภาษาก็จบเห่แล้ว ทุกอย่างต้องลบทิ้งหมด อย่างเช่น
- วันที่ผมเปลี่ยนจาก Angular มาเป็น React งี้ ผมไม่สามารถบอกได้ละว่าโค้ดที่ดีต้องมี Dependency injection ใน React มันไม่เหมาะเอามากๆ
- วันที่ผมใช้เทคโนโลยี Web API กับ MVC เพื่อเขียน C# JSON API Application ก็มีความแตกต่างมากในแง่ของ Entity ที่ใช้ในระดับ API ผูกกับ Object ขนาดไหน อันนึงออกแบบบนฐานของ Coupling Entity, Response กับอีกอันคือ Decoupling เลย
- ตอนผมทำ Rails, C# MVC, Phoenix ถึงแม้จะเป็น MVC เหมือนกันแต่สไตล์ก็แตกต่างกันมาก
ผมจึงอยากแชร์ว่าปกติแล้วผมดูอย่างไรว่าโค้ดไหนดีหรือโค้ดแบบไหนแย่ โดยไม่ขึ้นกับความคุ้นตาของตัวเอง
งั้นเรามาเริ่มกันเลยดีกว่า
Definition
กฎข้อแรกที่สำคัญที่สุดของโค้ดที่ดีสำหรับผมคือ
โค้ดที่ดีสำหรับผมคือมีนิยามของคำศัพท์ที่ชัดเจน และทำตามนิยามนั้น
ผมยกตัวอย่าง Rails บอกว่า Controller
Action Controller is the C in MVC. After the router has determined which controller to use for a request, the controller is responsible for making sense of the request, and producing the appropriate output. Luckily, Action Controller does most of the groundwork for you and uses smart conventions to make this as straightforward as possible.
แปลว่า Controller มีหน้าที่จัดการ Request และจัดการ Output ถ้าเรานิยามไว้แบบนี้ แปลว่าโค้ดที่เอา Controller ไปทำอย่างอื่นคือไม่ดีละ
แปลว่าผมอ่านโค้ด Rails ผมจะไม่อยากเห็น Controller ที่รู้เรื่องธุรกิจภายในมากกว่าการจัดการ Request/Response
class TransactionController
def create
raise TooLowTransactionError if params[:amount] < 20 # บรรทัดนี้คือไม่ดี เพราะมันรู้ว่าในธุรกิจของเราห้ามสร้าง Transaction ต่ำกว่า 20 บาท
# ....
end
ซึ่งแปลว่าถ้ามีใครก็ตามกำหนด Definition ของ Controller ต่างไปจากเรา เราอาจจะเอ๊ะในครั้งแรกว่าทำไมถึงกำหนดต่างกับมาตรฐานสากลล่ะ แต่ไม่ได้แปลว่าโค้ดไม่ดีนะครับ
สมมติใครซักคนกำหนดว่า Controller มีหน้าที่ประสานงานระหว่าง Model ในกรณีที่ API นั้นต้องใช้มากกว่า 1 Model แล้วเขาเขียนตามนั้น นี่ก็คือถือว่ายังเป็นโค้ดที่ดีในแง่ของ Honest to definition อยู่ครับ
ส่วนการกำหนด Definition แบบนี้มันเหมาะหรือไม่ จะทำให้ Controller บวมเกินเหตุมั้ย อันนั้นอีกเรื่องนึง ต้องไปดูตามบริบทของระบบที่พัฒนาอีกทีนึง
หรืออย่างเช่นใน MVC แบบ Rails ตัว Model จะเข้าถึงฐานข้อมูลได้ ก็จะเขียนบอกไว้ใน Definition ของ Model แต่ถ้าเราเริ่มทำ Hexagonal Architecture เราอาจจะมี Repository ที่เข้าถึงข้อมูลอีกที ไม่ให้ Model เขาถึงฐานข้อมูลโดยตรง
ทั้งสองอย่างผมมองว่าไม่ได้มีอันไหนเหนือกว่าอันไหน ขึ้นกับบริบทของระบบ
แต่สิ่งที่ดีเสมอโดยไม่ขึ้นกับบริบท คือคุณมีนิยามที่ชัดเจนว่าแต่ละคำศัพท์แปลว่าอะไร และคนร่วม Contribute เข้าใจตรงกันไม่สับสน สามารถทดสอบได้ง่ายๆ คือ สมมติพูดถึง Controller ถ้าผมเป็นสมาชิกใหม่เข้าไปถามว่า สิ่งนี้ควรอยู่ใน Controller มั้ย ทุกคนควรจะตอบได้ตรงกัน
สิ่งที่แย่โดยโดยไม่ขึ้นกับบริบท คือ นิยามที่ว่าแต่ละคำศัพท์คืออะไรมันกำกวม คนร่วมทีมอธิบายไม่ได้ว่า Model แปลว่าอะไร Controller แปลว่าอะไร ไปถามเจ้าตัวคนเขียนคนแรกก็ตอบไม่ได้ชัดเจนว่านิยามอะไร บอกได้แค่ประมาณนั้นอ่ะประมาณนี้อ่ะ แต่ละคนในทีมตอบไม่ตรงกันซักคน
อันนี้ผมมองว่าแย่เสมอในทุกบริบทของระบบ
Consistency
ต่อเนื่องจากนิยาม ความคงเส้นคงวาก็สำคัญ
ผมขอยกย่อหน้าเก่าขึ้นมา
หรืออย่างเช่นใน MVC แบบ Rails ตัว Model จะเข้าถึงฐานข้อมูลได้ ก็จะเขียนบอกไว้ใน Definition ของ Model แต่ถ้าเราเริ่มทำ Hexagonal Architecture เราอาจจะมี Repository ที่เข้าถึงข้อมูลอีกที ไม่ให้ Model เขาถึงฐานข้อมูลโดยตรง
ถ้าสมมติเราวางนิยามไว้แบบนี้ แต่บางทีเวลาเห็น Model บวมเราก็สร้าง Repository ขึ้นมา บางทีก็ใช้ บางทีก็ไม่ใช้ ไม่มีความคงเส้นคงวา อันนี้ก็ถือว่าไม่ดี
ไม่ว่าจะทำโค้ดในภาษาโบราณแค่ไหนไปจนถึงใหม่แค่ไหน ความคงเส้นคงวาของคำศัพท์และ Layering คือดีครับ และข้อยกเว้นยิ่งมีมากคือไม่ดี
หรือสมมติถ้าเราตกลงกันแล้วว่าเราจะใช้ map
เสมอ เราก็จะใช้มันเสมอ ไม่ใช้ for loop และตรงข้าม ถ้าเราตกลงกันแล้วว่าจะใช้ for loop เสมอไม่ใช้ map
ก็ควรจะคงเส้นคงวากับสิ่งนั้น
ถ้าจะใช้ผสมกันก็ต้องบอกได้ว่าตอนไหนใช้ map
ตอนไหนใช้ loop ที่ชัดเจนเข้าใจตรงกัน
การมีข้อยกเว้นได้มั้ย ได้ ถ้า Justify the cost ได้ แต่ถ้ามีไปเฉยๆ โดยไม่มีต้นสายปลายเหตุอธิบายได้ดีว่าทำไมถึงเกิดข้อยกเว้น อันนี้ผมมองว่าแย่โดยไม่เกี่ยวกับว่าออกแบบยังไง
Appropriate coupling
โค้ดที่ดีอีกอย่างนึงคือ เราเข้าใจว่าโค้ดแต่ละบรรทัดแก้ไขแล้วมีสิทธิ์กระทบอะไรบ้าง
ผมยกตัวอย่างจากบทความที่ผมเขียนเองเรื่อง โค้ดเหมือนจะ Clean
class Invoice
def pay_invoice
make_sure_invoice_approved
create_transaction_entry
decrease_company_total_balance
record_tax_deduction
end
end
ถ้าเราเขียนแบบนี้แล้วเราไม่สามารถทำความเข้าใจได้ว่าการแก้ไข create_transaction_entry
จะกระทบกับใบเสร็จส่วนไหนบ้าง จนกว่าจะขุดลึกๆ อันนี้คือแย่
ดังนั้นการออกแบบให้ Dependency ระหว่างโค้ดแต่ละส่วนเคลียร์ เข้าใจง่าย ไม่พันกันเกินไป จนสามารถพูดได้เต็มปากว่าถ้าผมแก้โค้ดบรรทัดนี้ ส่วนนี้อาจจะกระทบ และส่วนนั้นจะไม่กระทบแน่นอน
นั่นคือคุณลักษณะของโค้ดที่ดี โดยไม่เกี่ยวกับ Design pattern ที่ใช้
Local reasoning
คุณสมบัติสุดทายที่ผมจะพูดถึงวันนี้คือ Local reasoning หรือความสามารถในการทำความเข้าใจแยกส่วนได้
ถ้าคุณเห็นโค้ดแบบนี้
def run
# ทำทุกอย่าง
end
โปรแกรมลักษณะนี้เราต้องเข้าใจทุกอย่างที่มีในระบบก่อนที่จะแก้ไขบรรทัดนึง เพราะแต่ละบรรทัดอาจจะขึ้นกับบรรทัดอื่นได้โดยที่เราไม่รู้ จึงทำให้การทำความเข้าใจแค่ส่วนที่เราสนใจทำไม่ได้
แต่ถ้าเป็นแบบนี้ล่ะ
def vat
@amount * 0.07
end
เราเร่ิมเข้าใจได้ละว่าบรรทัดนี้ขึ้นกับจำนวน @amount
ในคลาสของเราเท่านั้น โดยมั่นใจได้ 100% ว่าจะไม่มี Surprise ใดๆ ออกมาที่จะมากระทบกับสิ่งนี้แบบงงๆ ได้ นอกจาก Amount
การที่เราสามารถทำความเข้าใจโค้ดเป็นส่วนๆ ได้ก็สำคัญ และเป็นสาเหตุนึงที่โค้ดที่ดีมักจจะมีขนาดของ Function, Method ที่เล็ก (แต่ผมว่าเราเชียร์ว่าทำฟังก์ชั่นเล็กๆ มากไปหน่อย จนหลายๆ คนเชื่อว่าฟังก์ชั่นเล็กคือดี โดยไม่เข้าใจว่าดียังไง ผมตอบให้ว่าจริงๆ เราอยากได้ความสามารถในการทำ Local reasoning)
ส่งท้าย
จริงๆ แล้วมันมีปัจจัยอีกมากมายที่ผมดู แต่วันนี้ขอเสนอ 4 ข้อครับ
- Domain alignment
- Naming convention
- etc.
แต่ 4 ข้อนี้เป็นข้อพื้นฐานที่ผมคิดว่าถ้าเริ่มมองโค้ดที่ไม่คุ้นชินจากมุมนี้ เราจะเข้าใจง่ายขึ้นว่าโค้ดที่ดีเป็นแบบไหน รวมไปถึงถ้าเราไปศึกษา Framework ใหม่ โดยเข้าใจว่า Framework ที่ดีจะมีคุณสมบัติพวกนี้ เราก็จะเข้าใจและคาดเดา (Deduce) Design บางอย่างได้อย่างรวดเร็วครับ อ่านแป๊ปเดียวก็อ๋อ เขาไว้ยังงี้นี่เอง ได้เร็วขึ้นครับ
Posted on March 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.