Kế thừa các phương thức lớp từ các mô-đun / mixin trong Ruby


95

Được biết, trong Ruby, các phương thức của lớp được kế thừa:

class P
  def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works

Tuy nhiên, tôi ngạc nhiên là nó không hoạt động với các mixin:

module M
  def self.mm; puts 'mixin' end
end
class N; include M end
M.mm # works
N.mm # does not work!

Tôi biết rằng phương thức #extend có thể thực hiện việc này:

module X; def mm; puts 'extender' end end
Y = Class.new.extend X
X.mm # works

Nhưng tôi đang viết một mixin (hoặc đúng hơn là muốn viết) có chứa cả phương thức phiên bản và phương thức lớp:

module Common
  def self.class_method; puts "class method here" end
  def instance_method; puts "instance method here" end
end

Bây giờ những gì tôi muốn làm là:

class A; include Common
  # custom part for A
end
class B; include Common
  # custom part for B
end

Tôi muốn A, B kế thừa cả phương thức thể hiện và lớp từ Commonmô-đun. Nhưng, tất nhiên, điều đó không hiệu quả. Vì vậy, không có một cách bí mật nào để làm cho sự kế thừa này hoạt động từ một mô-đun duy nhất?

Có vẻ như không phù hợp với tôi khi chia điều này thành hai mô-đun khác nhau, một để bao gồm, một để mở rộng. Một giải pháp khả thi khác là sử dụng một lớp Commonthay vì một mô-đun. Nhưng đây chỉ là một cách giải quyết. (Nếu có hai bộ các chức năng thông thường Common1Common2và chúng tôi thực sự cần phải có mixins?) Có bất kỳ lý do sâu tại sao phương pháp lớp thừa kế không làm việc từ mixins?



1
Với sự khác biệt, điều đó ở đây, tôi biết là có thể - tôi đang yêu cầu một cách làm ít xấu nhất và vì những lý do tại sao lựa chọn ngây thơ không hiệu quả.
Boris Stitnicky

1
Với nhiều kinh nghiệm hơn, tôi hiểu rằng Ruby sẽ đoán quá xa ý định của lập trình viên nếu việc bao gồm một mô-đun cũng thêm các phương thức mô-đun vào lớp singleton của includeer. Điều này là do "các phương thức mô-đun" trên thực tế không có gì khác ngoài các phương thức singleton. Mô-đun không đặc biệt để có các phương thức singleton, chúng đặc biệt vì là không gian tên nơi các phương thức và hằng số được định nghĩa. Không gian tên hoàn toàn không liên quan đến các phương thức singleton của một mô-đun, vì vậy trên thực tế, sự kế thừa lớp của các phương thức singleton đáng kinh ngạc hơn là việc thiếu nó trong các mô-đun.
Boris Stitnicky

Câu trả lời:


171

Một thành ngữ phổ biến là sử dụng includedcác phương thức lớp hook và insert từ đó.

module Foo
  def self.included base
    base.send :include, InstanceMethods
    base.extend ClassMethods
  end

  module InstanceMethods
    def bar1
      'bar1'
    end
  end

  module ClassMethods
    def bar2
      'bar2'
    end
  end
end

class Test
  include Foo
end

Test.new.bar1 # => "bar1"
Test.bar2 # => "bar2"

26
includethêm các phương thức thể hiện, extendthêm các phương thức lớp. Đây là cách nó hoạt động. Tôi không thấy sự mâu thuẫn, chỉ có những kỳ vọng chưa được đáp ứng :)
Sergio Tishedsev

1
Tôi đang dần hiểu ra một thực tế rằng đề xuất của bạn cũng thanh lịch như giải pháp thực tế của vấn đề này. Nhưng tôi rất muốn biết lý do tại sao thứ gì đó hoạt động với các lớp lại không hoạt động với các mô-đun.
Boris Stitnicky

6
@BorisStitnicky Tin tưởng câu trả lời này. Đây là một thành ngữ rất phổ biến trong Ruby, giải quyết chính xác trường hợp sử dụng bạn hỏi và chính xác lý do bạn gặp phải. Nó có thể trông "không nhã nhặn", nhưng đó là cách tốt nhất của bạn. (Nếu bạn làm điều này thường xuyên, bạn có thể di chuyển các includedđịnh nghĩa phương pháp để mô-đun khác và bao gồm RẰNG trong mô-đun chính của bạn;)
Phrogz

2
Đọc chủ đề này để biết thêm thông tin chi tiết về "tại sao?" .
Phrogz

2
@werkshy: bao gồm mô-đun trong một lớp giả.
Sergio Terrysev

47

Đây là câu chuyện đầy đủ, giải thích các khái niệm lập trình siêu ứng dụng cần thiết để hiểu tại sao việc bao gồm mô-đun hoạt động theo cách nó hoạt động trong Ruby.

Điều gì xảy ra khi một mô-đun được bao gồm?

Việc bao gồm một mô-đun vào một lớp sẽ thêm mô-đun vào tổ tiên của lớp. Bạn có thể xem tổ tiên của bất kỳ lớp hoặc mô-đun nào bằng cách gọi ancestorsphương thức của nó :

module M
  def foo; "foo"; end
end

class C
  include M

  def bar; "bar"; end
end

C.ancestors
#=> [C, M, Object, Kernel, BasicObject]
#       ^ look, it's right here!

Khi bạn gọi một phương thức trên một thể hiện của C, Ruby sẽ xem xét mọi mục trong danh sách tổ tiên này để tìm một phương thức thể hiện với tên đã cho. Vì chúng ta đã đưa Mvào C, Mbây giờ là tổ tiên của C, nên khi chúng ta gọi foomột phiên bản của C, Ruby sẽ tìm thấy phương thức đó trong M:

C.new.foo
#=> "foo"

Lưu ý rằng việc bao gồm không sao chép bất kỳ trường hợp hoặc phương thức lớp nào vào lớp - nó chỉ thêm một "ghi chú" vào lớp mà nó cũng sẽ tìm kiếm các phương thức thể hiện trong mô-đun được bao gồm.

Điều gì về các phương thức "lớp" trong mô-đun của chúng tôi?

Bởi vì việc bao gồm chỉ thay đổi cách các phương thức thể hiện được gửi đi, bao gồm một mô-đun vào một lớp chỉ làm cho các phương thức thể hiện của nó khả dụng trên lớp đó. Các phương thức "lớp" và các khai báo khác trong mô-đun không được tự động sao chép vào lớp:

module M
  def instance_method
    "foo"
  end

  def self.class_method
    "bar"
  end
end

class C
  include M
end

M.class_method
#=> "bar"

C.new.instance_method
#=> "foo"

C.class_method
#=> NoMethodError: undefined method `class_method' for C:Class

Ruby thực hiện các phương thức lớp như thế nào?

Trong Ruby, các lớp và mô-đun là các đối tượng thuần túy - chúng là các thể hiện của lớp ClassModule. Điều này có nghĩa là bạn có thể tự động tạo các lớp mới, gán chúng cho các biến, v.v.:

klass = Class.new do
  def foo
    "foo"
  end
end
#=> #<Class:0x2b613d0>

klass.new.foo
#=> "foo"

Cũng trong Ruby, bạn có khả năng xác định cái gọi là phương thức singleton trên các đối tượng. Các phương thức này được thêm vào dưới dạng các phương thức thể hiện mới vào lớp singleton đặc biệt, ẩn của đối tượng:

obj = Object.new

# define singleton method
def obj.foo
  "foo"
end

# here is our singleton method, on the singleton class of `obj`:
obj.singleton_class.instance_methods(false)
#=> [:foo]

Nhưng không phải các lớp và mô-đun cũng chỉ là các đối tượng đơn thuần? Trong thực tế, họ là! Điều đó có nghĩa là họ cũng có thể có các phương thức singleton? Có, nó có! Và đây là cách các phương thức lớp được sinh ra:

class Abc
end

# define singleton method
def Abc.foo
  "foo"
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

Hoặc, cách phổ biến hơn để xác định một phương thức lớp là sử dụng selftrong khối định nghĩa lớp, tham chiếu đến đối tượng lớp đang được tạo:

class Abc
  def self.foo
    "foo"
  end
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

Làm cách nào để đưa các phương thức lớp vào một mô-đun?

Như chúng ta vừa thiết lập, các phương thức lớp thực sự chỉ là các phương thức thể hiện trên lớp singleton của đối tượng lớp. Điều này có nghĩa là chúng ta chỉ có thể đưa một mô-đun vào lớp singleton để thêm một loạt các phương thức của lớp? Có, nó có!

module M
  def new_instance_method; "hi"; end

  module ClassMethods
    def new_class_method; "hello"; end
  end
end

class HostKlass
  include M
  self.singleton_class.include M::ClassMethods
end

HostKlass.new_class_method
#=> "hello"

Đây self.singleton_class.include M::ClassMethodsdòng không trông rất thoải mái, vì vậy của Ruby thêm Object#extend, mà không giống nhau - tức là bao gồm một module vào lớp singleton của đối tượng:

class HostKlass
  include M
  extend M::ClassMethods
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ there it is!

Di chuyển extendcuộc gọi vào mô-đun

Ví dụ trước này không phải là mã có cấu trúc tốt, vì hai lý do:

  1. Bây giờ chúng ta phải gọi cả hai includeextendtrong HostClassđịnh nghĩa để đưa mô-đun của chúng ta vào đúng cách. Điều này có thể rất cồng kềnh nếu bạn phải bao gồm nhiều mô-đun giống nhau.
  2. HostClasstài liệu tham khảo trực tiếp M::ClassMethods, là chi tiết triển khai của mô-đun MHostClasskhông cần biết hoặc quan tâm.

Vậy còn điều này thì sao: khi chúng ta gọi includeở dòng đầu tiên, bằng cách nào đó chúng ta thông báo cho mô-đun rằng nó đã được đưa vào, đồng thời cung cấp cho nó đối tượng lớp của chúng ta, để nó có thể gọi extendchính nó. Bằng cách này, nhiệm vụ của mô-đun là thêm các phương thức của lớp nếu nó muốn.

Đây chính xác là những gì phương pháp đặc biệtself.included dành cho. Ruby tự động gọi phương thức này bất cứ khi nào mô-đun được đưa vào một lớp (hoặc mô-đun) khác và chuyển vào đối tượng lớp chủ như đối số đầu tiên:

module M
  def new_instance_method; "hi"; end

  def self.included(base)  # `base` is `HostClass` in our case
    base.extend ClassMethods
  end

  module ClassMethods
    def new_class_method; "hello"; end
  end
end

class HostKlass
  include M

  def self.existing_class_method; "cool"; end
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ still there!

Tất nhiên, thêm các phương thức lớp không phải là điều duy nhất chúng ta có thể làm self.included. Chúng ta có đối tượng lớp, vì vậy chúng ta có thể gọi bất kỳ phương thức (lớp) nào khác trên nó:

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end

2
Câu trả lời tuyệt vời! Cuối cùng đã có thể hiểu được khái niệm sau một ngày vật lộn. Cảm ơn bạn.
Sankalp

1
Tôi nghĩ đây có thể là câu trả lời bằng văn bản hay nhất mà tôi từng thấy trên SO. Cảm ơn bạn vì sự rõ ràng đáng kinh ngạc và đã mở rộng yêu cầu của tôi về Ruby. Nếu tôi có thể tặng khoản tiền thưởng 100pt này, tôi sẽ!
Peter Nixey

7

Như Sergio đã đề cập trong phần bình luận, đối với những người đã ở trong Rails (hoặc không phiền khi phụ thuộc vào Hỗ trợ tích cực ), Concernở đây rất hữu ích:

require 'active_support/concern'

module Common
  extend ActiveSupport::Concern

  def instance_method
    puts "instance method here"
  end

  class_methods do
    def class_method
      puts "class method here"
    end
  end
end

class A
  include Common
end

3

Bạn có thể lấy bánh của mình và ăn bằng cách làm như sau:

module M
  def self.included(base)
    base.class_eval do # do anything you would do at class level
      def self.doit #class method
        @@fred = "Flintstone"
        "class method doit called"
      end # class method define
      def doit(str) #instance method
        @@common_var = "all instances"
        @instance_var = str
        "instance method doit called"
      end
      def get_them
        [@@common_var,@instance_var,@@fred]
      end
    end # class_eval
  end # included
end # module

class F; end
F.include M

F.doit  # >> "class method doit called"
a = F.new
b = F.new
a.doit("Yo") # "instance method doit called"
b.doit("Ho") # "instance method doit called"
a.get_them # >> ["all instances", "Yo", "Flintstone"]
b.get_them # >> ["all instances", "Ho", "Flintstone"]

Nếu bạn có ý định thêm các biến instance và class, bạn sẽ phải bứt tóc vì bạn sẽ gặp phải một đống mã bị hỏng trừ khi bạn làm theo cách này.


Có một số điều kỳ lạ không hoạt động khi truyền class_eval một khối, chẳng hạn như định nghĩa hằng số, xác định các lớp lồng nhau và sử dụng các biến lớp bên ngoài các phương thức. Để hỗ trợ những điều này, bạn có thể cung cấp cho class_eval một heredoc (chuỗi) thay vì một khối: base.class_eval << - 'END'
Paul Donohue
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.