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 Foo
thay 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ưViên ngọc trên tay vịnví 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 mà đố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#DelegateClass
phương thức của trình trợ giúp từ delegate
thư viện trong stdlib.
Dọn dẹp
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#prepend
thự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#prepend
câ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 include
một mixin thay vì prepend
ing 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#include
chèn mixin phía trên lớp trong hệ thống phân cấp thừa kế, có nghĩa là FooExtensions#bar
sẽ không bao giờ được gọi (và nếu nó được gọi, super
thì thực tế sẽ không đề cập đến Foo#bar
mà thay vào Object#bar
đó không tồn tại), vì Foo#bar
sẽ 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 bar
phươ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_bar
chỉ 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_method
có 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_method
thay 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 bar
phươ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 bar
ngay 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 self
trong 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ó self
là 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ó self
là gì?
Vâng, nó không, đó là lý do chúng ta cần bind
chú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. ( UnboundMethod
s 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 bind
làm gì? Chúng tôi chỉ đơn giản là bind
với chính mình, theo cách đó nó sẽ hành xử chính xác như bản gốc bar
sẽ có!
Cuối cùng, chúng ta cần gọi Method
cá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 call
phươ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ỉ
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_bar
phươ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
.
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#prepend
ví 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 bar
phươ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 bar
giá trị trả về trong bar:after
. Có lẽ chúng ta có thể (ab) sử dụng super
từ 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 prepend
một mixin với một phương thức ghi đè gọi super
ở cuố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 prepend
một mixin với một phương thức ghi đè gọi super
ngay từ đầu của phương thức.
Bạn cũng có thể thực hiện công cụ trước và sau khi gọi super
, bạn có thể gọi super
nhiều lần và cả truy xuất và thao tác super
giá trị trả về, tạo ra prepend
sứ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
và
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 super
cho 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ế
super
trong một phương pháp ghi đè trong một prepend
mixin ed về cơ bản giống như old
trong đề 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 def
mộ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 super
bê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ế
redef
ining một phương thức tương đương với ghi đè phương thức trong một prepend
mixin ed. super
trong phương thức ghi đè hành xử như super
hoặc old
trong đề xuất này.