[Code Refactoring#2] จงหลีกเลี่ยงการใช้ Monkey Patch!

Nuttavut Thongjor

Monkey Patch เป็นเทคนิคหนึ่งที่เพื่อนๆหลายคนคงเคยใช้อยู่แล้ว เทคนิคนี้แม้จะมีประโยชน์แต่การใช้งานจริงบางครั้งก็มีโทษเช่นกัน

Monkey Patch คืออะไร

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

Ruby
13.times { |i| puts i }

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

โปรแกรมดังกล่าวนอกจากใช้เพื่อวนลูปได้แบบเดียวกับ for แล้ว สิ่งที่โดดเด่นกว่าคือเป็นคำสั่งที่อ่านแล้วเข้าใจทันที หากเราต้องการให้ JavaScript มีเมธอด times บนตัวเลขบ้างเราสามารถทำได้ไหม?

JavaScript
1;(3).times((i) => console.log)

เราสามารถทำได้ครับ เราทราบแล้วว่าตัวเลขของเรามาจาก Number หากเราต้องการให้ตัวเลขของเรามีเมธอด times เราก็แค่เพิ่มเมธอดดังกล่าวให้กับ prototype ของ Number ดังนี้

JavaScript
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// 0
9// 1
10// 2

วิธีการขยายความสามารถของโค๊ดอื่นที่เรามีอยู่แล้วเช่นนี้เรียกว่า Monkey Patching นั่นเอง

Monkey Patch คือเทคนิคของการโปรแกรมเพื่อสร้างส่วนของโปรแกรมไว้ขยายหรือเปลี่ยนแปลงการทำงานของโค้ดอื่นในช่วยงRuntime

Monkey Patch คือ Bad Practice

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

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

เป็นที่ทราบกันดีว่ากรณีที่เราต้องการรวมอาร์เรย์ให้เป็นข้อความ เราสามารถทำได้ผ่านเมธอด join ของอาร์เรย์ หากเราไม่ระบุอาร์กิวเมนต์ใดๆ จะถือว่าการรวมนั้นไม่มีตัวคั่น

JavaScript
1;[1, 2, 3].join() // 123

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

JavaScript
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 เราสามารถทำได้ดังนี้

Ruby
1class String
2 # เพิ่มเมธอด start_with ไปยังคลาส String
3 def start_with(prefix)
4 prefix + self
5 end
6end
7
8puts 'World'.start_with('Hello ') // Hello World

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

Ruby
1module StringRefinement
2 refine String do
3 def start_with(prefix)
4 prefix + self
5 end
6 end
7end
8
9class MyApp
10 # Monkey Patch ที่เราทำกับ String
11 # จะมองเห็นได้แค่ภายในคลาสนี้
12 # พ้นขอบเขตคลาสนี้แล้ว จะไม่สามารถใช้งานได้
13 using StringRefinement
14
15 def say_hello
16 puts 'World'.start_with('Hello ')
17 end
18end
19
20app = MyApp.new
21app.say_hello # Hello World
22
23# พ้นขอบเขตคลาส ไม่สามารถใช้งานเมธอดดังกล่าวได้
24'World'.start_with('Hello ') # undefined method `start_with' for "World":String

แล้วถ้าตัวภาษาจำกัดขอบเขตการใช้งานไม่ได้ละ?

สิ่งที่นำเสนอไปในหัวข้อก่อนหน้านี้มีใช้งานในภาษา Ruby สำหรับภาษาอื่นเช่น JavaScript เราไม่มีฟีเจอร์นี้ เมื่อเป็นเช่นนี้เราควรจำกัดขอบเขตอย่างไรดี?

คำตอบของปัญหานี้ง่ายมากครับ ให้นึกถึงไลบรารี่อย่าง Lodash

JavaScript
1_.head([1, 2, 3]) // 1

กรณีของ Lodash นั้น เราเพิ่ม Utility Functions ต่างๆเข้าไปภายใต้ _ แน่นอนครับหากเราไม่เริ่มต้นด้วย _ แล้ว เราย่อมไม่สามารถเรียกใช้งานเมธอดต่างๆเหล่านั้นได้ นี่คือวิธีการจำกัดขอบเขตของเราให้เมธอดต่างๆอยู่ภายใต้อ็อบเจ็กต์หนึ่งๆเท่านั้นนั่นเอง

เพื่อทำเมธอด times ของเราให้ดีขึ้น เราจึงย้ายจากการทำ Monkey Patch โดยตรงไปที่ Number ให้เป็นเพียงเมธอดหนึ่งของอ็อบเจ็กต์ _ เท่านั้น ดังนี้

JavaScript
1const _ = {
2 times(n, fn) {
3 for (let i = 0; i < n; i++) {
4 fn(i)
5 }
6 },
7}
8
9_.times(3, console.log)

สรุป

Monkey Patch เอื้อประโยชน์ต่อการเขียนโปรแกรมมากมาย แต่นั่นหละครับความสามารถที่ยิ่งใหญ่ย่อมมาพร้อมกับความรับผิดชอบที่ใหญ่ยิ่ง เพื่อนๆจึงควรใช้ Monkey Patch ด้วยความตระหนักรู้ถึงพิษภัยที่แอบแฝงในขนมหวานด้วยเช่นกัน

สารบัญ

สารบัญ

  • Monkey Patch คืออะไร
  • Monkey Patch คือ Bad Practice
  • ทางรอดด้วย Refinement
  • แล้วถ้าตัวภาษาจำกัดขอบเขตการใช้งานไม่ได้ละ?
  • สรุป