Monkey Patch เป็นเทคนิคหนึ่งที่เพื่อนๆหลายคนคงเคยใช้อยู่แล้ว เทคนิคนี้แม้จะมีประโยชน์แต่การใช้งานจริงบางครั้งก็มีโทษเช่นกัน
สารบัญ
Monkey Patch คืออะไร
ภาษาโปรแกรมสมัยใหม่มักมีไวยากรณ์ที่เอื้อต่อการอ่านเพื่อทำความเข้าใจได้มากขึ้น สำหรับภาษา Ruby ที่มองทุกอย่างเป็นอ็อบเจ็กต์ เราจึงสามารถเรียกใช้เมธอดผ่านตัวเลขได้เช่นกัน
3.times { |i| puts i }
เหตุที่ตัวเลขเป็นอ็อบเจ็กต์ที่มีเมธอดชื่อ times
เราจึงสามารถเรียกใช้งานเมธอดดังกล่าวได้โดยตรง รูปแบบโปรแกรมข้างต้นเป็นไปอย่างชัดเจน นั่นคือเป็นการออกคำสั่งเพื่อวนลูป 3 ครั้งเพื่อแสดงผลตัวเลข i
นั่นเอง
โปรแกรมดังกล่าวนอกจากใช้เพื่อวนลูปได้แบบเดียวกับ for
แล้ว สิ่งที่โดดเด่นกว่าคือเป็นคำสั่งที่อ่านแล้วเข้าใจทันที หากเราต้องการให้ JavaScript มีเมธอด times
บนตัวเลขบ้างเราสามารถทำได้ไหม?
(3).times(i => console.log)
เราสามารถทำได้ครับ เราทราบแล้วว่าตัวเลขของเรามาจาก Number
หากเราต้องการให้ตัวเลขของเรามีเมธอด times
เราก็แค่เพิ่มเมธอดดังกล่าวให้กับ prototype
ของ Number
ดังนี้
Number.prototype.times = function(fn) {
for(let i = 0; i < this; i++) {
fn(i)
}
};
(3).times(i => console.log)
// ผลลัพธ์เป็น
// 0
// 1
// 2
วิธีการขยายความสามารถของโค๊ดอื่นที่เรามีอยู่แล้วเช่นนี้เรียกว่า Monkey Patching นั่นเอง
Monkey Patch คือเทคนิคของการโปรแกรมเพื่อสร้างส่วนของโปรแกรมไว้ขยายหรือเปลี่ยนแปลงการทำงานของโค้ดอื่นในช่วยงRuntime
Monkey Patch คือ Bad Practice
Monkey Patch เป็นเทคนิคช่วยขยายขีดความสามารถของโค้ดเก่าก็จริง แต่ต้องไม่ลืมครับว่าเรากำลังเพิ่มหรือแก้ไขความสามารถนั้นบนชนิดข้อมูลหลักในภาษาของเรา
โปรแกรมข้างต้นเป็นการเพิ่ม times
ให้กับตัวเลข ความหมายคือทุกครั้งที่เราเรียกใช้ตัวเลข เมธอดที่เรานิยามขึ้นนี้ก็จะตามไปหลอกหลอนทุกที่ กรณีนี้อาจยังไม่เลวร้ายเท่าการแก้ไขเมธอดที่มีอยู่เดิม
เป็นที่ทราบกันดีว่ากรณีที่เราต้องการรวมอาร์เรย์ให้เป็นข้อความ เราสามารถทำได้ผ่านเมธอด join
ของอาร์เรย์ หากเราไม่ระบุอาร์กิวเมนต์ใดๆ จะถือว่าการรวมนั้นไม่มีตัวคั่น
[1, 2, 3].join() // 123
หากเราต้องการเปลี่ยนพฤติกรรมเสียใหม่ โดยกำหนดให้ค่าเริ่มต้นของการ join
คือการรวมแต่ละอีลีเมนต์ในอาร์เรย์เข้าด้วยกันโดยใช้ตัวคั่นเป็นเครื่องหมาย - เราสามารถเขียนทับ join
ตัวเก่าได้ดังนี้
Array.prototype.join = function(separator = '-') {
return this.reduce((result, item) => result + separator + item)
}
[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
เราสามารถทำได้ดังนี้
class String
# เพิ่มเมธอด start_with ไปยังคลาส String
def start_with(prefix)
prefix + self
end
end
puts 'World'.start_with('Hello ') // Hello World
แน่นอนว่าการเพิ่มเมธอดเข้าไปโดยตรงใน String ย่อมกระทบวงกว้าง ทุกส่วนของโปรแกรมที่ใช้งานคลาส String ย่อมมองเห็นเมธอดดังกล่าวหมด เพื่อเป็นการจำกัดพื้นที่ใช้งานเราจึงต้องอาศัย Refinement
module StringRefinement
refine String do
def start_with(prefix)
prefix + self
end
end
end
class MyApp
# Monkey Patch ที่เราทำกับ String
# จะมองเห็นได้แค่ภายในคลาสนี้
# พ้นขอบเขตคลาสนี้แล้ว จะไม่สามารถใช้งานได้
using StringRefinement
def say_hello
puts 'World'.start_with('Hello ')
end
end
app = MyApp.new
app.say_hello # Hello World
# พ้นขอบเขตคลาส ไม่สามารถใช้งานเมธอดดังกล่าวได้
'World'.start_with('Hello ') # undefined method `start_with' for "World":String
แล้วถ้าตัวภาษาจำกัดขอบเขตการใช้งานไม่ได้ละ?
สิ่งที่นำเสนอไปในหัวข้อก่อนหน้านี้มีใช้งานในภาษา Ruby สำหรับภาษาอื่นเช่น JavaScript เราไม่มีฟีเจอร์นี้ เมื่อเป็นเช่นนี้เราควรจำกัดขอบเขตอย่างไรดี?
คำตอบของปัญหานี้ง่ายมากครับ ให้นึกถึงไลบรารี่อย่าง Lodash
_.head([1, 2, 3]) // 1
กรณีของ Lodash นั้น เราเพิ่ม Utility Functions ต่างๆเข้าไปภายใต้ _
แน่นอนครับหากเราไม่เริ่มต้นด้วย _
แล้ว เราย่อมไม่สามารถเรียกใช้งานเมธอดต่างๆเหล่านั้นได้ นี่คือวิธีการจำกัดขอบเขตของเราให้เมธอดต่างๆอยู่ภายใต้อ็อบเจ็กต์หนึ่งๆเท่านั้นนั่นเอง
เพื่อทำเมธอด times
ของเราให้ดีขึ้น เราจึงย้ายจากการทำ Monkey Patch โดยตรงไปที่ Number ให้เป็นเพียงเมธอดหนึ่งของอ็อบเจ็กต์ _
เท่านั้น ดังนี้
const _ = {
times(n, fn) {
for(let i = 0; i < n; i++) {
fn(i)
}
}
}
_.times(3, console.log)
สรุป
Monkey Patch เอื้อประโยชน์ต่อการเขียนโปรแกรมมากมาย แต่นั่นหละครับความสามารถที่ยิ่งใหญ่ย่อมมาพร้อมกับความรับผิดชอบที่ใหญ่ยิ่ง เพื่อนๆจึงควรใช้ Monkey Patch ด้วยความตระหนักรู้ถึงพิษภัยที่แอบแฝงในขนมหวานด้วยเช่นกัน