Ruby'deki modüllerden / karışımlardan sınıf yöntemlerini devralma


95

Ruby'de sınıf yöntemlerinin miras alındığı bilinmektedir:

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

Ancak, mixin'lerle çalışmaması beni şaşırttı:

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

#Extend yönteminin bunu yapabileceğini biliyorum:

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

Ancak hem örnek yöntemlerini hem de sınıf yöntemlerini içeren bir mixin yazıyorum (veya daha doğrusu yazmak istiyorum):

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

Şimdi yapmak istediğim şey şu:

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

A, B'nin hem örnek hem de sınıf yöntemlerini Commonmodülden miras almasını istiyorum . Ama elbette bu işe yaramıyor. Öyleyse, bu mirası tek bir modülden çalıştırmanın gizli bir yolu yok mu?

Bunu biri dahil, diğeri genişletmek için iki farklı modüle bölmek bana uygunsuz görünüyor. Başka bir olası çözüm, Commonmodül yerine bir sınıf kullanmak olabilir. Ancak bu yalnızca bir çözümdür. (Ne var yaygın işlevleri iki takım ise Common1ve Common2biz gerçekten katmalar olması gerekir?) Sınıf yöntemi miras Mixins gelen işi yapmaz neden herhangi derin bir nedeni var mı?



1
Şu ayrımla, burada mümkün olduğunu biliyorum - bunu yapmanın en az çirkin yolunu ve naif seçimin neden işe yaramadığını soruyorum.
Boris Stitnicky

1
Daha fazla deneyimle, eğer bir modül dahil ederse modül yöntemlerini dahil edenin singleton sınıfına eklerse Ruby'nin programcının amacını tahmin etmekten çok ileri gideceğini anladım. Bunun nedeni, "modül yöntemleri" nin aslında tek yöntem yöntemlerinden başka bir şey olmamasıdır. Modüller, tekil yöntemlere sahip olmak için özel değildir, yöntemlerin ve sabitlerin tanımlandığı ad alanları olarak özeldirler. Ad alanı, bir modülün tekil yöntemleriyle tamamen ilgisizdir, bu nedenle aslında tekli yöntemlerin sınıf kalıtımı, modüllerde bulunmamasından daha şaşırtıcıdır.
Boris Stitnicky

Yanıtlar:


171

Yaygın bir deyim, includedhook kullanmak ve oradan sınıf yöntemlerini enjekte etmektir .

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örnek yöntemler extendekler, sınıf yöntemleri ekler. Bu nasıl çalışır. Tutarsızlık görmüyorum, sadece karşılanmayan beklentiler :)
Sergio Tulentsev

1
Yavaş yavaş, önerinizin bu sorunun pratik çözümü kadar zarif olduğu gerçeğine katlanıyorum. Ancak, sınıflarda çalışan bir şeyin modüllerle çalışmamasının nedenini bilmek isterim.
Boris Stitnicky

6
@BorisStitnicky Bu yanıta güvenin. Bu Ruby'de çok yaygın bir deyimdir, tam olarak sorduğunuz kullanım durumunu ve tam da deneyimlediğiniz nedenlerle çözer. "İnceliksiz" görünebilir, ama en iyisi budur. (Bunu sık sık yaparsanız, includedyöntem tanımını başka bir modüle taşıyabilir ve BUNU ana modülünüze
ekleyebilirsiniz

2
"Neden" konusunda daha fazla fikir edinmek için bu konuyu okuyun .
Phrogz

2
@werkshy: modülü kukla bir sınıfa dahil edin.
Sergio Tulentsev

47

Modül dahil etmenin neden Ruby'de olduğu gibi çalıştığını anlamak için gereken metaprogramlama kavramlarını açıklayan tam hikaye.

Bir modül dahil edildiğinde ne olur?

Bir modülün bir sınıfa dahil edilmesi, modülü sınıfın atalarına ekler . ancestorsYöntemini çağırarak herhangi bir sınıfın veya modülün atalarına bakabilirsiniz :

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!

Bir örneğinde bir yöntem çağırdığınızda C, Ruby sağlanan ada sahip bir örnek yöntemi bulmak için bu üst öğe listesinin her öğesine bakacaktır . Biz dahil beri Miçine C, Mşimdi bir atası Cdediğimiz zaman bu kadar, foobir örneği C, Yakut bu yöntemi bulacaksınız M:

C.new.foo
#=> "foo"

Not o içerme sınıfına herhangi örneği veya sınıf yöntemleri kopyalamaz - bu sadece o da dahil modülünde örnek yöntemler aramaya gerektiğini sınıfa bir "not" ekler.

Modülümüzdeki "sınıf" yöntemleri ne olacak?

Dahil etme, yalnızca örnek yöntemlerinin gönderilme şeklini değiştirdiğinden, bir modülün bir sınıfa dahil edilmesi, yalnızca örnek yöntemlerinin o sınıfta kullanılabilir olmasını sağlar. Modüldeki "sınıf" yöntemleri ve diğer bildirimler otomatik olarak sınıfa kopyalanmaz:

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 sınıf yöntemlerini nasıl uygular?

Ruby'de, sınıflar ve modüller düz nesnelerdir - bunlar sınıfın örnekleridir Classve Module. Bu, dinamik olarak yeni sınıflar oluşturabileceğiniz, bunları değişkenlere atayabileceğiniz vb. Anlamına gelir:

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

klass.new.foo
#=> "foo"

Ayrıca Ruby'de, nesneler üzerinde tekil yöntemler tanımlama olanağına sahipsiniz . Bu yöntemler , nesnenin özel, gizli tekli sınıfına yeni örnek yöntemler olarak eklenir :

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]

Ancak sınıflar ve modüller aynı zamanda düz nesneler değil mi? Aslında öyleler! Bu, tekil yöntemlere de sahip olabilecekleri anlamına mı geliyor? Evet öyle! Ve sınıf yöntemleri böyle doğar:

class Abc
end

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

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

Veya, bir sınıf yöntemini tanımlamanın daha yaygın yolu, selfoluşturulan sınıf nesnesine atıfta bulunan sınıf tanımlama bloğu içinde kullanmaktır :

class Abc
  def self.foo
    "foo"
  end
end

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

Sınıf yöntemlerini bir modüle nasıl dahil ederim?

Az önce belirlediğimiz gibi, sınıf yöntemleri gerçekten sadece sınıf nesnesinin singleton sınıfındaki örnek yöntemleridir. Bu , bir grup sınıf yöntemi eklemek için singleton sınıfına bir modül dahil edebileceğimiz anlamına mı geliyor ? Evet öyle!

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"

Bu self.singleton_class.include M::ClassMethodssatır çok hoş görünmüyor, bu yüzden Ruby de eklendi Object#extend, ki bu aynı şeyi yapıyor - yani nesnenin singleton sınıfına bir modül içeriyor:

class HostKlass
  include M
  extend M::ClassMethods
end

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

extendÇağrıyı modüle taşıma

Bu önceki örnek, iki nedenden ötürü iyi yapılandırılmış kod değildir:

  1. Şimdi aramak zorunda hem include ve extendde HostClassbizim modülü düzgün dahil olsun tanımı. Çok sayıda benzer modül eklemeniz gerekirse, bu çok külfetli olabilir.
  2. HostClassdoğrudan referanslar M::ClassMethods, modülün bilmesi veya dikkat etmesi gereken bir uygulama detayıdır .MHostClass

Öyleyse şuna ne dersiniz: includeİlk satırda aradığımızda, modülün dahil edildiğini bir şekilde bildiririz ve ayrıca extendkendisini arayabilmesi için ona sınıf nesnemizi veririz . Bu şekilde, eğer isterse sınıf yöntemlerini eklemek modülün görevidir.

Özel self.includedyöntem tam olarak bunun içindir. Ruby, modül başka bir sınıfa (veya modüle) dahil edildiğinde otomatik olarak bu yöntemi çağırır ve ana bilgisayar sınıfı nesnesini ilk argüman olarak iletir:

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!

Elbette, sınıf yöntemleri eklemek yapabileceğimiz tek şey değil self.included. Sınıf nesnesine sahibiz, böylece onun üzerinde herhangi bir başka (sınıf) yöntemi çağırabiliriz:

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

2
Harika cevap! Bir günlük mücadelenin ardından nihayet kavramı anlayabildim. Teşekkür ederim.
Sankalp

1
Sanırım bu, SO'da gördüğüm en iyi yazılı cevap olabilir. İnanılmaz netlik ve Ruby anlayışımı genişlettiğiniz için teşekkür ederim. Buna 100 puanlık bir bonus hediye edebilseydim verirdim!
Peter Nixey

7

Sergio Raylar zaten (veya bağlı umursamıyorum çocuklar için yapılan yorumlar, belirtildiği gibi Aktif Destek ), Concernburada yararlıdır:

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

Bunu yaparak kekinizi alabilir ve yiyebilirsiniz:

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

Örnek ve sınıf değişkenleri eklemeyi düşünüyorsanız, bu şekilde yapmadığınız sürece bir grup bozuk kodla karşılaşacağınız için saçınızı çekip çıkaracaksınız.


Sabitleri tanımlama, yuvalanmış sınıfları tanımlama ve sınıf değişkenlerini yöntemlerin dışında kullanma gibi bir blok class_eval'i geçerken çalışmayan birkaç garip şey vardır. Bunları desteklemek için class_eval'e blok yerine heredoc (dizge) verebilirsiniz: base.class_eval << - 'END'
Paul Donohue
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.