Apa yang setara dengan antarmuka java di Ruby?

102

Bisakah kita mengekspos antarmuka di Ruby seperti yang kita lakukan di java dan menerapkan modul atau kelas Ruby untuk mengimplementasikan metode yang ditentukan oleh antarmuka.

Salah satu caranya adalah dengan menggunakan inheritance dan method_missing untuk mencapai hal yang sama, tetapi adakah pendekatan lain yang lebih sesuai yang tersedia?

crazycrv.dll
sumber
6
Anda harus bertanya dua kali pada diri sendiri mengapa Anda membutuhkan ini. Seringkali cukup banyak antarmuka yang digunakan hanya untuk mengumpulkan hal sialan yang tidak menjadi masalah di ruby.
Arnis Lapsa
1
Pertanyaan ini mungkin atau mungkin tidak dianggap sebagai duplikat dari [ Di Ruby, apa yang setara dengan antarmuka di C #? ] ( StackOverflow.Com/q/3505521/#3507460 ).
Jörg W Mittag
2
Mengapa saya membutuhkan ini? Saya ingin mengimplementasikan sesuatu yang dapat Anda sebut sebagai "versionable" yang membuat dokumen / file dapat diversi tetapi dapat diversi menggunakan apa .... Misalnya, saya dapat membuatnya dapat diversi menggunakan software repositori yang ada seperti SVN atau CVS. Mekanisme apa pun yang saya pilih harus menyediakan beberapa fungsi minimum dasar. Saya ingin menggunakan interface like thing untuk menegakkan implementasi fungsi minimal ini dengan implementasi repositori baru yang mendasarinya.
crazycrv
Sandi Metz dalam buku POODR-nya menggunakan tes untuk mendokumentasikan antarmuka. Sangatlah berharga untuk membaca buku ini. Pada 2015 saya akan mengatakan bahwa jawaban @ aleksander-pohl adalah yang terbaik.
Greg Dan

Jawaban:

85

Ruby memiliki Antarmuka seperti bahasa lainnya.

Perhatikan bahwa Anda harus berhati-hati untuk tidak menyamakan konsep Antarmuka , yang merupakan spesifikasi abstrak dari tanggung jawab, jaminan, dan protokol dari sebuah unit dengan konsep interfaceyang merupakan kata kunci dalam pemrograman Java, C # dan VB.NET bahasa. Di Ruby, kami menggunakan yang pertama sepanjang waktu, tetapi yang terakhir tidak ada.

Sangat penting untuk membedakan keduanya. Yang penting adalah Antarmuka , bukan interface. Ini interfacememberi tahu Anda tidak ada yang berguna. Tidak ada yang mendemonstrasikan ini lebih baik daripada antarmuka marker di Java, yang merupakan antarmuka yang tidak memiliki anggota sama sekali: lihat saja java.io.Serializabledan java.lang.Cloneable; keduanya memiliki interfacearti yang sangat berbeda, namun mereka memiliki tanda tangan yang persis sama .

Jadi, jika dua interfaceitu memiliki arti yang berbeda, memiliki tanda tangan yang sama, apa sebenarnya yang interfacemenjamin Anda?

Contoh bagus lainnya:

package java.util;

interface List<E> implements Collection<E>, Iterable<E> {
    void add(int index, E element)
        throws UnsupportedOperationException, ClassCastException,
            NullPointerException, IllegalArgumentException,
            IndexOutOfBoundsException;
}

Apa Antarmuka dari java.util.List<E>.add?

  • agar panjang koleksi tidak berkurang
  • bahwa semua barang yang ada di koleksi sebelumnya masih ada
  • yang elementada di dalam koleksi

Dan yang mana yang benar-benar muncul di interface? Tidak ada! Tidak ada apa pun di dalamnya interfaceyang mengatakan bahwa Addmetode tersebut bahkan harus menambahkan sama sekali, itu mungkin juga menghapus elemen dari koleksi.

Ini adalah implementasi yang sangat valid dari itu interface:

class MyCollection<E> implements java.util.List<E> {
    void add(int index, E element)
        throws UnsupportedOperationException, ClassCastException,
            NullPointerException, IllegalArgumentException,
            IndexOutOfBoundsException {
        remove(element);
    }
}

Contoh lain: di mana java.util.Set<E>sebenarnya dikatakan bahwa itu, Anda tahu, satu set ? Tidak kemana-mana! Atau lebih tepatnya, dalam dokumentasi. Dalam Bahasa Inggris.

Di hampir semua kasus interfaces, baik dari Java dan .NET, semua informasi yang relevan sebenarnya ada di dokumen, bukan di jenisnya. Jadi, jika tipe tidak memberi tahu Anda sesuatu yang menarik, mengapa tetap menyimpannya? Mengapa tidak hanya berpegang pada dokumentasi? Dan itulah yang dilakukan Ruby.

Perhatikan bahwa ada bahasa lain di mana Antarmuka sebenarnya dapat dijelaskan dengan cara yang berarti. Namun, bahasa-bahasa itu biasanya tidak memanggil konstruksi yang mendeskripsikan Antarmuka " interface", mereka menyebutnya type. Dalam bahasa pemrograman yang diketik secara dependen, Anda dapat, misalnya, mengekspresikan properti yang sortdikembalikan oleh suatu fungsi dengan panjang yang sama dengan aslinya, bahwa setiap elemen yang ada dalam aslinya juga ada dalam koleksi yang diurutkan dan tidak ada elemen yang lebih besar. muncul sebelum elemen yang lebih kecil.

Jadi, singkatnya: Ruby tidak memiliki padanan dengan Java interface. Ini tidak , bagaimanapun, memiliki setara dengan Java Antarmuka , dan itu persis sama dengan di Jawa: dokumentasi.

Juga, seperti di Java, Tes Penerimaan juga dapat digunakan untuk menentukan Antarmuka .

Khususnya, di Ruby, Antarmuka suatu objek ditentukan oleh apa yang dapat dilakukannya , bukan apa classyang ada, atau apa moduleyang digabungkannya. Objek apa pun yang memiliki <<metode dapat ditambahkan. Ini sangat berguna dalam pengujian unit, di mana Anda dapat dengan mudah meneruskan Arrayatau Stringbukan yang lebih rumit Logger, meskipun Arraydan Loggertidak membagikan eksplisit interfaceselain fakta bahwa keduanya memiliki metode yang dipanggil <<.

Contoh lain adalah StringIO, yang mengimplementasikan sama Antarmuka sebagai IOdan dengan demikian sebagian besar dari Antarmuka dari File, tetapi tanpa berbagi apapun nenek moyang selain Object.

Jörg W Mittag
sumber
279
Meskipun bacaan yang bagus, saya tidak menemukan jawaban yang membantu. Bunyinya seperti disertasi tentang mengapa interfacetidak berguna, kehilangan tujuan penggunaannya. Akan lebih mudah untuk mengatakan bahwa ruby ​​diketik secara dinamis dan memiliki fokus yang berbeda dalam pikiran dan membuat konsep seperti IOC tidak perlu / tidak diinginkan. Ini adalah perubahan yang sulit jika Anda terbiasa Desain dengan Kontrak. Something Rails bisa mendapatkan keuntungan, yang disadari oleh tim inti seperti yang Anda lihat di versi terbaru.
goliatone
12
Pertanyaan tindak lanjut: apa cara terbaik untuk mendokumentasikan antarmuka di Ruby? Kata kunci Java interfacemungkin tidak memberikan semua info yang relevan, tetapi menyediakan tempat yang jelas untuk meletakkan dokumentasi. Saya telah menulis kelas di Ruby yang mengimplementasikan (cukup) IO, tetapi saya melakukannya dengan trial and error dan tidak terlalu senang dengan prosesnya. Saya juga telah menulis beberapa implementasi antarmuka saya sendiri, tetapi mendokumentasikan metode mana yang diperlukan dan apa yang seharusnya mereka lakukan sehingga anggota lain dari tim saya dapat membuat implementasi terbukti merupakan tantangan.
Patrick
9
The interface membangun memang hanya diperlukan untuk mengobati berbagai jenis sebagai yang sama dalam bahasa tunggal warisan statis diketik (misalnya mengobati LinkedHashSetdan ArrayListbaik sebagai Collection), ia memiliki cukup banyak tidak ada yang dengan Antarmuka sebagai jawaban ini menunjukkan. Ruby tidak diketik secara statis sehingga tidak diperlukan konstruksi .
Esailija
16
Saya membaca ini sebagai "beberapa antarmuka tidak masuk akal, oleh karena itu antarmuka buruk. Mengapa Anda ingin menggunakan antarmuka?". Itu tidak menjawab pertanyaan dan terus terang hanya terdengar seperti seseorang yang tidak mengerti untuk apa antarmuka dan manfaatnya.
Oddman
13
Argumen Anda tentang ketidakabsahan antarmuka Daftar dengan mengutip metode yang melakukan penghapusan dalam fungsi yang disebut "add" adalah contoh klasik dari argumen reductio ad absurdum. Secara khusus dimungkinkan dalam bahasa apa pun (termasuk ruby) untuk menulis metode yang melakukan sesuatu yang berbeda dari yang diharapkan. Ini bukan argumen yang valid terhadap "antarmuka" itu hanya kode yang buruk.
Justin Ohms
58

Coba "contoh bersama" rspec:

https://www.relishapp.com/rspec/rspec-core/v/3-5/docs/example-groups/shared-examples

Anda menulis spesifikasi untuk antarmuka Anda dan kemudian meletakkan satu baris di setiap spesifikasi pelaksana, mis.

it_behaves_like "my interface"

Contoh lengkapnya:

RSpec.shared_examples "a collection" do
  describe "#size" do
    it "returns number of elements" do
      collection = described_class.new([7, 2, 4])
      expect(collection.size).to eq(3)
    end
  end
end

RSpec.describe Array do
  it_behaves_like "a collection"
end

RSpec.describe Set do
  it_behaves_like "a collection"
end

Pembaruan : Delapan tahun kemudian (2020) ruby ​​sekarang memiliki dukungan untuk antarmuka yang diketik secara statis melalui sorbet. Lihat Kelas dan Antarmuka Abstrak di dokumentasi sorbet.

Jared Beck
sumber
15
Saya percaya bahwa ini harus menjadi jawaban yang diterima. Ini adalah cara kebanyakan bahasa lemah dapat menyediakan antarmuka seperti Java. Yang diterima menjelaskan mengapa Ruby tidak memiliki antarmuka, bukan bagaimana cara mengemulasinya.
SystematicFrank
1
Saya setuju, jawaban ini sangat membantu saya sebagai pengembang Java yang pindah ke Ruby daripada jawaban yang diterima di atas.
Cam
Ya, tetapi inti dari sebuah antarmuka adalah ia memiliki nama metode yang sama tetapi kelas konkretnya harus menjadi orang yang mengimplementasikan perilaku, yang mungkin berbeda. Jadi, apa yang seharusnya saya uji dalam contoh yang dibagikan?
Rob Wise
Ruby membuat segalanya pragmatis. Jika Anda ingin mendokumentasikan dan menulis kode dengan baik, tambahkan tes / spesifikasi dan itu akan menjadi semacam pemeriksaan pengetikan statis.
Dmitry Polushkin
41

Bisakah kita mengekspos antarmuka di Ruby seperti yang kita lakukan di java dan menerapkan modul atau kelas Ruby untuk mengimplementasikan metode yang ditentukan oleh antarmuka.

Ruby tidak memiliki fungsionalitas itu. Pada prinsipnya, ini tidak membutuhkannya karena Ruby menggunakan apa yang disebut mengetik bebek .

Ada beberapa pendekatan yang bisa Anda lakukan.

Tulis implementasi yang memunculkan pengecualian; jika subclass mencoba menggunakan metode yang tidak diimplementasikan, itu akan gagal

class CollectionInterface
  def add(something)
    raise 'not implemented'
  end
end

Bersamaan dengan di atas, Anda harus menulis kode pengujian yang memberlakukan kontrak Anda (apa yang posting lain di sini salah sebut Antarmuka )

Jika Anda menemukan diri Anda menulis metode void seperti di atas setiap saat, maka tulis modul pembantu yang menangkapnya

module Interface
  def method(name)
    define_method(name) { |*args|
      raise "interface method #{name} not implemented"
    }
  end
end

class Collection
  extend Interface
  method :add
  method :remove
end

Sekarang, gabungkan yang di atas dengan modul Ruby dan Anda mendekati apa yang Anda inginkan ...

module Interface
  def method(name)
    define_method(name) { |*args|
      raise "interface method #{name} not implemented"
    }
  end
end

module Collection
  extend Interface
  method :add
  method :remove
end

col = Collection.new # <-- fails, as it should

Dan kemudian Anda bisa melakukannya

class MyCollection
  include Collection

  def add(thing)
    puts "Adding #{thing}"
  end
end

c1 = MyCollection.new
c1.add(1)     # <-- output 'Adding 1'
c1.remove(1)  # <-- fails with not implemented

Izinkan saya menekankan sekali lagi: ini belum sempurna, karena segala sesuatu di Ruby terjadi saat runtime; tidak ada pemeriksaan waktu kompilasi. Jika Anda memasangkan ini dengan pengujian, maka Anda seharusnya dapat menemukan kesalahan. Lebih jauh lagi, jika Anda mengambil langkah di atas lebih jauh, Anda mungkin bisa menulis Antarmuka yang melakukan pemeriksaan pada kelas pertama kali objek dari kelas itu dibuat; membuat tes Anda sesederhana menelepon MyCollection.new... ya, di atas :)

carlosayam.dll
sumber
Oke, tetapi jika Collection = MyCollection Anda menerapkan metode yang tidak ditentukan dalam Antarmuka, ini berfungsi dengan sempurna, jadi Anda tidak dapat memastikan Objek Anda hanya memiliki definisi metode Antarmuka.
Joel AZEMAR
Ini luar biasa, terima kasih. Pengetikan bebek boleh-boleh saja, tetapi terkadang ada baiknya untuk secara eksplisit mengkomunikasikan kepada developer lain bagaimana sebuah antarmuka seharusnya berperilaku.
Mirodinho
10

Seperti yang dikatakan semua orang di sini, tidak ada sistem antarmuka untuk ruby. Namun melalui introspeksi, Anda bisa menerapkannya sendiri dengan cukup mudah. Berikut adalah contoh sederhana yang dapat ditingkatkan dalam banyak cara untuk membantu Anda memulai:

class Object
  def interface(method_hash)
    obj = new
    method_hash.each do |k,v|
      if !obj.respond_to?(k) || !((instance_method(k).arity+1)*-1)
        raise NotImplementedError, "#{obj.class} must implement the method #{k} receiving #{v} parameters"
      end
    end
  end
end

class Person
  def work(one,two,three)
    one + two + three
  end

  def sleep
  end

  interface({:work => 3, :sleep => 0})
end

Menghapus salah satu metode yang dideklarasikan pada Person atau mengubahnya, jumlah argumen akan memunculkan a NotImplementedError.

fotanus.dll
sumber
5

Tidak ada yang namanya antarmuka dalam cara Java. Tapi ada hal lain yang bisa Anda nikmati di ruby.

Jika Anda ingin menerapkan beberapa jenis dan antarmuka - sehingga objek dapat diperiksa apakah mereka memiliki beberapa metode / pesan yang Anda butuhkan darinya -, Anda kemudian dapat melihat rubycontracts . Ini mendefinisikan mekanisme yang mirip dengan PyProtocols . Sebuah blog tentang pengecekan tipe ruby ​​ada di sini .

Pendekatan yang disebutkan bukanlah proyek hidup, meskipun tujuannya tampaknya bagus pada awalnya, tampaknya sebagian besar pengembang ruby ​​dapat hidup tanpa pemeriksaan tipe yang ketat. Tetapi fleksibilitas ruby ​​memungkinkan untuk mengimplementasikan pemeriksaan tipe.

Jika Anda ingin memperluas objek atau kelas (hal yang sama di ruby) dengan perilaku tertentu atau agak memiliki cara ruby ​​untuk beberapa pewarisan, gunakan includeatau extendmekanisme. Dengan includeAnda dapat memasukkan metode dari kelas atau modul lain ke dalam suatu objek. Dengan extendAnda dapat menambahkan perilaku ke kelas, sehingga instance-nya akan memiliki metode tambahan. Itu penjelasan yang sangat singkat.

Saya berpendapat cara terbaik untuk menyelesaikan kebutuhan antarmuka Java adalah dengan memahami model objek ruby ​​(lihat kuliah Dave Thomas misalnya). Mungkin Anda akan melupakan antarmuka Java. Atau Anda memiliki aplikasi luar biasa di jadwal Anda.

fifigyuri
sumber
Kuliah Dave Thomas itu ada di balik paywall.
Purplejacket
5

Seperti yang ditunjukkan oleh banyak jawaban, tidak ada cara di Ruby untuk memaksa kelas mengimplementasikan metode tertentu, dengan mewarisi dari kelas, termasuk modul atau yang serupa. Alasannya mungkin karena prevalensi TDD di komunitas Ruby, yang merupakan cara berbeda untuk mendefinisikan antarmuka - pengujian tidak hanya menentukan tanda tangan metode, tetapi juga perilakunya. Jadi, jika Anda ingin mengimplementasikan kelas yang berbeda, yang mengimplementasikan beberapa antarmuka yang sudah ditentukan, Anda harus memastikan bahwa semua tes lulus.

Biasanya tes didefinisikan secara terpisah menggunakan tiruan dan rintisan. Tetapi ada juga alat seperti Bogus , yang memungkinkan untuk menentukan uji kontrak. Tes semacam itu tidak hanya mendefinisikan perilaku kelas "primer", tetapi juga memeriksa bahwa metode yang dipotong ada di kelas yang bekerja sama.

Jika Anda benar-benar peduli dengan antarmuka di Ruby, saya akan merekomendasikan menggunakan kerangka kerja pengujian yang mengimplementasikan pengujian kontrak.

Aleksander Pohl
sumber
3

Semua contoh di sini menarik tetapi kehilangan validasi kontrak Antarmuka, maksud saya jika Anda ingin objek Anda menerapkan semua definisi metode Antarmuka dan hanya yang ini Anda tidak bisa. Jadi saya usulkan Anda contoh sederhana yang cepat (dapat ditingkatkan pasti) untuk memastikan Anda memiliki apa yang Anda harapkan melalui Antarmuka Anda (Kontrak).

pertimbangkan Antarmuka Anda dengan metode yang ditentukan seperti itu

class FooInterface
  class NotDefinedMethod < StandardError; end
  REQUIRED_METHODS = %i(foo).freeze
  def initialize(object)
    @object = object
    ensure_method_are_defined!
  end
  def method_missing(method, *args, &block)
    ensure_asking_for_defined_method!(method)
    @object.public_send(method, *args, &block)
  end
  private
  def ensure_method_are_defined!
    REQUIRED_METHODS.each do |method|
      if !@object.respond_to?(method)
        raise NotImplementedError, "#{@object.class} must implement the method #{method}"
      end
    end
  end
  def ensure_asking_for_defined_method!(method)
    unless REQUIRED_METHODS.include?(method)
      raise NotDefinedMethod, "#{method} doesn't belong to Interface definition"
    end
  end
end

Kemudian Anda dapat menulis objek dengan setidaknya kontrak Antarmuka:

class FooImplementation
  def foo
    puts('foo')
  end
  def bar
    puts('bar')
  end
end

Anda dapat memanggil Objek Anda dengan aman melalui Antarmuka untuk memastikan Anda persis seperti yang didefinisikan Antarmuka

#  > FooInterface.new(FooImplementation.new).foo
# => foo

#  > FooInterface.new(FooImplementation.new).bar
# => FooInterface::NotDefinedMethod: bar doesn't belong to Interface definition

Dan Anda juga dapat memastikan Objek Anda mengimplementasikan semua definisi metode Antarmuka Anda

class BadFooImplementation
end

#  > FooInterface.new(BadFooImplementation.new)
# => NotImplementedError: BadFooImplementation must implement the method foo
Joel AZEMAR
sumber
2

Saya telah menyampaikan sedikit jawaban carlosayam untuk kebutuhan tambahan saya. Ini menambahkan beberapa penegakan dan opsi tambahan ke kelas Antarmuka: required_variabledan optional_variableyang mendukung nilai default.

Saya tidak yakin Anda ingin menggunakan pemrograman meta ini dengan sesuatu yang terlalu besar.

Seperti jawaban lain yang telah dinyatakan, Anda sebaiknya menulis tes yang dengan benar menegakkan apa yang Anda cari, terutama setelah Anda ingin mulai menerapkan parameter dan mengembalikan nilai.

Peringatan bahwa metode ini hanya memberikan kesalahan pada pemanggilan kode. Pengujian akan tetap diperlukan untuk penegakan yang tepat sebelum waktu proses.

Contoh Kode

interface.rb

module Interface
  def method(name)
    define_method(name) do
      raise "Interface method #{name} not implemented"
    end
  end

  def required_variable(name)
    define_method(name) do
      sub_class_var = instance_variable_get("@#{name}")
      throw "@#{name} must be defined" unless sub_class_var
      sub_class_var
    end
  end

  def optional_variable(name, default)
    define_method(name) do
      instance_variable_get("@#{name}") || default
    end
  end
end

plugin.rb

Saya menggunakan perpustakaan tunggal untuk pola tertentu yang saya gunakan. Dengan cara ini setiap subclass mewarisi pustaka tunggal saat mengimplementasikan "antarmuka" ini.

require 'singleton'

class Plugin
  include Singleton

  class << self
    extend Interface

    required_variable(:name)
    required_variable(:description)
    optional_variable(:safe, false)
    optional_variable(:dependencies, [])

    method :run
  end
end

my_plugin.rb

Untuk kebutuhan saya, ini mengharuskan kelas yang mengimplementasikan subkelas "antarmuka" itu.

class MyPlugin < Plugin

  @name = 'My Plugin'
  @description = 'I am a plugin'
  @safe = true

  def self.run
    puts 'Do Stuff™'
  end
end
CTS_AE
sumber
2

Ruby sendiri tidak memiliki padanan yang tepat untuk antarmuka di Java.

Namun, karena antarmuka semacam itu terkadang sangat berguna, saya mengembangkan permata untuk Ruby sendiri, yang mengemulasi antarmuka Java dengan cara yang sangat sederhana.

Ini namanya class_interface.

Ini bekerja cukup sederhana. Pertama instal permata dengan gem install class_interfaceatau tambahkan ke Gemfile dan rund Anda bundle install.

Mendefinisikan antarmuka:

require 'class_interface'

class IExample
  MIN_AGE = Integer
  DEFAULT_ENV = String
  SOME_CONSTANT = nil

  def self.some_static_method
  end

  def some_instance_method
  end
end

Menerapkan antarmuka itu:

class MyImplementation
  MIN_AGE = 21
  DEFAULT_ENV = 'dev' 
  SOME_CONSTANT = 'some_value'

  def specific_method
    puts "very specific"
  end

  def self.some_static_method
    puts "static method is implemented!"
  end

  def some_instance_method
    # implementation
  end

  def self.another_methods
    # implementation
  end

  implements IExample
end

Jika Anda tidak menerapkan konstanta atau metode tertentu atau nomor parameter tidak cocok, kesalahan terkait akan dimunculkan sebelum program Ruby dijalankan. Anda bahkan dapat menentukan tipe konstanta dengan menetapkan tipe di antarmuka. Jika nihil, semua jenis diperbolehkan.

Metode "mengimplementasikan" harus dipanggil pada baris terakhir kelas, karena itu adalah posisi kode di mana metode yang diimplementasikan di atas sudah diperiksa.

Selengkapnya di: https://github.com/magynhard/class_interface

magynhard
sumber
0

Saya menyadari bahwa saya menggunakan pola "Kesalahan tidak diterapkan" terlalu banyak untuk pemeriksaan keamanan pada objek yang saya inginkan untuk perilaku tertentu. Akhirnya menulis permata yang pada dasarnya memungkinkan untuk menggunakan antarmuka seperti ini:

require 'playable' 

class Instrument 
  implements Playable
end

Instrument.new #will throw: Interface::Error::NotImplementedError: Expected Instrument to implement play for interface Playable

Itu tidak memeriksa argumen metode . Itu seperti versi 0.2.0. Contoh lebih rinci di https://github.com/bluegod/rint

BLuEGoD
sumber