[Code Refactoring#2] จงหลีกเลี่ยงการใช้ Monkey Patch!
Monkey Patch เป็นเทคนิคหนึ่งที่เพื่อนๆหลายคนคงเคยใช้อยู่แล้ว เทคนิคนี้แม้จะมีประโยชน์แต่การใช้งานจริงบางครั้งก็มีโทษเช่นกัน
Monkey Patch คืออะไร
ภาษาโปรแกรมสมัยใหม่มักมีไวยากรณ์ที่เอื้อต่อการอ่านเพื่อทำความเข้าใจได้มากขึ้น สำหรับภาษา Ruby ที่มองทุกอย่างเป็นอ็อบเจ็กต์ เราจึงสามารถเรียกใช้เมธอดผ่านตัวเลขได้เช่นกัน
13.times { |i| puts i }
เหตุที่ตัวเลขเป็นอ็อบเจ็กต์ที่มีเมธอดชื่อ times
เราจึงสามารถเรียกใช้งานเมธอดดังกล่าวได้โดยตรง รูปแบบโปรแกรมข้างต้นเป็นไปอย่างชัดเจน นั่นคือเป็นการออกคำสั่งเพื่อวนลูป 3 ครั้งเพื่อแสดงผลตัวเลข i
นั่นเอง
โปรแกรมดังกล่าวนอกจากใช้เพื่อวนลูปได้แบบเดียวกับ for
แล้ว สิ่งที่โดดเด่นกว่าคือเป็นคำสั่งที่อ่านแล้วเข้าใจทันที หากเราต้องการให้ JavaScript มีเมธอด times
บนตัวเลขบ้างเราสามารถทำได้ไหม?
1;(3).times((i) => console.log)
เราสามารถทำได้ครับ เราทราบแล้วว่าตัวเลขของเรามาจาก Number
หากเราต้องการให้ตัวเลขของเรามีเมธอด times
เราก็แค่เพิ่มเมธอดดังกล่าวให้กับ prototype
ของ Number
ดังนี้
1Number.prototype.times = function (fn) {2 for (let i = 0; i < this; i++) {3 fn(i)4 }5}6;(3).times((i) => console.log)7// ผลลัพธ์เป็น8// 09// 110// 2
วิธีการขยายความสามารถของโค๊ดอื่นที่เรามีอยู่แล้วเช่นนี้เรียกว่า Monkey Patching นั่นเอง
Monkey Patch คือเทคนิคของการโปรแกรมเพื่อสร้างส่วนของโปรแกรมไว้ขยายหรือเปลี่ยนแปลงการทำงานของโค้ดอื่นในช่วยงRuntime
Monkey Patch คือ Bad Practice
Monkey Patch เป็นเทคนิคช่วยขยายขีดความสามารถของโค้ดเก่าก็จริง แต่ต้องไม่ลืมครับว่าเรากำลังเพิ่มหรือแก้ไขความสามารถนั้นบนชนิดข้อมูลหลักในภาษาของเรา
โปรแกรมข้างต้นเป็นการเพิ่ม times
ให้กับตัวเลข ความหมายคือทุกครั้งที่เราเรียกใช้ตัวเลข เมธอดที่เรานิยามขึ้นนี้ก็จะตามไปหลอกหลอนทุกที่ กรณีนี้อาจยังไม่เลวร้ายเท่าการแก้ไขเมธอดที่มีอยู่เดิม
เป็นที่ทราบกันดีว่ากรณีที่เราต้องการรวมอาร์เรย์ให้เป็นข้อความ เราสามารถทำได้ผ่านเมธอด join
ของอาร์เรย์ หากเราไม่ระบุอาร์กิวเมนต์ใดๆ จะถือว่าการรวมนั้นไม่มีตัวคั่น
1;[1, 2, 3].join() // 123
หากเราต้องการเปลี่ยนพฤติกรรมเสียใหม่ โดยกำหนดให้ค่าเริ่มต้นของการ join
คือการรวมแต่ละอีลีเมนต์ในอาร์เรย์เข้าด้วยกันโดยใช้ตัวคั่นเป็นเครื่องหมาย - เราสามารถเขียนทับ join
ตัวเก่าได้ดังนี้
1Array.prototype.join = function (separator = '-') {2 return this.reduce((result, item) => result + separator + item)3}[(1, 2, 3)].join() // 1-2-3
นี่คือฝันร้ายของเราเลยหละ อย่าลืมนะครับว่าทุกที่ในโปรเจคเราที่เราใช้ join
กับอาร์เรย์ ทุกส่วนจะได้รับผลกระทบจากการเปลี่ยนแปลงนี้ทั้งสิ้น
นอกจากนี้หากในทีมเราต่างทำ Monkey Patch โดยการเพิ่มเมธอดชื่อเดียวกันไปบนอ็อบเจ็กต์ชนิดเดียวกัน แล้วแบบนี้เราจะทราบได้อย่างไรว่าโค้ดของใครกันแน่ที่จะได้รับการทำงาน?
ทั้งหมดทั้งมวลคือการ์ดกับดักที่ Monkey Patch ได้เปิดหงายเอาไว้ รอเหยื่อแบบพวกเราตกลงไปในหลุมพลาง เมื่อเป็นเช่นนี้เราต้องระวังกันซักหน่อยแล้วเมื่อต้องการใช้ Monkey Patch
ทางรอดด้วย Refinement
ปัญหาหลักของการใช้ Monkey Patch นั่นคือส่วนอื่นของโปรแกรมที่เรียกใช้โค้ดนี้จะได้รับผลกระทบด้วยดั่งวิญญาณตามติด เมื่อเป็นเช่นนี้ทางรอดของเราจึงเป็นการจำกัดพื้นที่ เพื่อไม่ให้ผลกระทบจากการใช้ Monkey Patch กระจายไปสู่วงกว้าง
สำหรับภาษา Ruby มีฟีเจอร์ที่เรียกว่า Refinement ด้วยความสามารถนี้ทำให้เราจำกัดขอบเขตการใช้ Monkey Patch ของเราได้ หากเราต้องการเพิ่มเมธอดชื่อ start_with
ให้กับ String
เราสามารถทำได้ดังนี้
1class String2 # เพิ่มเมธอด start_with ไปยังคลาส String3 def start_with(prefix)4 prefix + self5 end6end78puts 'World'.start_with('Hello ') // Hello World
แน่นอนว่าการเพิ่มเมธอดเข้าไปโดยตรงใน String ย่อมกระทบวงกว้าง ทุกส่วนของโปรแกรมที่ใช้งานคลาส String ย่อมมองเห็นเมธอดดังกล่าวหมด เพื่อเป็นการจำกัดพื้นที่ใช้งานเราจึงต้องอาศัย Refinement
1module StringRefinement2 refine String do3 def start_with(prefix)4 prefix + self5 end6 end7end89class MyApp10 # Monkey Patch ที่เราทำกับ String11 # จะมองเห็นได้แค่ภายในคลาสนี้12 # พ้นขอบเขตคลาสนี้แล้ว จะไม่สามารถใช้งานได้13 using StringRefinement1415 def say_hello16 puts 'World'.start_with('Hello ')17 end18end1920app = MyApp.new21app.say_hello # Hello World2223# พ้นขอบเขตคลาส ไม่สามารถใช้งานเมธอดดังกล่าวได้24'World'.start_with('Hello ') # undefined method `start_with' for "World":String
แล้วถ้าตัวภาษาจำกัดขอบเขตการใช้งานไม่ได้ละ?
สิ่งที่นำเสนอไปในหัวข้อก่อนหน้านี้มีใช้งานในภาษา Ruby สำหรับภาษาอื่นเช่น JavaScript เราไม่มีฟีเจอร์นี้ เมื่อเป็นเช่นนี้เราควรจำกัดขอบเขตอย่างไรดี?
คำตอบของปัญหานี้ง่ายมากครับ ให้นึกถึงไลบรารี่อย่าง Lodash
1_.head([1, 2, 3]) // 1
กรณีของ Lodash นั้น เราเพิ่ม Utility Functions ต่างๆเข้าไปภายใต้ _
แน่นอนครับหากเราไม่เริ่มต้นด้วย _
แล้ว เราย่อมไม่สามารถเรียกใช้งานเมธอดต่างๆเหล่านั้นได้ นี่คือวิธีการจำกัดขอบเขตของเราให้เมธอดต่างๆอยู่ภายใต้อ็อบเจ็กต์หนึ่งๆเท่านั้นนั่นเอง
เพื่อทำเมธอด times
ของเราให้ดีขึ้น เราจึงย้ายจากการทำ Monkey Patch โดยตรงไปที่ Number ให้เป็นเพียงเมธอดหนึ่งของอ็อบเจ็กต์ _
เท่านั้น ดังนี้
1const _ = {2 times(n, fn) {3 for (let i = 0; i < n; i++) {4 fn(i)5 }6 },7}89_.times(3, console.log)
สรุป
Monkey Patch เอื้อประโยชน์ต่อการเขียนโปรแกรมมากมาย แต่นั่นหละครับความสามารถที่ยิ่งใหญ่ย่อมมาพร้อมกับความรับผิดชอบที่ใหญ่ยิ่ง เพื่อนๆจึงควรใช้ Monkey Patch ด้วยความตระหนักรู้ถึงพิษภัยที่แอบแฝงในขนมหวานด้วยเช่นกัน
สารบัญ
- Monkey Patch คืออะไร
- Monkey Patch คือ Bad Practice
- ทางรอดด้วย Refinement
- แล้วถ้าตัวภาษาจำกัดขอบเขตการใช้งานไม่ได้ละ?
- สรุป