Bisakah saya menjalankan metode instance pada modul Ruby tanpa menyertakannya?

181

Latar Belakang:

Saya memiliki modul yang menyatakan sejumlah metode contoh

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

  def format_text(x); ...
end

Dan saya ingin memanggil beberapa metode ini dari dalam kelas. Bagaimana Anda biasanya melakukan ini di ruby ​​adalah seperti ini:

class UsefulWorker
  include UsefulThings

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

Masalah

include UsefulThingsmembawa semua metode dari UsefulThings. Dalam hal ini saya hanya ingin format_textdan secara eksplisit tidak mau get_filedan delete_file.

Saya dapat melihat beberapa solusi yang mungkin untuk ini:

  1. Entah bagaimana memohon metode secara langsung pada modul tanpa menyertakannya di mana saja
    • Saya tidak tahu bagaimana / jika ini bisa dilakukan. (Karenanya pertanyaan ini)
  2. Entah bagaimana memasukkan Usefulthingsdan hanya membawa beberapa metode itu
    • Saya juga tidak tahu bagaimana / jika ini bisa dilakukan
  3. Buat kelas proxy, termasuk UsefulThingsdi dalamnya, lalu delegasikan format_textke instance proxy itu
    • Ini akan berhasil, tetapi kelas proxy anonim adalah peretasan. Yuck.
  4. Bagi modul menjadi 2 atau lebih modul yang lebih kecil
    • Ini juga akan berhasil, dan mungkin merupakan solusi terbaik yang dapat saya pikirkan, tetapi saya lebih memilih untuk menghindarinya karena saya akan berakhir dengan proliferasi puluhan dan lusinan modul - mengelola ini akan memberatkan

Mengapa ada banyak fungsi yang tidak berhubungan dalam satu modul? Itu ApplicationHelperdari aplikasi rel, yang tim kami telah memutuskan secara de facto sebagai tempat pembuangan untuk sesuatu yang tidak cukup spesifik untuk dimiliki di tempat lain. Sebagian besar metode utilitas mandiri yang digunakan di mana-mana. Saya dapat memecahnya menjadi pembantu yang terpisah, tetapi akan ada 30 dari mereka, semua dengan 1 metode masing-masing ... ini tampaknya tidak produktif

Orion Edwards
sumber
Jika Anda mengambil pendekatan ke-4 (membelah modul), Anda bisa membuatnya sehingga satu modul selalu secara otomatis memasukkan yang lain dengan menggunakan Module#includedpanggilan balik untuk memicu yang includelain. The format_textMetode dapat dipindahkan ke dalamnya modul sendiri, karena tampaknya berguna pada itu sendiri. Ini akan membuat manajemen sedikit kurang memberatkan.
Batkins
Saya bingung dengan semua referensi dalam jawaban untuk fungsi modul. Misalkan Anda memiliki module UT; def add1; self+1; end; def add2; self+2; end; enddan ingin menggunakan add1tetapi tidak add2di kelas Fixnum. Bagaimana bisa membantu untuk memiliki fungsi modul untuk itu? Apakah saya melewatkan sesuatu?
Cary Swoveland

Jawaban:

135

Jika suatu metode pada modul diubah menjadi fungsi modul, Anda cukup membatalkannya dari Mods seolah-olah telah dinyatakan sebagai

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

Pendekatan module_function di bawah ini akan menghindari pemutusan kelas yang menyertakan semua Mod.

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  

Namun, saya ingin tahu mengapa satu set fungsi yang tidak terkait semuanya ada dalam modul yang sama?

Diedit untuk menunjukkan bahwa termasuk masih berfungsi jika public :foodipanggil setelahmodule_function :foo

dgtized
sumber
1
Selain itu, module_functionmengubah metode menjadi pribadi, yang akan memecahkan kode lain - jika tidak ini akan menjadi jawaban yang diterima
Orion Edwards
Saya akhirnya melakukan hal yang layak dan refactoring kode saya menjadi modul terpisah. Itu tidak seburuk yang saya kira. Jawaban Anda masih akan menyelesaikannya dengan benar. WRT kendala asli saya, jadi terima!
Orion Edwards
@dgtized fungsi terkait dapat berakhir dalam satu modul sepanjang waktu, itu tidak berarti bahwa saya ingin mencemari namespace saya dengan semuanya. Contoh sederhana jika ada a Files.truncatedan a Strings.truncatedan saya ingin menggunakan keduanya di kelas yang sama, secara eksplisit. Membuat kelas / instance baru setiap kali saya memerlukan metode khusus atau memodifikasi yang asli bukanlah pendekatan yang baik, meskipun saya bukan seorang dev Ruby.
TWiStErRob
146

Saya pikir cara terpendek untuk hanya melakukan panggilan tunggal (tanpa mengubah modul yang ada atau membuat yang baru) adalah sebagai berikut:

Class.new.extend(UsefulThings).get_file
dolzenko
sumber
2
Sangat berguna untuk file erb. html.erb, atau js.erb. terima kasih! Saya ingin tahu apakah sistem ini menghabiskan memori
Albert Català
5
@ AlbertCatalà menurut pengujian saya dan stackoverflow.com/a/23645285/54247 kelas anonim adalah sampah yang dikumpulkan sama seperti yang lainnya, jadi tidak boleh membuang-buang memori.
dolzenko
1
Jika Anda tidak suka membuat kelas anonim sebagai proxy, Anda juga bisa menggunakan objek sebagai proxy untuk metode ini. Object.new.extend(UsefulThings).get_file
3limin4t0r
83

Cara lain untuk melakukannya jika Anda "memiliki" modul ini adalah menggunakannya 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
Dustin
sumber
27
Dan untuk kasus di mana Anda tidak memilikinya: UsefulThings.send: module_function,: b
Dustin
3
module_function mengubah metode ini menjadi metode privat (baik itu yang dilakukan di IRB saya), yang akan memecah penelepon lain :-(
Orion Edwards
Saya sebenarnya menyukai pendekatan ini, setidaknya untuk tujuan saya. Sekarang saya bisa menelepon ModuleName.method :method_nameuntuk mendapatkan objek metode dan memanggilnya via method_obj.call. Kalau tidak, saya harus mengikat metode ke instance dari objek asli, yang tidak mungkin jika objek asli adalah Modul. Menanggapi Orion Edwards, module_functionapakah membuat metode instance asli pribadi. ruby-doc.org/core/classes/Module.html#M001642
John
2
Orion - Saya tidak percaya itu benar. Menurut ruby-doc.org/docs/ProgrammingRuby/html/… , module_function "menciptakan fungsi modul untuk metode yang disebutkan. Fungsi-fungsi ini dapat dipanggil dengan modul sebagai penerima, dan juga tersedia sebagai metode contoh untuk kelas yang menggabungkan modul. Fungsi modul adalah salinan dari aslinya, dan karenanya dapat diubah secara independen. Versi metode-instance dibuat privat. Jika digunakan tanpa argumen, metode yang didefinisikan selanjutnya menjadi fungsi modul. "
Ryan Crispin Heneise
2
Anda juga bisa mendefinisikannya sebagaidef self.a; puts 'aaay'; end
Tilo
17

Jika Anda ingin memanggil metode ini tanpa menyertakan modul di kelas lain maka Anda perlu mendefinisikannya sebagai metode modul:

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

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

dan kemudian Anda dapat memanggil mereka

UsefulThings.format_text("xxx")

atau

UsefulThings::format_text("xxx")

Tapi bagaimanapun saya akan merekomendasikan agar Anda meletakkan hanya metode terkait dalam satu modul atau dalam satu kelas. Jika Anda memiliki masalah yang ingin Anda sertakan hanya satu metode dari modul maka itu terdengar seperti bau kode yang buruk dan itu bukan gaya Ruby yang baik untuk menggabungkan metode yang tidak terkait.

Raimonds Simanovskis
sumber
17

Untuk memanggil metode instance modul tanpa menyertakan modul (dan tanpa membuat objek perantara):

class UsefulWorker
  def do_work
    UsefulThings.instance_method(:format_text).bind(self).call("abc")
    ...
  end
end
renier
sumber
Hati-hati dengan pendekatan ini: mengikat pada diri sendiri mungkin tidak menyediakan metode dengan segala yang diharapkannya. Sebagai contoh, mungkin format_textmengasumsikan adanya metode lain yang disediakan oleh modul, yang (umumnya) tidak ada.
Nathan
Ini caranya, dapat memuat modul apa pun, tidak masalah jika metode dukungan modul dapat dimuat secara langsung. Bahkan lebih baik untuk mengubah level Modul. Tetapi dalam beberapa kasus, baris ini adalah apa yang ingin didapatkan oleh penanya.
twindai
6

Pertama, saya akan merekomendasikan memecah modul menjadi hal-hal berguna yang Anda butuhkan. Tetapi Anda selalu dapat membuat kelas yang diperluas untuk permohonan Anda:

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
Dustin
sumber
4

A. Jika Anda, selalu ingin memanggil mereka dengan cara yang "memenuhi syarat", mandiri (UsefulThings.get_file), maka buatlah statis seperti yang ditunjukkan orang lain,

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. Jika Anda masih ingin mempertahankan pendekatan mixin dalam kasus yang sama, dan juga pemanggilan mandiri satu kali, Anda dapat memiliki modul satu-liner yang diperluas dengan mixin:

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

  def format_text(x); ...
end

module UsefulThings
  extend UsefulThingsMixin
end

Jadi keduanya berfungsi:

  UsefulThings.get_file()       # one off

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

IMHO itu lebih bersih daripada module_functionuntuk setiap metode tunggal - kalau-kalau ingin semuanya.

inger
sumber
extend selfadalah ungkapan yang umum.
smathy
4

Seperti yang saya pahami pertanyaannya, Anda ingin mencampur beberapa metode instance modul ke dalam kelas.

Mari kita mulai dengan mempertimbangkan bagaimana Modul # termasuk bekerja. Misalkan kita memiliki modul UsefulThingsyang berisi dua metode contoh:

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

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

dan Fixnum includemodul itu:

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

Kami melihat bahwa:

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

Apakah Anda berharap UsefulThings#add3untuk menimpa Fixnum#add3, sehingga 1.add3akan kembali 4? Pertimbangkan ini:

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

Ketika kelas includemodul, modul menjadi kelas super kelas. Jadi, karena cara kerja warisan, mengirim add3ke instance Fixnumakan menyebabkan Fixnum#add3dipanggil, kembali dog.

Sekarang mari kita tambahkan metode :add2ke UsefulThings:

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

Kami sekarang berharap Fixnumuntuk includehanya metode add1dan add3. Dengan melakukan hal itu, kami berharap mendapatkan hasil yang sama seperti di atas.

Misalkan, seperti di atas, kita mengeksekusi:

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

Apa hasilnya? Metode yang tidak diinginkan :add2ditambahkan ke Fixnum, :add1ditambahkan dan, untuk alasan yang saya jelaskan di atas, :add3tidak ditambahkan. Jadi yang harus kita lakukan adalah undef :add2. Kita dapat melakukannya dengan metode pembantu sederhana:

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

yang kami panggil seperti ini:

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

Kemudian:

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

yang merupakan hasil yang kita inginkan.

Cary Swoveland
sumber
2

Tidak yakin apakah seseorang masih membutuhkannya setelah 10 tahun tetapi saya menyelesaikannya menggunakan 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"
Vitaly
sumber
0

Setelah hampir 9 tahun, inilah solusi generik:

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

Trik malang yang perlu Anda terapkan adalah memasukkan modul setelah metode telah ditetapkan. Atau Anda juga dapat memasukkannya setelah konteks didefinisikan sebagai ModuleIncluded.send(:include, CreateModuleFunctions).

Atau Anda dapat menggunakannya melalui permata reflection_utils .

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

require 'reflection_utils'
include ReflectionUtils::CreateModuleFunctions
ini adalah desainku
sumber
Nah, pendekatan Anda seperti mayoritas respons yang dapat kita lihat di sini tidak mengatasi masalah asli dan memuat semua metode. Satu-satunya jawaban yang baik adalah melepaskan ikatan metode dari modul asli dan mengikatnya di kelas yang ditargetkan, karena @renier sudah merespons 3 tahun yang lalu.
Joel AZEMAR
@ JoelAZEMAR Saya pikir Anda salah paham solusi ini. Itu harus ditambahkan ke modul yang ingin Anda gunakan. Akibatnya tidak ada metode yang harus dimasukkan untuk menggunakannya. Seperti yang disarankan oleh OP sebagai salah satu solusi yang mungkin: "1, Entah bagaimana memanggil metode langsung pada modul tanpa menyertakannya di mana saja". Begini Cara kerjanya.
thisismydesign