Khi khỉ vá một phương thức cá thể, bạn có thể gọi phương thức bị ghi đè từ triển khai mới không?


442

Nói rằng tôi đang vá một phương thức trong một lớp, làm thế nào tôi có thể gọi phương thức được ghi đè từ phương thức ghi đè? Tức là một cái gì đó hơi giốngsuper

Ví dụ

class Foo
  def bar()
    "Hello"
  end
end 

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

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

Không phải lớp Foo đầu tiên là một số khác và Foo thứ hai thừa hưởng từ nó?
Draco Ater

1
Không, tôi đang vá khỉ. Tôi đã hy vọng sẽ có một cái gì đó giống như siêu () mà tôi có thể sử dụng để gọi phương thức ban đầu
James Hollingworth

1
Điều này là cần thiết khi bạn không kiểm soát việc tạo Foo sử dụng Foo::bar. Vì vậy, bạn phải khỉ vá phương pháp.
Halil Özgür

Câu trả lời:


1165

EDIT : Đã 9 năm kể từ khi tôi viết câu trả lời này, và nó xứng đáng được phẫu thuật thẩm mỹ để giữ cho nó hiện tại.

Bạn có thể xem phiên bản cuối cùng trước khi chỉnh sửa tại đây .


Bạn không thể gọi phương thức ghi đè bằng tên hoặc từ khóa. Đó là một trong nhiều lý do tại sao nên tránh vá khỉ và kế thừa được ưu tiên hơn, vì rõ ràng bạn có thể gọi phương thức ghi đè .

Tránh vá khỉ

Di sản

Vì vậy, nếu có thể, bạn nên thích một cái gì đó như thế này:

class Foo
  def bar
    'Hello'
  end
end 

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

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

Điều này hoạt động, nếu bạn kiểm soát việc tạo ra các Foođối tượng. Chỉ cần thay đổi mọi nơi tạo ra một Foothay vì tạo một ExtendedFoo. Điều này thậm chí còn hoạt động tốt hơn nếu bạn sử dụng Mẫu thiết kế tiêm phụ thuộc , mẫu thiết kế phương thức nhà máy , mẫu thiết kế nhà máy trừu tượng hoặc một cái gì đó dọc theo các dòng đó, bởi vì trong trường hợp đó, chỉ có nơi bạn cần thay đổi.

Phái đoàn

Nếu bạn không kiểm soát việc tạo các Foođối tượng, chẳng hạn vì chúng được tạo bởi một khung nằm ngoài tầm kiểm soát của bạn (nhưví dụ), sau đó bạn có thể sử dụng Mẫu thiết kế Wrapper :

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'

Về cơ bản, ở ranh giới của hệ thống, nơi mà các Foođối tượng đi vào mã của bạn, bạn quấn nó vào đối tượng khác, và sau đó sử dụng đối tượng thay vì một bản gốc ở khắp mọi nơi khác trong mã của bạn.

Điều này sử dụng Object#DelegateClassphương thức của trình trợ giúp từ delegatethư viện trong stdlib.

Dọn dẹp

Module#prepend: Chuẩn bị Mixin

Hai phương pháp trên yêu cầu thay đổi hệ thống để tránh việc vá khỉ. Phần này cho thấy phương pháp vá khỉ ưa thích và ít xâm lấn nhất, nên thay đổi hệ thống không phải là một lựa chọn.

Module#prependđã được thêm vào để hỗ trợ chính xác hơn hoặc ít hơn trường hợp sử dụng này. Module#prependthực hiện tương tự như Module#include, ngoại trừ nó trộn trong mixin ngay bên dưới lớp:

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'

Lưu ý: Tôi cũng đã viết một chút về Module#prependcâu hỏi này: phần trước của mô-đun Ruby so với đạo hàm

Kế thừa Mixin (bị hỏng)

Tôi đã thấy một số người dùng thử (và hỏi về lý do tại sao nó không hoạt động ở đây trên StackOverflow) một cái gì đó như thế này, tức là sử dụng includemột mixin thay vì prepending nó:

class Foo
  def bar
    'Hello'
  end
end 

module FooExtensions
  def bar
    super + ' World'
  end
end

class Foo
  include FooExtensions
end

Thật không may, điều đó sẽ không làm việc. Đó là một ý tưởng tốt, bởi vì nó sử dụng sự kế thừa, có nghĩa là bạn có thể sử dụng super. Tuy nhiên, Module#includechèn mixin phía trên lớp trong hệ thống phân cấp thừa kế, có nghĩa là FooExtensions#barsẽ không bao giờ được gọi (và nếu nó được gọi, superthì thực tế sẽ không đề cập đến Foo#barmà thay vào Object#barđó không tồn tại), vì Foo#barsẽ luôn được tìm thấy trước tiên.

Phương pháp gói

Câu hỏi lớn là: làm thế nào chúng ta có thể giữ vững barphương thức mà không thực sự giữ một phương thức thực tế ? Câu trả lời là, vì nó thường như vậy, trong lập trình chức năng. Chúng ta nắm giữ phương thức như một đối tượng thực tế và chúng ta sử dụng một bao đóng (tức là một khối) để đảm bảo rằng chúng ta và chỉ chúng ta giữ lấy đối tượng đó:

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'

Điều này rất rõ ràng: vì old_barchỉ là một biến cục bộ, nó sẽ vượt ra khỏi phạm vi ở phần cuối của lớp và không thể truy cập nó từ bất cứ đâu, ngay cả khi sử dụng phản xạ! Và vì Module#define_methodcó một khối, và các khối đóng trên môi trường từ vựng xung quanh của chúng (đó là lý do tại sao chúng ta đang sử dụng define_methodthay vì defở đây), nên nó (và chỉ có nó) sẽ vẫn có quyền truy cập old_bar, ngay cả khi nó đã vượt quá phạm vi.

Giải thích ngắn gọn:

old_bar = instance_method(:bar)

Ở đây chúng ta đang gói barphương thức vào một UnboundMethodđối tượng phương thức và gán nó cho biến cục bộ old_bar. Điều này có nghĩa, bây giờ chúng ta có một cách để giữ lấy barngay cả sau khi nó đã bị ghi đè.

old_bar.bind(self)

Đây là một chút khó khăn. Về cơ bản, trong Ruby (và gần như tất cả các ngôn ngữ OO dựa trên một lần gửi), một phương thức được liên kết với một đối tượng người nhận cụ thể, được gọi selftrong Ruby. Nói cách khác: một phương thức luôn biết nó được gọi là đối tượng nào, nó biết nó selflà gì . Nhưng, chúng tôi đã lấy phương thức trực tiếp từ một lớp, làm sao nó biết nó selflà gì?

Vâng, nó không, đó là lý do chúng ta cần bindchúng tôi UnboundMethodđến một đối tượng đầu tiên, mà sẽ trả về một Methodđối tượng mà sau đó chúng ta có thể gọi. ( UnboundMethods không thể được gọi, vì họ không biết phải làm gì mà không biết self.)

Và chúng ta bindlàm gì? Chúng tôi chỉ đơn giản là bindvới chính mình, theo cách đó nó sẽ hành xử chính xác như bản gốc barsẽ có!

Cuối cùng, chúng ta cần gọi Methodcái được trả về bind. Trong Ruby 1.9, có một số cú pháp mới tiện lợi cho điều đó ( .()), nhưng nếu bạn ở trên 1.8, bạn chỉ cần sử dụng callphương thức này; đó là những gì .()được dịch sang anyway.

Dưới đây là một vài câu hỏi khác, trong đó một số khái niệm được giải thích:

Dịch vụ vá khỉ

alias_method chuỗi

Vấn đề chúng ta gặp phải khi vá khỉ là khi chúng ta ghi đè lên phương thức, phương thức không còn nữa, vì vậy chúng ta không thể gọi nó nữa. Vì vậy, hãy tạo một bản sao lưu!

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'

Vấn đề với điều này là hiện tại chúng ta đã làm ô nhiễm không gian tên bằng một old_barphương thức không cần thiết . Phương pháp này sẽ hiển thị trong tài liệu của chúng tôi, nó sẽ hiển thị trong hoàn thành mã trong IDE của chúng tôi, nó sẽ hiển thị trong quá trình phản chiếu. Ngoài ra, nó vẫn có thể được gọi, nhưng có lẽ chúng tôi đã vá nó, vì chúng tôi không thích hành vi của nó ở nơi đầu tiên, vì vậy chúng tôi có thể không muốn người khác gọi nó.

Mặc dù thực tế rằng điều này có một số thuộc tính không mong muốn, nhưng nó không may trở nên phổ biến thông qua AciveSupport Module#alias_method_chain.

Một bên: Tinh chỉnh

Trong trường hợp bạn chỉ cần các hành vi khác nhau ở một vài nơi cụ thể chứ không phải trên toàn bộ hệ thống, bạn có thể sử dụng các Tinh chỉnh để hạn chế bản vá khỉ ở một phạm vi cụ thể. Tôi sẽ chứng minh điều đó ở đây bằng cách sử dụng Module#prependví dụ từ phía trên:

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!

Bạn có thể thấy một ví dụ phức tạp hơn về việc sử dụng các sàng lọc trong câu hỏi này: Làm thế nào để kích hoạt bản vá khỉ cho phương pháp cụ thể?


Những ý tưởng bị bỏ rơi

Trước khi cộng đồng Ruby ổn định Module#prepend, có nhiều ý tưởng khác nhau xuất hiện mà đôi khi bạn có thể thấy được tham chiếu trong các cuộc thảo luận cũ hơn. Tất cả những điều này được trợ cấp bởi Module#prepend.

Kết hợp phương pháp

Một ý tưởng là ý tưởng về các tổ hợp phương pháp từ CLOS. Về cơ bản, đây là một phiên bản rất nhẹ của một tập hợp con của Lập trình hướng theo khía cạnh.

Sử dụng cú pháp như

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

bạn sẽ có thể móc nối vào trong việc thực hiện barphương thức.

Tuy nhiên, không rõ ràng nếu và làm thế nào bạn có quyền truy cập vào bargiá trị trả về trong bar:after. Có lẽ chúng ta có thể (ab) sử dụng supertừ khóa?

class Foo
  def bar
    'Hello'
  end
end 

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

Thay thế

Bộ kết hợp trước tương đương với việc trộn prependmột mixin với một phương thức ghi đè gọi supercuối phương thức. Tương tự như vậy, bộ kết hợp sau tương đương với việc trộn prependmột mixin với một phương thức ghi đè gọi superngay từ đầu của phương thức.

Bạn cũng có thể thực hiện công cụ trước sau khi gọi super, bạn có thể gọi supernhiều lần và cả truy xuất và thao tác supergiá trị trả về, tạo ra prependsức mạnh hơn các tổ hợp phương thức.

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 từ khóa

Ý tưởng này thêm một từ khóa mới tương tự super, cho phép bạn gọi phương thức được ghi đè giống như cách supercho phép bạn gọi phương thức được ghi đè :

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  def bar
    old + ' World'
  end
end

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

Vấn đề chính với điều này là nó không tương thích ngược: nếu bạn có phương thức được gọi old, bạn sẽ không thể gọi nó nữa!

Thay thế

supertrong một phương pháp ghi đè trong một prependmixin ed về cơ bản giống như oldtrong đề xuất này.

redef từ khóa

Tương tự như trên, nhưng thay vì thêm một từ khóa mới để gọi phương thức ghi đè và để lại defmột mình, chúng tôi thêm một từ khóa mới để xác định lại các phương thức. Điều này tương thích ngược, vì dù sao cú pháp hiện tại là bất hợp pháp:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    old + ' World'
  end
end

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

Thay vì thêm hai từ khóa mới, chúng tôi cũng có thể xác định lại ý nghĩa của superbên trong redef:

class Foo
  def bar
    'Hello'
  end
end 

class Foo
  redef bar
    super + ' World'
  end
end

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

Thay thế

redefining một phương thức tương đương với ghi đè phương thức trong một prependmixin ed. supertrong phương thức ghi đè hành xử như superhoặc oldtrong đề xuất này.


@ Jorg W Mittag, phương pháp tiếp cận chủ đề gói có an toàn không? Điều gì xảy ra khi hai luồng đồng thời gọi bindtrên cùng một old_methodbiến?
Harish Shetty

1
@KandadaBoggu: Tôi đang cố gắng tìm hiểu chính xác ý của bạn là gì :-) Tuy nhiên, tôi khá chắc chắn rằng nó không kém phần an toàn so với bất kỳ loại siêu lập trình nào khác trong Ruby. Đặc biệt, mỗi cuộc gọi UnboundMethod#bindsẽ trả về một cuộc gọi mới, khác nhau Method, vì vậy, tôi không thấy bất kỳ xung đột nào phát sinh, bất kể bạn gọi nó hai lần liên tiếp hay hai lần cùng một lúc từ các luồng khác nhau.
Jörg W Mittag

1
Đã tìm kiếm một lời giải thích về việc vá như thế này kể từ khi tôi bắt đầu trên ruby ​​và đường ray. Câu trả lời chính xác! Điều duy nhất còn thiếu đối với tôi là một ghi chú về class_eval so với việc mở lại một lớp. Đây là: stackoverflow.com/a/10304721/188462
Eugene


5
Bạn tìm ở đâu oldredef? 2.0.0 của tôi không có chúng. À, thật khó để không bỏ lỡ những ý tưởng cạnh tranh khác không được đưa vào Ruby là:
Nakilon 17/07/14


-1

Lớp sẽ ghi đè phải được tải lại sau lớp có chứa phương thức ban đầu, vì vậy requirenó trong tệp sẽ ghi đè.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.