Babel Coder

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

beginner

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 ด้วยความตระหนักรู้ถึงพิษภัยที่แอบแฝงในขนมหวานด้วยเช่นกัน


แสดงความคิดเห็นของคุณ


No any discussions