การสืบทอดเมธอดคลาสจากโมดูล / มิกซ์อินใน Ruby


97

เป็นที่ทราบกันดีว่าใน Ruby เมธอดคลาสได้รับการสืบทอด:

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

อย่างไรก็ตามเป็นเรื่องที่น่าแปลกใจสำหรับฉันที่มันใช้ไม่ได้กับมิกซ์อิน:

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

ฉันรู้ว่าวิธี #extend สามารถทำได้:

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

แต่ฉันกำลังเขียน mixin (หรืออยากจะเขียน) ที่มีทั้งวิธีการอินสแตนซ์และวิธีคลาส:

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

ตอนนี้สิ่งที่ฉันต้องการจะทำคือ:

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

ฉันต้องการให้ A, B สืบทอดทั้งอินสแตนซ์และคลาสเมธอดจากCommonโมดูล แต่แน่นอนว่าไม่ได้ผล ไม่มีวิธีลับในการทำให้การสืบทอดนี้ทำงานจากโมดูลเดียวหรือไม่?

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



1
ด้วยความแตกต่างที่นี่ฉันรู้ว่ามันเป็นไปได้ - ฉันกำลังขอวิธีที่น่าเกลียดน้อยที่สุดในการทำและด้วยเหตุผลว่าทำไมตัวเลือกที่ไร้เดียงสาไม่ได้ผล
Boris Stitnicky

1
ด้วยประสบการณ์ที่มากขึ้นฉันเข้าใจว่า Ruby จะคาดเดาเจตนาของโปรแกรมเมอร์มากเกินไปหากรวมโมดูลด้วยก็เพิ่มวิธีการของโมดูลลงในคลาสซิงเกิลตันของตัวรวม เนื่องจาก "วิธีการโมดูล" ในความเป็นจริงแล้วไม่มีอะไรนอกจากวิธีการแบบซิงเกิลตัน โมดูลไม่ได้มีความพิเศษสำหรับการมีเมธอดแบบซิงเกิลมันเป็นแบบพิเศษสำหรับการเป็นเนมสเปซที่กำหนดเมธอดและค่าคงที่ เนมสเปซไม่เกี่ยวข้องกับเมธอดแบบซิงเกิลตันของโมดูลดังนั้นจริงๆแล้วการสืบทอดคลาสของเมธอดซิงเกิลตันนั้นน่าประหลาดใจมากกว่าการไม่มีในโมดูล
Boris Stitnicky

คำตอบ:


172

สำนวนทั่วไปคือการใช้includedhook และ injection class method จากที่นั่น

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
includeเพิ่มวิธีการอินสแตนซ์extendเพิ่มเมธอดคลาส นี่คือวิธีการทำงาน ฉันไม่เห็นความไม่ลงรอยกันมี แต่ความคาดหวังที่ไม่เป็นไปตามนั้น :)
Sergio Tulentsev

1
ฉันค่อยๆทนกับความจริงที่ว่าข้อเสนอแนะของคุณนั้นสวยงามพอ ๆ กับวิธีแก้ปัญหาในทางปฏิบัติของปัญหานี้ แต่ฉันขอขอบคุณที่ทราบเหตุผลว่าทำไมบางสิ่งที่ทำงานกับคลาสไม่ทำงานกับโมดูล
Boris Stitnicky

6
@BorisStitnicky เชื่อถือคำตอบนี้ นี่เป็นสำนวนที่ใช้กันทั่วไปใน Ruby ซึ่งแก้ปัญหาการใช้งานที่คุณถามได้อย่างแม่นยำและด้วยเหตุผลที่คุณประสบ อาจดู "ไม่สง่างาม" แต่เป็นทางออกที่ดีที่สุดของคุณ (หากคุณทำเช่นนี้บ่อยครั้งคุณสามารถย้ายincludedนิยามวิธีการไปยังโมดูลอื่นและรวมสิ่งนั้นไว้ในโมดูลหลักของคุณได้)
Phrogz

2
อ่านกระทู้นี้เพื่อรับข้อมูลเชิงลึกเพิ่มเติมเกี่ยวกับ"ทำไม" .
Phrogz

2
@werkshy: รวมโมดูลไว้ในคลาสจำลอง
Sergio Tulentsev

50

นี่คือเรื่องราวทั้งหมดโดยอธิบายถึงแนวคิดการเขียนโปรแกรมเมตาที่จำเป็นเพื่อทำความเข้าใจว่าเหตุใดการรวมโมดูลจึงทำงานได้เหมือนใน Ruby

จะเกิดอะไรขึ้นเมื่อรวมโมดูล

การรวมโมดูลลงในคลาสจะเป็นการเพิ่มโมดูลให้กับบรรพบุรุษของคลาส คุณสามารถดูบรรพบุรุษของคลาสหรือโมดูลใดก็ได้โดยเรียกancestorsใช้เมธอด:

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!

เมื่อคุณเรียกใช้เมธอดบนอินสแตนซ์CRuby จะดูทุกรายการของรายการบรรพบุรุษนี้เพื่อค้นหาวิธีการอินสแตนซ์ที่มีชื่อที่ระบุ เนื่องจากเรารวมMเข้าC, Mอยู่ในขณะนี้เป็นบรรพบุรุษของCดังนั้นเมื่อเราเรียกว่าfooอินสแตนซ์ของCทับทิมจะพบวิธีการที่ในM:

C.new.foo
#=> "foo"

โปรดทราบว่าการรวมไม่ได้คัดลอกอินสแตนซ์หรือเมธอดคลาสใด ๆ ไปยังคลาสแต่เพียงเพิ่ม "โน้ต" ให้กับคลาสซึ่งควรมองหาวิธีการอินสแตนซ์ในโมดูลที่รวมไว้ด้วย

แล้วเมธอด "คลาส" ในโมดูลของเราล่ะ?

เนื่องจากการรวมจะเปลี่ยนแปลงวิธีการส่งอินสแตนซ์เท่านั้นการรวมโมดูลลงในคลาสทำให้เมธอดอินสแตนซ์พร้อมใช้งานในคลาสนั้นเท่านั้น วิธีการ "คลาส" และการประกาศอื่น ๆ ในโมดูลจะไม่ถูกคัดลอกไปยังคลาสโดยอัตโนมัติ:

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 ใช้วิธีการเรียนอย่างไร

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

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

klass.new.foo
#=> "foo"

นอกจากนี้ใน Ruby คุณมีความเป็นไปได้ในการกำหนดวิธีการแบบซิงเกิลตันบนวัตถุ วิธีการเหล่านี้ถูกเพิ่มเป็นวิธีการอินสแตนซ์ใหม่ในคลาส singletonพิเศษที่ซ่อนอยู่ของอ็อบเจ็กต์:

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]

แต่คลาสและโมดูลก็ไม่ใช่แค่วัตถุธรรมดาเช่นกัน? ในความเป็นจริงพวกเขา! นั่นหมายความว่าพวกเขาสามารถมีวิธีการแบบซิงเกิลตันได้เช่นกัน? ใช่! และนี่คือวิธีการกำเนิดคลาส:

class Abc
end

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

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

หรือวิธีทั่วไปในการกำหนดเมธอดคลาสคือการใช้selfภายในบล็อกนิยามคลาสซึ่งอ้างถึงคลาสอ็อบเจ็กต์ที่กำลังสร้าง:

class Abc
  def self.foo
    "foo"
  end
end

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

ฉันจะรวมเมธอดคลาสไว้ในโมดูลได้อย่างไร

ในขณะที่เราเพิ่งสร้างเมธอดคลาสเป็นเพียงวิธีการอินสแตนซ์บนคลาสซิงเกิลตันของคลาสอ็อบเจ็กต์ หมายความว่าเราสามารถรวมโมดูลลงในคลาสซิงเกิลตันเพื่อเพิ่มเมธอดคลาสได้หรือไม่? ใช่!

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"

นี้self.singleton_class.include M::ClassMethodsสายไม่ได้ดูดีมากดังนั้นทับทิมเพิ่มObject#extendซึ่งไม่เหมือนกัน - คือรวมถึงโมดูลเข้าไปในชั้นเดี่ยวของวัตถุนี้:

class HostKlass
  include M
  extend M::ClassMethods
end

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

การย้ายการextendโทรไปยังโมดูล

ตัวอย่างก่อนหน้านี้ไม่ใช่โค้ดที่มีโครงสร้างดีด้วยเหตุผลสองประการ:

  1. ตอนนี้เราต้องเรียกทั้งสอง includeและextendในHostClassคำจำกัดความเพื่อให้โมดูลของเรารวมอย่างถูกต้อง สิ่งนี้จะยุ่งยากมากหากคุณต้องรวมโมดูลที่คล้ายกันจำนวนมาก
  2. HostClassการอ้างอิงโดยตรงM::ClassMethodsซึ่งเป็นรายละเอียดการใช้งานของโมดูลMที่HostClassไม่จำเป็นต้องรู้หรือสนใจ

แล้วสิ่งนี้: เมื่อเราเรียกincludeบรรทัดแรกเราจะแจ้งให้โมดูลทราบว่ามีการรวมและยังให้คลาสอ็อบเจ็กต์ของเราด้วยเพื่อให้สามารถเรียกextendตัวเองได้ วิธีนี้เป็นหน้าที่ของโมดูลที่จะต้องเพิ่มเมธอดคลาสหากต้องการ

นี่คือสิ่งที่เป็นself.includedวิธีพิเศษสำหรับ Ruby เรียกเมธอดนี้โดยอัตโนมัติเมื่อใดก็ตามที่โมดูลรวมอยู่ในคลาสอื่น (หรือโมดูล) และส่งผ่านอ็อบเจ็กต์คลาสโฮสต์เป็นอาร์กิวเมนต์แรก:

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!

self.includedของหลักสูตรการเพิ่มวิธีการเรียนไม่ได้เป็นเพียงสิ่งเดียวที่เราสามารถทำได้ใน เรามีคลาสออบเจ็กต์ดังนั้นเราจึงสามารถเรียกใช้เมธอด (คลาส) อื่น ๆ ได้:

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

2
คำตอบที่ยอดเยี่ยม! ในที่สุดก็สามารถเข้าใจแนวคิดนี้ได้หลังจากต่อสู้กันมาทั้งวัน ขอขอบคุณ.
Sankalp

1
ฉันคิดว่านี่อาจเป็นคำตอบที่เขียนได้ดีที่สุดที่ฉันเคยเห็นใน SO ขอบคุณสำหรับความชัดเจนที่น่าทึ่งและการขยาย Ruby ที่ไม่มีที่สิ้นสุดของฉัน ถ้าฉันสามารถให้ของขวัญนี้เป็นโบนัส 100 คะแนนฉันจะ!
Peter Nixey

7

ดังที่ Sergio กล่าวไว้ในความคิดเห็นสำหรับผู้ชายที่อยู่ใน Rails อยู่แล้ว (หรือไม่สนใจขึ้นอยู่กับActive Support ) Concernมีประโยชน์ที่นี่:

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

คุณสามารถมีเค้กของคุณและกินมันด้วยโดยทำดังนี้:

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"]

หากคุณตั้งใจจะเพิ่มอินสแตนซ์และตัวแปรคลาสคุณจะต้องดึงผมของคุณออกมาเพราะคุณจะพบกับโค้ดที่เสียหายจำนวนมากเว้นแต่คุณจะทำแบบนี้


มีบางสิ่งแปลก ๆ ที่ใช้ไม่ได้เมื่อส่ง class_eval ไปยังบล็อกเช่นการกำหนดค่าคงที่การกำหนดคลาสที่ซ้อนกันและการใช้ตัวแปรคลาสนอกเมธอด เพื่อสนับสนุนสิ่งเหล่านี้คุณสามารถให้ class_eval เป็น heredoc (สตริง) แทนการบล็อก: base.class_eval << - 'END'
Paul Donohue
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.