Bagaimana cara menerapkan kelas abstrak di ruby?

121

Saya tahu tidak ada konsep kelas abstrak di ruby. Tetapi jika memang perlu diterapkan, bagaimana cara melakukannya? Saya mencoba sesuatu seperti ...

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

class B < A
  ...
  ...
end

Tetapi ketika saya mencoba untuk membuat instance B, itu secara internal akan memanggil A.newyang akan memunculkan pengecualian.

Selain itu, modul tidak dapat dibuat instance-nya tetapi juga tidak dapat diwariskan. membuat metode baru menjadi pribadi juga tidak akan berfungsi. Ada petunjuk?

Chirantan
sumber
1
Modul dapat dicampur, tetapi saya kira Anda memerlukan warisan klasik untuk beberapa alasan lain?
Zach
6
Bukannya saya perlu menerapkan kelas abstrak. Saya bertanya-tanya tentang bagaimana melakukannya, jika memang ada yang perlu melakukannya. Masalah pemrograman. Itu dia.
Chirantan
127
raise "Doh! You are trying to write Java in Ruby".
Andrew Grimm

Jawaban:

61

Saya tidak suka menggunakan kelas abstrak di Ruby (hampir selalu ada cara yang lebih baik). Namun, jika menurut Anda ini adalah teknik terbaik untuk situasi tersebut, Anda dapat menggunakan cuplikan berikut agar lebih deklaratif tentang metode mana yang abstrak:

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

Pada dasarnya, Anda hanya memanggil abstract_methodsdengan daftar metode yang abstrak, dan ketika dipanggil oleh instance kelas abstrak, NotImplementedErrorpengecualian akan dimunculkan.

nakajima
sumber
Ini lebih seperti antarmuka sebenarnya tetapi saya mengerti. Terima kasih.
Chirantan
6
Ini tidak terdengar sebagai kasus penggunaan yang valid NotImplementedErroryang pada dasarnya berarti "bergantung pada platform, tidak tersedia untuk Anda". Lihat dokumen .
skalee
7
Anda mengganti metode satu baris dengan meta-pemrograman, sekarang perlu menyertakan mixin dan memanggil metode. Saya tidak berpikir ini lebih deklaratif sama sekali.
Pascal
1
Saya mengedit jawaban ini memperbaiki beberapa bug kode dan memperbaruinya, jadi sekarang berjalan. Juga menyederhanakan kode sedikit, untuk menggunakan memperpanjang daripada menyertakan, karena: yehudakatz.com/2009/11/12/better-ruby-idioms
Magne
1
@ManishShrivastava: Silakan lihat kode ini sekarang, untuk komentar tentang pentingnya menggunakan END di sini vs end
Magne
113

Hanya untuk berpadu di sini, saya pikir tidak ada alasan untuk menghentikan seseorang dari membuat instance kelas abstrak, terutama karena mereka dapat menambahkan metode ke dalamnya dengan cepat .

Bahasa pengetikan bebek, seperti Ruby, menggunakan keberadaan / ketidakhadiran atau perilaku metode pada waktu proses untuk menentukan apakah mereka harus dipanggil atau tidak. Oleh karena itu, pertanyaan Anda, karena berlaku untuk metode abstrak , masuk akal

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

dan itu seharusnya tentang akhir cerita. Satu-satunya alasan untuk menggunakan kelas abstrak di Java adalah untuk menegaskan bahwa metode tertentu "diisi" sementara yang lain memiliki perilakunya di kelas abstrak. Dalam bahasa pengetikan bebek, fokusnya adalah pada metode, bukan pada kelas / tipe, jadi Anda harus memindahkan kekhawatiran Anda ke level itu.

Dalam pertanyaan Anda, pada dasarnya Anda mencoba membuat ulang abstractkata kunci dari Java, yang merupakan code-bau untuk melakukan Java di Ruby.

Dan Rosenstark
sumber
3
@ Christopher Perry: Ada alasan mengapa?
SasQ
10
@ChristopherPerry Saya masih belum mengerti. Mengapa saya tidak menginginkan ketergantungan ini jika orang tua dan saudara kandungnya memang terkait dan saya ingin hubungan ini menjadi eksplisit? Juga, untuk membuat objek dari beberapa kelas di dalam kelas lain, Anda perlu mengetahui definisinya juga. Inheritance biasanya diimplementasikan sebagai komposisi, hanya membuat antarmuka dari objek yang dibuat menjadi bagian dari antarmuka kelas yang menyematkannya. Jadi, Anda tetap memerlukan definisi objek yang disematkan atau diwarisi. Atau mungkin Anda sedang membicarakan hal lain? Bisakah Anda menjelaskan lebih lanjut tentang itu?
SasQ
2
@SasQ, Anda tidak perlu mengetahui detail implementasi kelas induk untuk membuatnya, Anda hanya perlu mengetahui 'API-nya. Namun, jika Anda mewarisi, Anda bergantung pada implementasi orang tua. Jika implementasi mengubah kode Anda bisa rusak dengan cara yang tidak terduga. Detail lebih lanjut di sini
Christopher Perry
16
Maaf, tapi "Lebih menyukai komposisi daripada warisan" tidak mengatakan "Selalu komposisi pengguna". Meskipun secara umum warisan harus dihindari, ada beberapa kasus penggunaan ketika mereka lebih cocok. Jangan mengikuti buku secara membabi buta.
Nowaker
1
@Naker Poin yang sangat penting. Seringkali kita cenderung dibutakan oleh hal-hal yang kita baca atau dengar, alih-alih berpikir "apa pendekatan pragmatis dalam kasus ini". Jarang sekali hitam atau putih.
Per Lundberg
44

Coba ini:

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

class B < A
  def initialize
  end
end
Andrew Peters
sumber
38
Jika Anda ingin dapat menggunakan super dalam #initializeB, Anda sebenarnya dapat meningkatkan apa pun di A # inisialisasi if self.class == A.
mk12
17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end
bluehavana
sumber
7
Selain itu, seseorang dapat menggunakan hook yang diwariskan dari kelas induk untuk membuat metode konstruktor secara otomatis terlihat di semua subclass: def A.inherited (subclass); subclass.instance_eval {public_class_method: baru}; akhir
t6d
1
T6d sangat bagus. Sebagai komentar, pastikan itu didokumentasikan, karena itu adalah perilaku yang mengejutkan (paling tidak melanggar kejutan).
bluehavana
16

untuk siapa saja di dunia rails, mengimplementasikan model ActiveRecord sebagai kelas abstrak dilakukan dengan deklarasi ini di file model:

self.abstract_class = true
Fred Willmore
sumber
12

2 ¢ saya: Saya memilih mixin DSL yang sederhana dan ringan:

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

Dan, tentu saja, menambahkan kesalahan lain untuk menginisialisasi kelas dasar akan menjadi hal yang sepele dalam kasus ini.

Anthony Navarre
sumber
1
EDIT: Untuk ukuran yang baik, karena pendekatan ini menggunakan metode_definisi, orang mungkin ingin memastikan pelacakan balik tetap utuh, misalnya: err = NotImplementedError.new(message); err.set_backtrace caller()YMMV
Anthony Navarre
Saya menemukan pendekatan ini cukup elegan. Terima kasih atas kontribusi Anda untuk pertanyaan ini.
wes.hysell
12

Dalam 6 1/2 tahun terakhir pemrograman Ruby, saya tidak membutuhkannya kelas abstrak sekali pun.

Jika Anda berpikir Anda membutuhkan kelas abstrak, Anda berpikir terlalu banyak dalam bahasa yang menyediakan / membutuhkannya, bukan di Ruby.

Seperti yang disarankan orang lain, mixin lebih sesuai untuk hal-hal yang seharusnya menjadi antarmuka (seperti yang didefinisikan oleh Java), dan memikirkan kembali desain Anda lebih tepat untuk hal-hal yang "membutuhkan" kelas abstrak dari bahasa lain seperti C ++.

Pembaruan 2019: Saya tidak membutuhkan kelas abstrak di Ruby selama 16½ tahun penggunaan. Segala sesuatu yang dikatakan oleh semua orang yang mengomentari tanggapan saya ditangani dengan benar-benar mempelajari Ruby dan menggunakan alat yang sesuai, seperti modul (yang bahkan memberi Anda implementasi umum). Ada orang-orang di tim yang saya kelola yang telah membuat kelas yang memiliki implementasi dasar yang gagal (seperti kelas abstrak), tetapi ini sebagian besar adalah pemborosan kode karena NoMethodErrorakan menghasilkan hasil yang sama persis dengan AbstractClassErrorproduksi.

Austin Ziegler
sumber
24
Anda juga tidak / membutuhkan / kelas abstrak di Java. Ini adalah cara untuk mendokumentasikan bahwa itu adalah kelas dasar dan tidak boleh dipakai untuk orang yang memperluas kelas Anda.
fijiaaron
3
IMO, beberapa bahasa harus membatasi pengertian Anda tentang pemrograman berorientasi objek. Apa yang sesuai dalam situasi tertentu tidak harus bergantung pada bahasanya, kecuali ada alasan terkait kinerja (atau sesuatu yang lebih menarik).
thekingoftruth
10
@fijiaaron: Jika Anda berpikir demikian, maka Anda pasti tidak memahami apa itu kelas dasar abstrak. Mereka tidak banyak untuk "mendokumentasikan" bahwa kelas tidak boleh dipakai (itu lebih merupakan efek samping dari menjadi abstrak). Ini lebih tentang menyatakan antarmuka umum untuk sekelompok kelas turunan, yang kemudian menjamin bahwa itu akan diimplementasikan (jika tidak, kelas turunan akan tetap abstrak juga). Tujuannya adalah untuk mendukung Prinsip Substitusi Liskov untuk kelas-kelas yang instansiasi tidak masuk akal.
SasQ
1
(lanjutan) Tentu saja seseorang dapat membuat hanya beberapa kelas dengan beberapa metode & properti umum di dalamnya, tetapi compiler / interpreter tidak akan tahu bahwa kelas-kelas ini terkait dengan cara apa pun. Nama metode & properti bisa sama di masing-masing kelas ini, tetapi yang ini saja belum berarti bahwa mereka mewakili fungsionalitas yang sama (nama korespondensi bisa jadi hanya kebetulan). Satu-satunya cara untuk memberi tahu compiler tentang relasi ini adalah dengan menggunakan kelas dasar, tetapi tidak selalu masuk akal untuk keberadaan instance kelas dasar ini sendiri.
SasQ
4

Secara pribadi saya mengangkat NotImplementedError dalam metode kelas abstrak. Tetapi Anda mungkin ingin meninggalkannya dari metode 'baru', karena alasan yang Anda sebutkan.

Zack
sumber
Tapi lalu bagaimana cara menghentikannya agar tidak dipakai?
Chirantan
Secara pribadi saya baru saja memulai dengan Ruby, tetapi dengan Python, subclass dengan metode __init ___ () yang dideklarasikan tidak secara otomatis memanggil metode __init __ () superclass mereka. Saya berharap akan ada konsep serupa di Ruby, tetapi seperti yang saya katakan, saya baru saja memulai.
Zack
Dalam initializemetode ruby, orang tua tidak dipanggil secara otomatis kecuali secara eksplisit dipanggil dengan super.
mk12
4

Jika Anda ingin menggunakan kelas uninstantiable, dalam metode A.new Anda, periksa apakah self == A sebelum melakukan kesalahan.

Tapi sungguh, modul tampaknya lebih seperti yang Anda inginkan di sini - misalnya, Enumerable adalah jenis yang mungkin menjadi kelas abstrak dalam bahasa lain. Anda secara teknis tidak dapat membuat subkelasnya, tetapi panggilan include SomeModulemencapai tujuan yang kira-kira sama. Apakah ada alasan mengapa ini tidak berhasil untuk Anda?

Membuang
sumber
4

Tujuan apa yang Anda coba layani dengan kelas abstrak? Mungkin ada cara yang lebih baik untuk melakukannya di ruby, tetapi Anda tidak memberikan detailnya.

Pointer saya adalah ini; gunakan mixin bukan warisan.

jshen
sumber
Memang. Menggabungkan sebuah Module akan sama dengan menggunakan AbstractClass: wiki.c2.com/?AbstractClass PS: Sebut saja modul dan bukan mixin, karena modul adalah apa adanya dan mencampurnya itulah yang Anda lakukan dengannya.
Magne
3

Jawaban lain:

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

Ini bergantung pada #method_missing normal untuk melaporkan metode yang tidak diterapkan, tetapi mencegah kelas abstrak diterapkan (bahkan jika mereka memiliki metode inisialisasi)

class A
  include Abstract
end
class B < A
end

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

Seperti yang dikatakan poster lain, Anda mungkin harus menggunakan mixin, daripada kelas abstrak.

rampion
sumber
3

Saya melakukannya dengan cara ini, sehingga mendefinisikan baru pada kelas anak untuk menemukan yang baru pada kelas non abstrak. Saya masih tidak melihat ada praktik praktis dalam menggunakan kelas abstrak di ruby.

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
ZeusTheTrueGod
sumber
3

Ada juga yang kecil ini abstract_type permata , memungkinkan untuk mendeklarasikan kelas dan modul abstrak dengan cara yang tidak mengganggu.

Contoh (dari file README.md ):

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
シ リ ル
sumber
1

Tidak ada yang salah dengan pendekatan Anda. Menaikkan kesalahan dalam menginisialisasi tampaknya baik-baik saja, selama tentu saja semua subkelas Anda menimpa inisialisasi. Tetapi Anda tidak ingin mendefinisikan diri sendiri. Baru seperti itu. Inilah yang akan saya lakukan.

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

Pendekatan lain akan meletakkan semua fungsionalitas itu dalam sebuah modul, yang seperti yang Anda sebutkan tidak akan pernah dapat diinstiasi. Kemudian sertakan modul di kelas Anda daripada mewarisi dari kelas lain. Namun, ini akan merusak hal-hal seperti super.

Jadi itu tergantung pada bagaimana Anda ingin menyusunnya. Meskipun modul tampak seperti solusi yang lebih bersih untuk memecahkan masalah "Bagaimana cara menulis beberapa hal yang berkenan digunakan kelas lain"

Alex Wayne
sumber
Saya juga tidak ingin melakukan itu. Kalau begitu, anak-anak tidak bisa menyebut "super".
Austin Ziegler
0

Meskipun ini tidak terasa seperti Ruby, Anda dapat melakukan ini:

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

    puts 'initialized'
  end
end

class B < A
end

Hasil:

>> A.new
  (rib):2:in `main'
  (rib):2:in `new'
  (rib):3:in `initialize'
RuntimeError: abstract class
>> B.new
initialized
=> #<B:0x00007f80620d8358>
>>
lulalala
sumber