ฉันสามารถเรียกใช้วิธีการแบบอินสแตนซ์บนโมดูล Ruby โดยไม่รวมมันได้หรือไม่


181

พื้นหลัง:

ฉันมีโมดูลที่ประกาศจำนวนวิธีการอินสแตนซ์

module UsefulThings
  def get_file; ...
  def delete_file; ...

  def format_text(x); ...
end

และฉันต้องการเรียกวิธีการเหล่านี้บางอย่างจากภายในชั้นเรียน ปกติคุณทำเช่นนี้ในทับทิมเป็นเช่นนี้:

class UsefulWorker
  include UsefulThings

  def do_work
    format_text("abc")
    ...
  end
end

ปัญหา

include UsefulThingsนำในทุกUsefulThingsวิธีการจาก ในกรณีนี้ผมเพียงต้องการformat_textและชัดเจนไม่ต้องการและget_filedelete_file

ฉันสามารถดูวิธีแก้ปัญหาที่เป็นไปได้หลายประการดังนี้:

  1. อย่างใดเรียกวิธีการโดยตรงในโมดูลโดยไม่รวมที่ใดก็ได้
    • ฉันไม่ทราบว่าสามารถทำได้หรือไม่ (ดังนั้นคำถามนี้)
  2. อย่างใดรวมถึงUsefulthingsและนำวิธีการบางอย่างเท่านั้น
    • ฉันก็ไม่รู้เหมือนกันว่าจะทำสิ่งนี้ได้อย่างไร
  3. สร้างคลาสพร็อกซีรวมUsefulThingsไว้ในนั้นจากนั้นมอบสิทธิ์format_textให้อินสแตนซ์ของพร็อกซีนั้น
    • สิ่งนี้จะได้ผล แต่คลาสพร็อกซีที่ไม่ระบุชื่อเป็นแฮ็ก yuck
  4. แบ่งโมดูลออกเป็น 2 โมดูลหรือเล็กกว่า
    • สิ่งนี้จะได้ผลและอาจเป็นทางออกที่ดีที่สุดที่ฉันสามารถนึกได้ แต่ฉันต้องการหลีกเลี่ยงเพราะฉันต้องจบด้วยโมดูลที่เพิ่มจำนวนมากขึ้นเรื่อย ๆ - การจัดการสิ่งนี้จะเป็นภาระ

ทำไมมีฟังก์ชั่นที่ไม่เกี่ยวข้องจำนวนมากในโมดูลเดียว? มันApplicationHelperมาจากแอพพลิเคชั่นรางซึ่งทีมงานของเราได้ตัดสินใจยกเลิกการทุ่มตลาดสำหรับสิ่งที่ไม่เฉพาะเจาะจงมากพอที่จะอยู่ที่อื่น วิธีการยูทิลิตี้แบบสแตนด์อโลนส่วนใหญ่ที่ใช้ในทุกที่ ฉันสามารถแบ่งมันออกเป็นผู้ช่วยเหลือแยกกันได้ แต่จะมี 30 วิธีด้วยวิธี 1 วิธี ... นี่ดูเหมือนจะไม่เกิดผล


หากคุณใช้วิธีที่ 4 (แยกโมดูล) คุณสามารถทำให้มันเป็นโมดูลหนึ่งที่รวมอื่นโดยอัตโนมัติโดยใช้การModule#includedโทรกลับเพื่อเรียกincludeอีกคนหนึ่ง format_textวิธีการจะถูกย้ายไปลงในโมดูลของตัวเองเพราะมันน่าจะเป็นประโยชน์ในการเป็นของตัวเอง สิ่งนี้จะทำให้การจัดการเป็นภาระน้อยลงเล็กน้อย
Batkins

ฉันสับสนโดยการอ้างอิงทั้งหมดในคำตอบของฟังก์ชั่นโมดูล สมมติว่าคุณมีmodule UT; def add1; self+1; end; def add2; self+2; end; endและคุณต้องการที่จะใช้add1แต่ไม่ในชั้นเรียนadd2 Fixnumมันจะช่วยให้มีฟังก์ชั่นโมดูลสำหรับสิ่งนั้นได้อย่างไร ฉันพลาดอะไรไปรึเปล่า?
Cary Swoveland

คำตอบ:


135

หากวิธีการในโมดูลกลายเป็นฟังก์ชั่นโมดูลคุณก็สามารถเรียกมันออกจาก Mods ราวกับว่ามันได้รับการประกาศเป็น

module Mods
  def self.foo
     puts "Mods.foo(self)"
  end
end

วิธีการ module_function ด้านล่างจะหลีกเลี่ยงการทำลายคลาสใด ๆ ซึ่งรวมถึง Mods ทั้งหมด

module Mods
  def foo
    puts "Mods.foo"
  end
end

class Includer
  include Mods
end

Includer.new.foo

Mods.module_eval do
  module_function(:foo)
  public :foo
end

Includer.new.foo # this would break without public :foo above

class Thing
  def bar
    Mods.foo
  end
end

Thing.new.bar  

อย่างไรก็ตามฉันอยากรู้ว่าทำไมชุดของฟังก์ชั่นที่ไม่เกี่ยวข้องทั้งหมดอยู่ในโมดูลเดียวกันตั้งแต่แรก?

แก้ไขเพื่อแสดงว่ายังคงใช้งานได้หากpublic :fooถูกเรียกใช้หลังจากนั้นmodule_function :foo


1
หลีกเลี่ยงmodule_functionวิธีอื่นให้กลายเป็นไพรเวตซึ่งจะทำลายโค้ดอื่น - มิฉะนั้นนี่จะเป็นคำตอบที่ได้รับการยอมรับ
Orion Edwards

ฉันลงเอยด้วยการทำสิ่งที่ดีและเปลี่ยนรหัสของฉันให้เป็นโมดูลแยกต่างหาก มันไม่ได้เลวร้ายอย่างที่ฉันคิดว่ามันอาจจะเป็น คำตอบของคุณคือยังคงแก้มันได้อย่างถูกต้องที่สุด WRT ข้อ จำกัด เดิมของฉันได้รับการยอมรับ!
Orion Edwards

@dgtized ฟังก์ชั่นที่เกี่ยวข้องอาจจะจบลงในโมดูลเดียวตลอดเวลานั่นไม่ได้หมายความว่าฉันต้องการที่จะสร้างมลภาวะเนมสเปซของฉันกับพวกเขาทั้งหมด ตัวอย่างง่าย ๆ ถ้ามี a Files.truncateและ a Strings.truncateและฉันต้องการใช้ทั้งในคลาสเดียวกันอย่างชัดเจน การสร้างคลาส / อินสแตนซ์ใหม่ทุกครั้งที่ฉันต้องการวิธีการเฉพาะหรือแก้ไขต้นฉบับไม่ใช่วิธีการที่ดีแม้ว่าฉันไม่ใช่นักพัฒนาทับทิม
TWiStErRob

146

ฉันคิดว่าวิธีที่สั้นที่สุดในการโทรออกเพียงครั้งเดียว (โดยไม่ต้องเปลี่ยนโมดูลที่มีอยู่หรือสร้างสายใหม่) จะเป็นดังนี้:

Class.new.extend(UsefulThings).get_file

2
มีประโยชน์มากสำหรับไฟล์ erb html.erb หรือ js.erb ขอบคุณมาก! ฉันสงสัยว่าระบบนี้จะสิ้นเปลืองความทรงจำหรือไม่
อัลเบิร์ตคาตาลัน

5
@ AlbertCatalàตามการทดสอบของฉันและstackoverflow.com/a/23645285/54247คลาสที่ไม่ระบุชื่อนั้นเป็นขยะที่เก็บรวบรวมเช่นเดียวกับทุกอย่างอื่นดังนั้นจึงไม่ควรเสียความทรงจำ
dolzenko

1
หากคุณไม่ต้องการสร้างคลาสที่ไม่ระบุชื่อเป็นพร็อกซีคุณสามารถใช้วัตถุเป็นพร็อกซีสำหรับวิธีการนี้ได้ Object.new.extend(UsefulThings).get_file
3limin4t0r

83

อีกวิธีหนึ่งที่จะทำมันถ้าคุณ "ตัวเอง" module_functionโมดูลคือการใช้งาน

module UsefulThings
  def a
    puts "aaay"
  end
  module_function :a

  def b
    puts "beee"
  end
end

def test
  UsefulThings.a
  UsefulThings.b # Fails!  Not a module method
end

test

27
และสำหรับกรณีที่คุณไม่ได้เป็นเจ้าของ: UsefulThings.send: module_function,: b
Dustin

3
module_function แปลงวิธีการให้เป็นส่วนตัว (ดีใน IRB ของฉันต่อไป) ซึ่งจะทำลายผู้โทรรายอื่น :-(
Orion Edwards

จริงๆแล้วฉันชอบวิธีนี้เพื่อวัตถุประสงค์ของฉันอย่างน้อย ตอนนี้ผมสามารถเรียกที่จะได้รับวัตถุวิธีการและเรียกว่าผ่านModuleName.method :method_name method_obj.callมิฉะนั้นฉันจะต้องผูกวิธีการกับอินสแตนซ์ของวัตถุต้นฉบับซึ่งเป็นไปไม่ได้ถ้าวัตถุต้นฉบับเป็นโมดูล เพื่อตอบสนองต่อ Orion Edwards module_functionทำให้เมธอดอินสแตนซ์ดั้งเดิมเป็นแบบส่วนตัว ruby-doc.org/core/classes/Module.html#M001642
John

2
กลุ่มดาวนายพราน - ฉันไม่เชื่อว่าเป็นเรื่องจริง ตามruby-doc.org/docs/ProgrammingRuby/html/ … , module_function "สร้างฟังก์ชั่นโมดูลสำหรับวิธีการตั้งชื่อฟังก์ชั่นเหล่านี้อาจถูกเรียกด้วยโมดูลเป็นผู้รับ โมดูลฟังก์ชั่นโมดูลเป็นสำเนาของต้นฉบับและอื่น ๆ อาจมีการเปลี่ยนแปลงได้อย่างอิสระรุ่นอินสแตนซ์เมธอดถูกทำให้เป็นส่วนตัวหากใช้โดยไม่มีอาร์กิวเมนต์วิธีที่กำหนดในภายหลังจะกลายเป็นฟังก์ชันโมดูล
Ryan Crispin Heneise

2
คุณสามารถนิยามมันเป็นdef self.a; puts 'aaay'; end
Tilo

17

ถ้าคุณต้องการเรียกเมธอดเหล่านี้โดยไม่รวมโมดูลในคลาสอื่นคุณต้องกำหนดเป็นเมธอดโมดูล:

module UsefulThings
  def self.get_file; ...
  def self.delete_file; ...

  def self.format_text(x); ...
end

จากนั้นคุณสามารถโทรหาพวกเขาด้วย

UsefulThings.format_text("xxx")

หรือ

UsefulThings::format_text("xxx")

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


17

ในการเรียกใช้วิธีการอินสแตนซ์โมดูลโดยไม่รวมโมดูล (และไม่สร้างวัตถุตัวกลาง):

class UsefulWorker
  def do_work
    UsefulThings.instance_method(:format_text).bind(self).call("abc")
    ...
  end
end

ระวังด้วยวิธีนี้: การผูกมัดกับตัวเองอาจไม่ได้ให้วิธีการกับทุกสิ่งที่มันคาดหวัง ตัวอย่างเช่นformat_textสมมติว่าการดำรงอยู่ของวิธีอื่นที่จัดทำโดยโมดูลซึ่ง (โดยทั่วไป) จะไม่ปรากฏ
นาธาน

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

6

อันดับแรกฉันขอแนะนำให้แบ่งโมดูลเป็นสิ่งที่มีประโยชน์ที่คุณต้องการ แต่คุณสามารถสร้างคลาสที่ขยายออกมาสำหรับการร้องขอของคุณ:

module UsefulThings
  def a
    puts "aaay"
  end
  def b
    puts "beee"
  end
end

def test
  ob = Class.new.send(:include, UsefulThings).new
  ob.a
end

test

4

A. ในกรณีที่คุณต้องการโทรหาพวกเขาในแบบ "ผ่านการรับรอง" แบบสแตนด์อโลน (UsefulThings.get_file) จากนั้นเพียงทำให้พวกเขาคงที่ตามที่คนอื่น ๆ ชี้ให้เห็น

module UsefulThings
  def self.get_file; ...
  def self.delete_file; ...

  def self.format_text(x); ...

  # Or.. make all of the "static"
  class << self
     def write_file; ...
     def commit_file; ...
  end

end

B. หากคุณยังต้องการรักษาแนวทางมิกซ์อินไว้ในกรณีเดียวกันเช่นเดียวกับการเรียกใช้แบบสแตนด์อะโลนเพียงครั้งเดียวคุณสามารถมีโมดูลซับเดี่ยวที่ขยายตัวเองด้วยมิกซ์อิน:

module UsefulThingsMixin
  def get_file; ...
  def delete_file; ...

  def format_text(x); ...
end

module UsefulThings
  extend UsefulThingsMixin
end

ดังนั้นทั้งสองทำงานแล้ว:

  UsefulThings.get_file()       # one off

   class MyUser
      include UsefulThingsMixin  
      def f
         format_text             # all useful things available directly
      end
   end 

IMHO สะอาดกว่าmodule_functionทุกวิธี - ในกรณีที่ต้องการทั้งหมด


extend selfเป็นสำนวนสามัญ
smathy

4

เมื่อฉันเข้าใจคำถามคุณต้องการผสมผสานวิธีการบางอย่างของโมดูลเข้ากับคลาส

เริ่มต้นด้วยการพิจารณาว่าModule # includeทำงานอย่างไร สมมติว่าเรามีโมดูลUsefulThingsที่มีวิธีการสองวิธี:

module UsefulThings
  def add1
    self + 1
  end
  def add3
    self + 3
  end
end

UsefulThings.instance_methods
  #=> [:add1, :add3]

และFixnum includeโมดูลนั้น:

class Fixnum
  def add2
    puts "cat"
  end
  def add3
    puts "dog"
  end
  include UsefulThings
end

เราเห็นว่า:

Fixnum.instance_methods.select { |m| m.to_s.start_with? "add" }
  #=> [:add2, :add3, :add1] 
1.add1
2 
1.add2
cat
1.add3
dog

ถูกที่คุณคาดหวังว่าUsefulThings#add3จะแทนที่Fixnum#add3เพื่อที่1.add3จะกลับมา4? พิจารณาสิ่งนี้:

Fixnum.ancestors
  #=> [Fixnum, UsefulThings, Integer, Numeric, Comparable,
  #    Object, Kernel, BasicObject] 

เมื่อคลาสincludeของโมดูลโมดูลกลายเป็นซูเปอร์คลาสของคลาส ดังนั้นเนื่องจากวิธีการทำงานของมรดกการส่งadd3ตัวอย่างFixnumจะทำให้Fixnum#add3ถูกเรียกกลับdogที่จะเรียกกลับมา

ตอนนี้ให้เพิ่มวิธี:add2การUsefulThings:

module UsefulThings
  def add1
    self + 1
  end
  def add2
    self + 2
  end
  def add3
    self + 3
  end
end

ตอนนี้เราหวังว่าFixnumจะincludeมีเพียงวิธีการและadd1 add3เราคาดหวังให้ได้ผลลัพธ์เช่นเดียวกับข้างต้น

สมมติว่าข้างต้นเราดำเนินการ:

class Fixnum
  def add2
    puts "cat"
  end
  def add3
    puts "dog"
  end
  include UsefulThings
end

ผลลัพธ์คืออะไร วิธีการที่ไม่พึงประสงค์:add2จะถูกเพิ่มลงFixnum, :add1มีการเพิ่มและสำหรับเหตุผลที่ผมอธิบายข้างต้น:add3จะไม่เพิ่ม undef :add2ดังนั้นสิ่งที่เราต้องทำคือ เราสามารถทำได้ด้วยวิธีการช่วยเหลือง่ายๆ:

module Helpers
  def self.include_some(mod, klass, *args)
    klass.send(:include, mod)
    (mod.instance_methods - args - klass.instance_methods).each do |m|
      klass.send(:undef_method, m)
    end
  end
end

ซึ่งเราเรียกเช่นนี้:

class Fixnum
  def add2
    puts "cat"
  end
  def add3
    puts "dog"
  end
  Helpers.include_some(UsefulThings, self, :add1, :add3)
end

แล้ว:

Fixnum.instance_methods.select { |m| m.to_s.start_with? "add" }
  #=> [:add2, :add3, :add1] 
1.add1
2 
1.add2
cat
1.add3
dog

ซึ่งเป็นผลลัพธ์ที่เราต้องการ


2

ไม่แน่ใจว่าถ้ามีคนยังต้องการมันหลังจาก 10 ปี แต่ฉันแก้ไขมันด้วย eigenclass

module UsefulThings
  def useful_thing_1
    "thing_1"
  end

  class << self
    include UsefulThings
  end
end

class A
  include UsefulThings
end

class B
  extend UsefulThings
end

UsefulThings.useful_thing_1 # => "thing_1"
A.new.useful_thing_1 # => "thing_1"
B.useful_thing_1 # => "thing_1"

0

หลังจากเกือบ 9 ปีนี่เป็นคำตอบทั่วไป:

module CreateModuleFunctions
  def self.included(base)
    base.instance_methods.each do |method|
      base.module_eval do
        module_function(method)
        public(method)
      end
    end
  end
end

RSpec.describe CreateModuleFunctions do
  context "when included into a Module" do
    it "makes the Module's methods invokable via the Module" do
      module ModuleIncluded
        def instance_method_1;end
        def instance_method_2;end

        include CreateModuleFunctions
      end

      expect { ModuleIncluded.instance_method_1 }.to_not raise_error
    end
  end
end

เคล็ดลับที่โชคร้ายที่คุณต้องใช้คือการรวมโมดูลหลังจากที่วิธีการได้รับการกำหนด ModuleIncluded.send(:include, CreateModuleFunctions)หรือคุณยังอาจรวมถึงบริบทมันหลังจากที่ถูกกำหนดให้เป็น

หรือคุณสามารถใช้มันผ่านทางreflection_utils gem

spec.add_dependency "reflection_utils", ">= 0.3.0"

require 'reflection_utils'
include ReflectionUtils::CreateModuleFunctions

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

@ JoelAZEMAR ฉันคิดว่าคุณเข้าใจผิดวิธีนี้ มันจะถูกเพิ่มเข้าไปในโมดูลที่คุณต้องการใช้ ด้วยเหตุนี้จึงไม่มีวิธีการใดที่จะรวมอยู่ในวิธีการใช้งาน ตามที่ OP แนะนำให้เป็นหนึ่งในวิธีแก้ปัญหาที่เป็นไปได้: "1, จะเรียกใช้เมธอดบนโมดูลโดยตรงโดยไม่รวมวิธีใดก็ได้" นี่คือวิธีการทำงาน
thisismydesign
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.