Ruby'de soyut bir sınıf nasıl uygulanır?


121

Ruby'de soyut sınıf kavramı olmadığını biliyorum. Ama eğer uygulanması gerekiyorsa, nasıl yapılmalı? Bir şey denedim ...

class A
  def self.new
    raise 'Doh! You are trying to write Java in Ruby!'
  end
end

class B < A
  ...
  ...
end

Ama B'yi somutlaştırmaya çalıştığımda, içsel A.newolarak istisnayı artıracak olan çağrı yapacak.

Ayrıca, modüller somutlaştırılamaz, ancak bunlar da miras alınamaz. yeni yöntemi özel yapmak da işe yaramayacaktır. Herhangi bir işaret var mı?


1
Modüller karıştırılabilir, ancak başka bir nedenden dolayı klasik mirasa ihtiyacınız olduğunu varsayıyorum?
Zach

6
Soyut bir sınıf uygulamaya ihtiyacım olduğu için değil . Birinin yapması gerekirse, nasıl yapılacağını merak ediyordum. Bir programlama problemi. Bu kadar.
Chirantan

127
raise "Doh! You are trying to write Java in Ruby".
Andrew Grimm

Yanıtlar:


61

Ruby'de soyut sınıfları kullanmayı sevmiyorum (neredeyse her zaman daha iyi bir yol vardır). Durum için en iyi teknik olduğunu gerçekten düşünüyorsanız, hangi yöntemlerin soyut olduğu konusunda daha açıklayıcı olmak için aşağıdaki pasajı kullanabilirsiniz:

module Abstract
  def abstract_methods(*args)
    args.each do |name|
      class_eval(<<-END, __FILE__, __LINE__)
        def #{name}(*args)
          raise NotImplementedError.new("You must implement #{name}.")
        end
      END
      # important that this END is capitalized, since it marks the end of <<-END
    end
  end
end

require 'rubygems'
require 'rspec'

describe "abstract methods" do
  before(:each) do
    @klass = Class.new do
      extend Abstract

      abstract_methods :foo, :bar
    end
  end

  it "raises NoMethodError" do
    proc {
      @klass.new.foo
    }.should raise_error(NoMethodError)
  end

  it "can be overridden" do
    subclass = Class.new(@klass) do
      def foo
        :overridden
      end
    end

    subclass.new.foo.should == :overridden
  end
end

Temel olarak, sadece abstract_methodssoyut olan yöntemlerin listesiyle çağırırsınız ve soyut sınıfın bir örneği tarafından çağrıldıklarında, bir NotImplementedErroristisna ortaya çıkar.


Bu aslında daha çok bir arayüz gibi ama fikri anladım. Teşekkürler.
Chirantan

6
Bu, geçerli bir kullanım durumu gibi görünmüyor, NotImplementedErrorbunun anlamı "platforma bağlı, sizde mevcut değil". Belgelere bakın .
skalee

7
Tek satırlık bir yöntemi meta programlamayla değiştiriyorsunuz, şimdi bir mixin eklemeniz ve bir yöntemi çağırmanız gerekiyor. Bunun daha açıklayıcı olduğunu sanmıyorum.
Pascal

1
Bu yanıtı düzenledim, bazı hataları düzeltip kodu güncelledim, böylece şimdi çalışıyor. Ayrıca, kodu dahil etmek yerine ext
Magne

1
@ManishShrivastava: Burada END ve end kullanmanın önemi hakkındaki yorum için lütfen şimdi bu koda bakın
Magne

113

Buraya geç gelmek için, birisinin soyut sınıfı somutlaştırmasını engellemenin bir nedeni olmadığını düşünüyorum, özellikle de ona anında yöntemler ekleyebildikleri için .

Ruby gibi Duck-typing dilleri, çağrılmaları gerekip gerekmediğini belirlemek için çalışma zamanında yöntemlerin varlığını / yokluğunu veya davranışını kullanır. Bu nedenle sorunuz, soyut bir yöntem için geçerli olduğundan mantıklıdır

def get_db_name
   raise 'this method should be overriden and return the db name'
end

ve bu hikayenin sonu olmalı. Java'da soyut sınıfları kullanmanın tek nedeni, bazı yöntemlerin "doldurulduğunda" diğerlerinin davranışlarını soyut sınıfta tuttuğunda ısrar etmektir. Ördek yazma dilinde, odak sınıflara / türlere değil yöntemlere odaklanır, bu nedenle endişelerinizi bu seviyeye taşımalısınız.

Sorunuzda, temel olarak abstractanahtar kelimeyi Ruby'de Java yapmak için bir kod kokusu olan Java'dan yeniden oluşturmaya çalışıyorsunuz .


3
@Christopher Perry: Herhangi bir neden var mı?
SasQ

10
@ChristopherPerry Hala anlamadım. Ebeveyn ve kardeş akraba ise ve ben bu ilişkinin açık olmasını istiyorsam neden bu bağımlılığı istemeyeyim? Ayrıca, başka bir sınıfın içinde bir sınıfın bir nesnesini oluşturmak için tanımını da bilmeniz gerekir. Kalıtım genellikle kompozisyon olarak uygulanır, sadece oluşturulan nesnenin arayüzünü onu yerleştiren sınıfın arayüzünün bir parçası haline getirir. Bu nedenle, yine de gömülü veya miras alınan nesnenin tanımına ihtiyacınız var. Ya da belki başka bir şeyden bahsediyorsun? Bunun üzerine biraz daha detay verebilir misin?
SasQ

2
@SasQ, onu oluşturmak için ana sınıfın uygulama ayrıntılarını bilmenize gerek yok, yalnızca 'API'sini bilmeniz gerekir. Bununla birlikte, miras alırsanız, ebeveynlerin uygulamasına güvenirsiniz. Uygulama değişirse kodunuz beklenmedik şekillerde bozulabilir. Daha fazla ayrıntı burada
Christopher Perry

16
Maalesef "Bileşimi mirasa tercih et", "Her zaman kullanıcı bileşimi" demiyor. Genel olarak kalıtımdan kaçınılması gerekse de, daha iyi uydukları birkaç kullanım durumu vardır. Kitabı körü körüne takip etmeyin.
Nowaker

1
@Nowaker Çok önemli bir nokta. Çoğu zaman, "bu durumda pragmatik yaklaşım nedir" diye düşünmek yerine, okuduğumuz veya duyduğumuz şeyler yüzünden kör olma eğilimindeyiz. Nadiren tamamen siyah veya beyazdır.
Başına Lundberg

44

Bunu dene:

class A
  def initialize
    raise 'Doh! You are trying to instantiate an abstract class!'
  end
end

class B < A
  def initialize
  end
end

38
#initializeB'nin süper girişini kullanabilmek istiyorsanız, aslında A # ilklendirmesindeki her şeyi yükseltebilirsiniz if self.class == A.
mk12

17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end

7
Ek olarak, yapıcı yöntemini tüm alt sınıflarda otomatik olarak görünür kılmak için üst sınıfın miras alınan kancası kullanılabilir: def A.inherited (subclass); subclass.instance_eval {public_class_method: new}; end
t6d

1
Çok güzel t6d. Yorum olarak, şaşırtıcı bir davranış olduğundan (en az sürprizi ihlal eder) belgelendiğinden emin olun.
bluehavana 01

16

rails dünyasındaki herkes için, bir ActiveRecord modelinin soyut sınıf olarak uygulanması, model dosyasındaki şu bildirimle yapılır:

self.abstract_class = true

12

2 ¢'m: Basit, hafif bir DSL karışımını tercih ediyorum:

module Abstract
  extend ActiveSupport::Concern

  included do

    # Interface for declaratively indicating that one or more methods are to be
    # treated as abstract methods, only to be implemented in child classes.
    #
    # Arguments:
    # - methods (Symbol or Array) list of method names to be treated as
    #   abstract base methods
    #
    def self.abstract_methods(*methods)
      methods.each do |method_name|

        define_method method_name do
          raise NotImplementedError, 'This is an abstract base method. Implement in your subclass.'
        end

      end
    end

  end

end

# Usage:
class AbstractBaseWidget
  include Abstract
  abstract_methods :widgetify
end

class SpecialWidget < AbstractBaseWidget
end

SpecialWidget.new.widgetify # <= raises NotImplementedError

Ve tabii ki, temel sınıfı başlatmak için başka bir hata eklemek bu durumda önemsiz olacaktır.


1
DÜZENLEME: İyi bir ölçüm için, bu yaklaşım define_method'u kullandığından, geri izlemenin bozulmadan kaldığından emin olmak isteyebilir, örneğin: err = NotImplementedError.new(message); err.set_backtrace caller()YMMV
Anthony Navarre,

Bu yaklaşımı oldukça zarif buluyorum. Bu soruya katkınız için teşekkür ederiz.
wes.hysell

12

Ruby'yi programlamanın son 6 1/2 yılında ihtiyacım olmadı bir kez soyut bir sınıfa .

Soyut bir sınıfa ihtiyacınız olduğunu düşünüyorsanız, Ruby'de değil, bunları sağlayan / gerektiren bir dilde çok fazla düşünüyorsunuz.

Diğerlerinin de önerdiği gibi, bir mixin arayüz olması gereken şeyler için daha uygundur (Java'nın tanımladığı gibi) ve tasarımınızı yeniden düşünmek, C ++ gibi diğer dillerden soyut sınıflara "ihtiyaç duyan" şeyler için daha uygundur.

2019 Güncellemesi: 16½ yıldır Ruby'de soyut sınıflara ihtiyacım olmadı. Yanıtım hakkında yorum yapan herkesin söylediği her şey, Ruby'yi öğrenerek ve modüller gibi uygun araçları kullanarak (hatta size ortak uygulamalar sağlayan) ele alınmaktadır. Yönettiğim ekiplerde, başarısız olan (soyut bir sınıf gibi) temel uygulamaya sahip sınıflar yaratan insanlar var, ancak bunlar çoğunlukla bir kodlama israfı çünkü üretimde NoMethodErrorbir ile aynı sonucu üretecekler AbstractClassError.


24
Java'da da soyut bir sınıfa / ihtiyacınız / ihtiyacınız yok. Bu, bunun temel bir sınıf olduğunu ve sınıfınızı genişleten insanlar için somutlaştırılmaması gerektiğini belgelemenin bir yoludur.
fijiaaron

3
IMO, birkaç dil nesne yönelimli programlama nosyonunuzu kısıtlamalıdır. Belirli bir durumda neyin uygun olduğu, performansla ilgili bir neden (veya daha zorlayıcı bir şey) olmadığı sürece dile bağlı olmamalıdır.
thekingoftruth

10
@fijiaaron: Öyle düşünüyorsanız, o zaman kesinlikle soyut temel sınıfların ne olduğunu anlamıyorsunuz. Bir sınıfın somutlaştırılmaması gerektiğini "belgelemek" için çok fazla değiller (daha ziyade soyut olmasının bir yan etkisi). Daha çok, türetilmiş sınıflar için ortak bir arabirim belirtmekle ilgilidir, bu da daha sonra uygulanacağını garanti eder (değilse, türetilmiş sınıf da soyut kalacaktır). Amacı, ilklendirmenin pek bir anlam ifade etmediği sınıflar için Liskov'un İkame İlkesini desteklemektir.
SasQ

1
(devamı) Elbette, içlerinde bazı ortak yöntemler ve özellikler bulunan birkaç sınıf oluşturulabilir, ancak derleyici / yorumlayıcı bu sınıfların herhangi bir şekilde ilişkili olduğunu bilmez. Yöntemlerin ve özelliklerin adları bu sınıfların her birinde aynı olabilir, ancak bu tek başına aynı işlevselliği temsil ettikleri anlamına gelmez (ad yazışması sadece tesadüfi olabilir). Derleyiciye bu ilişkiden bahsetmenin tek yolu bir temel sınıf kullanmaktır, ancak bu temel sınıfın örneklerinin kendisinin var olması her zaman mantıklı değildir.
SasQ


4

Kişisel olarak, soyut sınıfların yöntemlerinde NotImplementedError'ı yükseltiyorum. Ancak bahsettiğiniz nedenlerden dolayı onu 'yeni' yöntemin dışında bırakmak isteyebilirsiniz.


Ama o zaman bunun somutlaştırılmasını nasıl durdurabiliriz?
Chirantan

Şahsen ben Ruby ile yeni başlıyorum, ancak Python'da, __init ___ () yöntemlerini bildiren alt sınıflar otomatik olarak üst sınıflarının __init __ () yöntemlerini çağırmazlar. Ruby'de de benzer bir kavram olmasını umardım, ama dediğim gibi yeni başlıyorum.
Zack

Ruby'de ebeveynin initializeyöntemleri, süper ile açıkça çağrılmadıkça otomatik olarak çağrılmaz.
mk12

4

Özgünleştirilemeyen bir sınıfla gitmek istiyorsanız, yeni yönteminizde, hatayı atmadan önce self == A olup olmadığını kontrol edin.

Ama gerçekte, bir modül burada istediğiniz gibi görünüyor - örneğin, Numaralandırılabilir diğer dillerde soyut bir sınıf olabilecek türden bir şey. Teknik olarak alt sınıflara giremezsiniz, ancak çağırmak include SomeModuleaşağı yukarı aynı amaca ulaşır. Bunun sizin için işe yaramamasının bir nedeni var mı?


4

Soyut bir sınıfla hangi amaca hizmet etmeye çalışıyorsunuz? Bunu yakutla yapmanın muhtemelen daha iyi bir yolu vardır, ancak herhangi bir ayrıntı vermediniz.

İşaretçim bu; kalıtım değil bir mixin kullanın .


Aslında. Bir Modülü karıştırmak, AbstractClass kullanmaya eşdeğer olacaktır: wiki.c2.com/?AbstractClass PS: Modüller ne olduklarıdır ve onları karıştırmak, onlarla yaptığınız şey olduğundan, onlara mixins değil modüller diyelim.
Magne

3

Başka bir cevap:

module Abstract
  def self.append_features(klass)
    # access an object's copy of its class's methods & such
    metaclass = lambda { |obj| class << obj; self ; end }

    metaclass[klass].instance_eval do
      old_new = instance_method(:new)
      undef_method :new

      define_method(:inherited) do |subklass|
        metaclass[subklass].instance_eval do
          define_method(:new, old_new)
        end
      end
    end
  end
end

Bu, uygulanmamış yöntemleri bildirmek için normal #method_missing'e dayanır, ancak soyut sınıfların uygulanmasını engeller (bir başlatma yöntemine sahip olsalar bile)

class A
  include Abstract
end
class B < A
end

B.new #=> #<B:0x24ea0>
A.new # raises #<NoMethodError: undefined method `new' for A:Class>

Diğer afişlerin de söylediği gibi, muhtemelen soyut bir sınıf yerine bir mixin kullanmalısınız.


3

Bunu bu şekilde yaptım, bu yüzden soyut olmayan sınıfta yeni bir şey bulmak için çocuk sınıfında yeniyi yeniden tanımlıyor. Ruby'de soyut sınıfları kullanmanın hala pratik olduğunu görmüyorum.

puts 'test inheritance'
module Abstract
  def new
    throw 'abstract!'
  end
  def inherited(child)
    @abstract = true
    puts 'inherited'
    non_abstract_parent = self.superclass;
    while non_abstract_parent.instance_eval {@abstract}
      non_abstract_parent = non_abstract_parent.superclass
    end
    puts "Non abstract superclass is #{non_abstract_parent}"
    (class << child;self;end).instance_eval do
      define_method :new, non_abstract_parent.method('new')
      # # Or this can be done in this style:
      # define_method :new do |*args,&block|
        # non_abstract_parent.method('new').unbind.bind(self).call(*args,&block)
      # end
    end
  end
end

class AbstractParent
  extend Abstract
  def initialize
    puts 'parent initializer'
  end
end

class Child < AbstractParent
  def initialize
    puts 'child initializer'
    super
  end
end

# AbstractParent.new
puts Child.new

class AbstractChild < AbstractParent
  extend Abstract
end

class Child2 < AbstractChild

end
puts Child2.new

3

Ayrıca abstract_type, soyut sınıfları ve modülleri göze çarpmayan bir şekilde bildirmeye izin veren bu küçük mücevher de var .

Örnek ( README.md dosyasından):

class Foo
  include AbstractType

  # Declare abstract instance method
  abstract_method :bar

  # Declare abstract singleton method
  abstract_singleton_method :baz
end

Foo.new  # raises NotImplementedError: Foo is an abstract type
Foo.baz  # raises NotImplementedError: Foo.baz is not implemented

# Subclassing to allow instantiation
class Baz < Foo; end

object = Baz.new
object.bar  # raises NotImplementedError: Baz#bar is not implemented

1

Yaklaşımınızda yanlış bir şey yok. Tüm alt sınıflarınız başlatıldıkça, başlatma sırasında bir hatanın yükseltilmesi iyi görünüyor. Ama kendini böyle tanımlamak istemezsin. İşte yapacağım şey.

class A
  class AbstractClassInstiationError < RuntimeError; end
  def initialize
    raise AbstractClassInstiationError, "Cannot instantiate this class directly, etc..."
  end
end

Başka bir yaklaşım, tüm bu işlevselliği, bahsettiğiniz gibi hiçbir zaman başlatılamayacak bir modüle koymak olacaktır. Ardından, başka bir sınıftan miras almak yerine modülü sınıflarınıza dahil edin. Ancak bu, süper gibi şeyleri bozar.

Yani onu nasıl yapılandırmak istediğinize bağlı. Modüller, "Diğer sınıfların kullanması için tasarlanmış bazı şeyleri nasıl yazabilirim" sorununu çözmek için daha temiz bir çözüm gibi görünse de


Ben de bunu yapmak istemem. O halde çocuklar "süper" diyemez.
Austin Ziegler


0

Bu Ruby gibi hissettirmese de, bunu yapabilirsiniz:

class A
  def initialize
    raise 'abstract class' if self.instance_of?(A)

    puts 'initialized'
  end
end

class B < A
end

Sonuçlar:

>> A.new
  (rib):2:in `main'
  (rib):2:in `new'
  (rib):3:in `initialize'
RuntimeError: abstract class
>> B.new
initialized
=> #<B:0x00007f80620d8358>
>>
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.