เมื่อลิงปะวิธีอินสแตนซ์คุณสามารถเรียกวิธีการแทนที่จากการใช้งานใหม่ได้หรือไม่?


444

สมมติว่าฉันกำลังแก้ไขวิธีการลิงในชั้นเรียนฉันจะเรียกวิธีการแทนที่จากวิธีการเอาชนะได้อย่างไร นั่นคือบางสิ่งบางอย่างเช่นsuper

เช่น

class Foo
  def bar()
    "Hello"
  end
end 

class Foo
  def bar()
    super() + " World"
  end
end

>> Foo.new.bar == "Hello World"

คลาส Foo แรกไม่ควรเป็นอื่นและ Foo ที่สองสืบทอดมาจากมันหรือ
Draco Ater

1
ไม่ฉันกำลังทำการปะของลิง ฉันหวังว่าจะมีบางอย่างเหมือน super () ฉันสามารถใช้เพื่อเรียกวิธีการดั้งเดิม
James Hollingworth

1
นี้เป็นสิ่งจำเป็นเมื่อคุณไม่ได้ควบคุมการสร้างFoo และFoo::barการใช้งานของ ดังนั้นคุณต้องแก้ไขวิธีการลิง
Halil Özgür

คำตอบ:


1166

แก้ไข : เป็นเวลา 9 ปีแล้วตั้งแต่แรกที่ฉันเขียนคำตอบนี้และสมควรได้รับการผ่าตัดเพื่อความงามเพื่อให้เป็นปัจจุบัน

คุณสามารถดูรุ่นสุดท้ายก่อนที่จะแก้ไขที่นี่


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

หลีกเลี่ยงการปะลิง

มรดก

ดังนั้นถ้าเป็นไปได้คุณควรจะชอบสิ่งนี้:

class Foo
  def bar
    'Hello'
  end
end 

class ExtendedFoo < Foo
  def bar
    super + ' World'
  end
end

ExtendedFoo.new.bar # => 'Hello World'

วิธีนี้ใช้ได้ผลถ้าคุณควบคุมการสร้างFooวัตถุ เพียงแค่เปลี่ยนสถานที่ซึ่งจะสร้างทุกแทนที่จะสร้างFoo ExtendedFooวิธีนี้จะทำงานได้ดียิ่งขึ้นถ้าคุณใช้รูปแบบการออกแบบการพึ่งพา , รูปแบบการออกแบบวิธีการของโรงงาน , รูปแบบการออกแบบโรงงานนามธรรมหรือบางสิ่งบางอย่างตามเส้นเหล่านั้นเพราะในกรณีนี้มีเพียงสถานที่ที่คุณต้องการเปลี่ยน

คณะผู้แทน

หากคุณไม่ได้ควบคุมการสร้างFooวัตถุเช่นเพราะพวกเขาถูกสร้างขึ้นโดยกรอบที่อยู่นอกการควบคุมของคุณ (เช่นตัวอย่าง) จากนั้นคุณสามารถใช้รูปแบบการออกแบบแรปเปอร์ :

require 'delegate'

class Foo
  def bar
    'Hello'
  end
end 

class WrappedFoo < DelegateClass(Foo)
  def initialize(wrapped_foo)
    super
  end

  def bar
    super + ' World'
  end
end

foo = Foo.new # this is not actually in your code, it comes from somewhere else

wrapped_foo = WrappedFoo.new(foo) # this is under your control

wrapped_foo.bar # => 'Hello World'

โดยทั่วไปที่ขอบเขตของระบบที่ได้Fooวัตถุเข้ามาในรหัสของคุณคุณห่อมันเป็นวัตถุอื่นแล้วใช้ว่าวัตถุแทนของเดิมทุกที่อื่นในรหัสของคุณ

นี่ใช้Object#DelegateClassเมธอดตัวช่วยจากdelegateไลบรารีใน stdlib

“ สะอาด” การปะลิง

Module#prepend: การผสม Mixin

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

Module#prependถูกเพิ่มเพื่อรองรับกรณีการใช้งานนี้มากหรือน้อย Module#prependทำสิ่งเดียวกันModule#includeยกเว้นยกเว้นผสมใน mixin ใต้ชั้นเรียนโดยตรง:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  prepend FooExtensions
end

Foo.new.bar # => 'Hello World'

หมายเหตุ: ฉันยังเขียนเล็กน้อยเกี่ยวกับModule#prependในคำถามนี้: โมดูลทับทิม prepend vs derivation

มรดก Mixin (แตก)

ฉันเคยเห็นบางคนลอง (และถามว่าทำไมมันไม่ทำงานที่นี่ใน StackOverflow) บางอย่างเช่นincludeไอเอ็นจีมิกซ์อินแทนที่จะเป็นprependไอเอ็นจี:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end

น่าเสียดายที่มันใช้ไม่ได้ superมันเป็นความคิดที่ดีเพราะใช้มรดกซึ่งหมายความว่าคุณสามารถใช้ อย่างไรก็ตามModule#includeแทรก mixin เหนือคลาสในลำดับชั้นการสืบทอดซึ่งหมายความว่าFooExtensions#barจะไม่ถูกเรียก (และถ้ามันถูกเรียกมันsuperจะไม่อ้างถึงจริง ๆFoo#barแต่แทนที่จะObject#barไม่มีอยู่) เนื่องจากFoo#barจะพบก่อนเสมอ

วิธีการห่อ

คำถามใหญ่คือ: เราจะยึดมั่นในbarวิธีการโดยไม่ต้องรักษาวิธีการจริง ? คำตอบอยู่ที่บ่อยครั้งในการเขียนโปรแกรมเชิงฟังก์ชัน เราได้รับวิธีการเป็นวัตถุจริงและเราใช้การปิด (เช่นบล็อก) เพื่อให้แน่ใจว่าเราและเราเท่านั้นที่ยึดมั่นกับวัตถุนั้น:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  old_bar = instance_method(:bar)

  define_method(:bar) do
    old_bar.bind(self).() + ' World'
  end
end

Foo.new.bar # => 'Hello World'

มันสะอาดมาก: เนื่องจากold_barเป็นเพียงตัวแปรท้องถิ่นมันจะออกไปนอกขอบเขตที่ส่วนท้ายของคลาสและไม่สามารถเข้าถึงได้จากทุกที่แม้แต่ใช้การสะท้อน! และเนื่องจากModule#define_methodใช้บล็อกและบล็อกที่อยู่ใกล้กับสภาพแวดล้อมคำศัพท์รอบตัว (ซึ่งเป็นสาเหตุที่เราใช้define_methodแทนที่defนี่) มัน (และมีเพียง ) ที่จะยังคงสามารถเข้าถึงold_barได้แม้ว่าจะออกไปนอกขอบเขตแล้ว

คำอธิบายสั้น ๆ :

old_bar = instance_method(:bar)

ที่นี่เราจะห่อbarวิธีการเป็นวัตถุวิธีการและกำหนดให้กับตัวแปรท้องถิ่นUnboundMethod old_barซึ่งหมายความว่าตอนนี้เรามีวิธีการที่จะยึดมั่นต่อไปbarแม้หลังจากที่มันถูกเขียนทับ

old_bar.bind(self)

นี่เป็นเรื่องยุ่งยากเล็กน้อย โดยทั่วไปใน Ruby (และในภาษา OO ที่ใช้การจัดส่งแบบเดี่ยว) วิธีการหนึ่งจะผูกกับวัตถุตัวรับเฉพาะที่เรียกว่าselfใน Ruby กล่าวอีกนัยหนึ่ง: วิธีการมักจะรู้ว่าสิ่งที่มันถูกเรียกมันก็รู้ว่ามันselfคืออะไร แต่เราคว้าวิธีโดยตรงจากชั้นเรียนรู้ได้อย่างไรว่ามันselfคืออะไร?

ดีมันไม่ได้ซึ่งเป็นเหตุผลที่เราต้องbindของเราUnboundMethodไปยังวัตถุที่เป็นครั้งแรกซึ่งจะกลับมาเป็นMethodวัตถุที่เรานั้นสามารถเรียก ( UnboundMethodไม่สามารถเรียกได้เพราะพวกเขาไม่รู้ว่าจะทำอย่างไรโดยไม่รู้selfตัว)

แล้วเราจะทำอย่างไรbindต่อ เราเพียง แต่bindมันกับตัวเองด้วยวิธีการที่จะทำงานตรงเหมือนเดิมbarจะมี!

สุดท้ายเราต้องเรียกว่าถูกส่งกลับจากMethod bindใน Ruby 1.9 มีไวยากรณ์ใหม่ที่ดีสำหรับ ( .()) แต่ถ้าคุณใช้ 1.8 คุณสามารถใช้callวิธีได้ นั่นคือสิ่งที่.()ได้รับการแปลแล้ว

ต่อไปนี้เป็นคำถามอื่นสองสามข้อที่แนวคิดเหล่านี้อธิบายไว้บางส่วน:

การปะของลิง“ สกปรก”

alias_method เชื่อมต่อ

ปัญหาที่เราประสบกับการปะแก้ลิงของเราคือเมื่อเราเขียนทับวิธีวิธีนั้นหายไปดังนั้นเราจึงไม่สามารถเรียกมันได้อีกต่อไป ดังนั้นขอทำสำเนาสำรอง!

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  alias_method :old_bar, :bar

  def bar
    old_bar + ' World'
  end
end

Foo.new.bar # => 'Hello World'
Foo.new.old_bar # => 'Hello'

ปัญหาของสิ่งนี้คือตอนนี้เราได้ทำให้เนมสเปซสกปรกด้วยold_barวิธีฟุ่มเฟือย วิธีนี้จะแสดงในเอกสารของเรามันจะแสดงในการเติมโค้ดใน IDE ของเรามันจะปรากฏขึ้นในระหว่างการไตร่ตรอง นอกจากนี้มันยังสามารถถูกเรียกได้ แต่สันนิษฐานว่าเราลิงปะติดมันเพราะเราไม่ชอบพฤติกรรมของมันในตอนแรกดังนั้นเราอาจไม่ต้องการให้คนอื่นเรียกมัน

แม้จะมีความจริงที่ว่านี้มีคุณสมบัติที่ไม่พึงประสงค์บางอย่างมันได้กลายเป็นโชคร้ายที่นิยมผ่าน Module#alias_method_chainAciveSupport

กัน: การปรับแต่ง

ในกรณีที่คุณต้องการพฤติกรรมที่แตกต่างกันในบางสถานที่เท่านั้นและไม่สามารถใช้ได้ทั่วทั้งระบบคุณสามารถใช้การปรับแต่งเพื่อ จำกัด การปะของลิงให้อยู่ในขอบเขตที่เฉพาะเจาะจง ฉันจะสาธิตที่นี่โดยใช้Module#prependตัวอย่างจากด้านบน:

class Foo
  def bar
    'Hello'
  end
end 

module ExtendedFoo
  module FooExtensions
    def bar
      super + ' World'
    end
  end

  refine Foo do
    prepend FooExtensions
  end
end

Foo.new.bar # => 'Hello'
# We haven’t activated our Refinement yet!

using ExtendedFoo
# Activate our Refinement

Foo.new.bar # => 'Hello World'
# There it is!

คุณสามารถดูตัวอย่างที่ซับซ้อนยิ่งขึ้นของการใช้การปรับแต่งในคำถามนี้: วิธีเปิดใช้งานการปะของลิงสำหรับวิธีการเฉพาะได้อย่างไร


ความคิดที่ถูกทิ้งร้าง

ก่อนที่ชุมชน Ruby จะตัดสินModule#prependมีหลายแนวคิดที่ลอยอยู่รอบ ๆ ซึ่งคุณอาจเห็นการอ้างอิงในการสนทนาที่เก่ากว่า Module#prependทั้งหมดเหล่านี้จะวิทย

ผู้ประสานวิธีการ

แนวคิดหนึ่งคือแนวคิดของ combinators วิธีการจาก CLOS นี่เป็นรุ่นย่อยที่มีน้ำหนักเบามากของการเขียนโปรแกรม Aspect-Oriented

ใช้ไวยากรณ์เช่น

class Foo
  def bar:before
    # will always run before bar, when bar is called
  end

  def bar:after
    # will always run after bar, when bar is called
    # may or may not be able to access and/or change bar’s return value
  end
end

คุณจะสามารถ“ เข้าหา” การดำเนินการของbarวิธีการได้

อย่างไรก็ตามยังไม่ชัดเจนว่าคุณจะเข้าถึงbarค่าตอบแทนภายในbar:afterได้อย่างไรและอย่างไร บางทีเรา (ab) ใช้superคำหลัก?

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar:after
    super + ' World'
  end
end

การแทนที่

คอมprependบิเนเตอร์ก่อนหน้านั้นเทียบเท่ากับมิกซ์อินด้วยวิธีการเอาชนะที่เรียกได้superที่ส่วนท้ายสุดของวิธี ในทำนองเดียวกันหลังจาก Combinator เทียบเท่ากับprependไอเอ็นจี mixin กับวิธีการเอาชนะว่าสายsuperที่มากจุดเริ่มต้นของวิธีการ

นอกจากนี้คุณยังสามารถทำสิ่งต่าง ๆ ก่อนและหลังการโทรsuperคุณสามารถโทรได้superหลายครั้งและทั้งดึงและจัดการsuperค่าส่งคืนของทำให้prependมีประสิทธิภาพมากกว่า combinators วิธี

class Foo
  def bar:before
    # will always run before bar, when bar is called
  end
end

# is the same as

module BarBefore
  def bar
    # will always run before bar, when bar is called
    super
  end
end

class Foo
  prepend BarBefore
end

และ

class Foo
  def bar:after
    # will always run after bar, when bar is called
    # may or may not be able to access and/or change bar’s return value
  end
end

# is the same as

class BarAfter
  def bar
    original_return_value = super
    # will always run after bar, when bar is called
    # has access to and can change bar’s return value
  end
end

class Foo
  prepend BarAfter
end

old คำสำคัญ

แนวคิดนี้เพิ่มคำหลักใหม่ที่คล้ายกับsuperซึ่งช่วยให้คุณสามารถเรียกใช้วิธีการเขียนทับแบบเดียวกับที่superให้คุณเรียกวิธีการเขียนทับ :

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'

ปัญหาหลักของเรื่องนี้คือมันย้อนกลับไม่เข้ากัน: ถ้าคุณมีวิธีการที่เรียกว่าoldคุณจะไม่สามารถเรียกมันได้อีกต่อไป!

การแทนที่

superในวิธีการเอาชนะในprependmixin ed เป็นหลักเช่นเดียวกับoldในข้อเสนอนี้

redef คำสำคัญ

คล้ายกับข้างต้น แต่แทนที่จะเพิ่มคำหลักใหม่สำหรับการเรียกใช้วิธีการเขียนทับและปล่อยให้defอยู่คนเดียวเราเพิ่มคำหลักใหม่สำหรับวิธีการกำหนดใหม่ สิ่งนี้เข้ากันได้ย้อนหลังเนื่องจากไวยากรณ์ปัจจุบันผิดกฎหมายอยู่แล้ว:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

Foo.new.bar # => 'Hello World'

แทนที่จะเพิ่มคำหลักใหม่สองคำเราสามารถนิยามความหมายsuperภายในใหม่ได้redef:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

Foo.new.bar # => 'Hello World'

การแทนที่

redefการ ining method นั้นเทียบเท่ากับการแทนที่ method ในprependmixin ed superในวิธีการเอาชนะพฤติกรรมเช่นsuperหรือoldในข้อเสนอนี้


@ Jörg W Mittag เป็นวิธีการห่อที่ปลอดภัยหรือไม่? จะเกิดอะไรขึ้นเมื่อเธรดที่เกิดขึ้นพร้อมกันสองตัวเรียกbindใช้old_methodตัวแปรเดียวกัน
Harish Shetty

1
@KandadaBoggu: ฉันกำลังพยายามหาว่าคุณหมายถึงอะไรโดย :-) อย่างไรก็ตามฉันค่อนข้างมั่นใจว่ามันไม่ได้เป็นเธรดที่ปลอดภัยน้อยไปกว่า metaprogramming ประเภทอื่น ๆ ใน Ruby โดยเฉพาะอย่างยิ่งการเรียกทุกครั้งUnboundMethod#bindจะส่งคืนสิ่งที่แตกต่างMethodดังนั้นฉันไม่เห็นความขัดแย้งใด ๆ ที่เกิดขึ้นไม่ว่าคุณจะเรียกมันสองครั้งติดต่อกันหรือสองครั้งในเวลาเดียวกันจากกระทู้ที่ต่างกัน
Jörg W Mittag

1
กำลังมองหาคำอธิบายเกี่ยวกับการปะแบบนี้มาตั้งแต่ฉันเริ่มจากทับทิมและราง คำตอบที่ดี! สิ่งเดียวที่ขาดหายไปสำหรับฉันคือโน้ตบน class_eval กับการเปิดคลาสอีกครั้ง นี่คือ: stackoverflow.com/a/10304721/188462
Eugene

1
Ruby 2.0 มีการปรับแต่งblog.wyeworks.com/2012/8/3/ruby-refinements-landed-in-trunk
NARKOZ

5
คุณจะหาที่ไหนoldและ redef2.0.0 ของฉันไม่มีพวกเขา อามันเป็นเรื่องยากที่จะไม่พลาดแนวคิดการแข่งขันอื่น ๆ ที่ไม่ได้ทำให้เป็นทับทิม ได้แก่ :
Nakilon

12

ดูวิธีการ aliasing นี่คือการเปลี่ยนชื่อเมธอดเป็นชื่อใหม่

สำหรับข้อมูลเพิ่มเติมและจุดเริ่มต้นดูที่บทความวิธีการเปลี่ยน (โดยเฉพาะส่วนแรก) ทับทิมเอกสาร API , นอกจากนี้ยังมี (ซับซ้อนน้อยกว่า) ตัวอย่างเช่น


-1

คลาสที่จะทำการแทนที่จะต้องถูกโหลดใหม่หลังจากคลาสที่มีวิธีดั้งเดิมดังนั้นrequireในไฟล์ที่จะทำการแทนที่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.