Was entspricht der Java-Schnittstelle in Ruby?


101

Können wir Schnittstellen in Ruby wie in Java verfügbar machen und die Ruby-Module oder -Klassen erzwingen, um die durch die Schnittstelle definierten Methoden zu implementieren?

Eine Möglichkeit besteht darin, Vererbung und method_missing zu verwenden, um dasselbe zu erreichen. Gibt es jedoch einen anderen geeigneteren Ansatz?



6
Sie sollten sich doppelt fragen, warum Sie das überhaupt brauchen. Oft werden genug Schnittstellen verwendet, um verdammte Dinge zu kompilieren, was bei Ruby kein Problem ist.
Arnis Lapsa

1
Diese Frage kann als Duplikat von [ Was entspricht in Ruby einer Schnittstelle in C #? ] ( StackOverflow.Com/q/3505521/#3507460 ).
Jörg W Mittag

2
Warum brauche ich das? Ich möchte etwas implementieren, das Sie als "versionierbar" bezeichnen können, wodurch die Dokumente / Dateien versionierbar, aber mit was versionierbar werden. Zum Beispiel kann ich es mit vorhandenen Repository-Software wie SVN oder CVS versionierbar machen. Welchen Mechanismus ich auch wähle, er sollte einige grundlegende Mindestfunktionen bieten. Ich möchte eine Schnittstelle wie eine Sache verwenden, um die Implementierung dieser minimalen Funktionen durch jede neue zugrunde liegende Repository-Implementierung zu erzwingen.
crazycrv

Sandi Metz verwendet in ihrem POODR-Buch Tests, um Schnittstellen zu dokumentieren. Es lohnt sich wirklich, dieses Buch zu lesen. Ab 2015 würde ich sagen, dass @ aleksander-pohl Antwort die beste ist.
Greg Dan

Antworten:


85

Ruby hat Schnittstellen wie jede andere Sprache.

Beachten Sie, dass Sie darauf achten müssen, das Konzept der Schnittstelle , bei der es sich um eine abstrakte Spezifikation der Verantwortlichkeiten, Garantien und Protokolle einer Einheit handelt, nicht mit dem Konzept des interfaceSchlüsselworts in der Java-, C # - und VB.NET-Programmierung zu verknüpfen Sprachen. In Ruby verwenden wir das erstere ständig, aber das letztere existiert einfach nicht.

Es ist sehr wichtig, die beiden zu unterscheiden. Wichtig ist das Interface , nicht das interface. Das interfacesagt dir so ziemlich nichts Nützliches. Nichts zeigt dies besser als die Marker-Schnittstellen in Java, bei denen es sich um Schnittstellen handelt, die überhaupt keine Mitglieder haben: Schauen Sie sich einfach java.io.Serializableund an java.lang.Cloneable. Diese beiden interfacebedeuten sehr unterschiedliche Dinge, haben aber genau die gleiche Signatur.

Also, wenn zwei interfaces , dass verschiedene Dinge bedeuten, die gleiche Signatur haben, was genau das ist interfaceauch Sie garantiert?

Ein weiteres gutes Beispiel:

package java.util;

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

Was ist die Schnittstelle von java.util.List<E>.add?

  • dass die Länge der Sammlung nicht abnimmt
  • dass alle Gegenstände, die zuvor in der Sammlung waren, noch da sind
  • das elementist in der Sammlung

Und welche davon taucht tatsächlich in der auf interface? Keiner! Es gibt nichts in dem interface, was besagt, dass die AddMethode überhaupt etwas hinzufügen muss , sie könnte genauso gut ein Element aus der Sammlung entfernen .

Dies ist eine absolut gültige Implementierung davon interface:

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

Ein weiteres Beispiel: Wo java.util.Set<E>steht eigentlich, dass es sich um eine Menge handelt ? Nirgends! Oder genauer gesagt in der Dokumentation. Auf Englisch.

In so ziemlich allen Fällen interfacesvon Java und .NET befinden sich alle relevanten Informationen tatsächlich in den Dokumenten, nicht in den Typen. Also, wenn die Typen Ihnen sowieso nichts Interessantes sagen, warum sollten Sie sie überhaupt behalten? Warum nicht einfach bei der Dokumentation bleiben? Und genau das macht Ruby.

Beachten Sie, dass es andere Sprachen gibt, in denen die Schnittstelle tatsächlich auf sinnvolle Weise beschrieben werden kann. Diese Sprachen rufen jedoch normalerweise nicht das Konstrukt auf, das die Schnittstelle " interface" beschreibt, sondern sie nennen es type. In einer abhängig typisierten Programmiersprache können Sie beispielsweise die Eigenschaften ausdrücken, dass eine sortFunktion eine Sammlung mit der gleichen Länge wie das Original zurückgibt, dass jedes Element im Original auch in der sortierten Sammlung enthalten ist und dass kein größeres Element vorhanden ist erscheint vor einem kleineren Element.

Kurz gesagt: Ruby hat kein Äquivalent zu Java interface. Es entspricht jedoch einer Java- Schnittstelle und ist genau das gleiche wie in der Java: -Dokumentation.

Ebenso wie in Java können Akzeptanztests auch zum Angeben von Schnittstellen verwendet werden.

Insbesondere in Ruby, die Schnittstelle ist eines Objekts bestimmt durch das, was er kann tun , nicht das, was classist, oder was modulesie mischt in. Jedes Objekt , das eine hat <<Methode kann angehängt werden. Dies ist sehr nützlich bei Unit-Tests, bei denen Sie einfach ein Arrayoder ein Stringstatt eines komplizierteren bestehen könnenLogger , obwohl Arrayund Loggerteilen nicht eine explizite interfaceabgesehen von der Tatsache , dass sie beide haben eine Methode aufgerufen <<.

Ein anderes Beispiel ist StringIO, dass dieselbe Schnittstelle wie IOund damit ein großer Teil der Schnittstelle von implementiert Filewird, ohne jedoch einen gemeinsamen Vorfahren zu teilen Object.


278
Während einer guten Lektüre finde ich die Antwort nicht so hilfreich. Es liest sich wie eine Dissertation darüber, warum interfacees nutzlos ist und den Sinn seiner Verwendung verfehlt. Es wäre einfacher gewesen zu sagen, dass Ruby dynamisch typisiert ist und einen anderen Fokus hat und Konzepte wie IOC unnötig / unerwünscht machen. Es ist eine schwierige Aufgabe, wenn Sie an Design by Contract gewöhnt sind. Davon könnte Rails profitieren, was das Kernteam erkannt hat, wie Sie in den neuesten Versionen sehen können.
Goliatone

12
Folgefrage: Wie kann eine Schnittstelle in Ruby am besten dokumentiert werden? Ein Java-Schlüsselwort enthält interfacemöglicherweise nicht alle relevanten Informationen, bietet jedoch einen offensichtlichen Platz für die Dokumentation. Ich habe eine Klasse in Ruby geschrieben, die (genug von) IO implementiert, aber ich habe es durch Ausprobieren gemacht und war mit dem Prozess nicht allzu zufrieden. Ich habe auch mehrere Implementierungen einer eigenen Schnittstelle geschrieben, aber es erwies sich als Herausforderung, zu dokumentieren, welche Methoden erforderlich sind und was sie tun sollen, damit andere Mitglieder meines Teams Implementierungen erstellen können.
Patrick

9
Das interface Konstrukt wird in der Tat nur benötigt, um verschiedene Typen in statisch typisierten Einzelvererbungssprachen (z. B. behandeln LinkedHashSetund ArrayListbeides als a Collection) als gleich zu behandeln. Es hat so ziemlich nichts mit Interface zu tun, wie diese Antwort zeigt. Ruby ist nicht statisch typisiert, daher ist das Konstrukt nicht erforderlich .
Esailija

16
Ich habe dies als "einige Schnittstellen machen keinen Sinn, daher sind Schnittstellen schlecht. Warum sollten Sie Schnittstellen verwenden wollen?" Gelesen. Es beantwortet die Frage nicht und klingt ehrlich gesagt nur wie jemand, der nicht versteht, wofür Schnittstellen sind und von dem er profitiert hat.
Oddman

13
Ihr Argument über die Ungültigkeit der List-Schnittstelle durch Zitieren einer Methode, die ein Entfernen in einer Funktion namens "add" ausführt, ist ein klassisches Beispiel für ein reductio ad absurdum-Argument. Insbesondere ist es in jeder Sprache (einschließlich Ruby) möglich, eine Methode zu schreiben, die etwas anderes tut als erwartet. Dies ist kein gültiges Argument gegen "Schnittstelle", es ist nur schlechter Code.
Justin Ohms

58

Probieren Sie die "gemeinsamen Beispiele" von rspec aus:

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

Sie schreiben eine Spezifikation für Ihre Schnittstelle und fügen dann eine Zeile in die Spezifikation jedes Implementierers ein, z.

it_behaves_like "my interface"

Vollständiges Beispiel:

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

Update : Acht Jahre später (2020) unterstützt Ruby nun statisch typisierte Schnittstellen über Sorbet. Siehe Abstrakte Klassen und Schnittstellen in den Sorbet-Dokumenten.


15
Ich glaube, dass dies die akzeptierte Antwort sein sollte. Auf diese Weise können die meisten typschwachen Sprachen Java-ähnliche Schnittstellen bereitstellen. Der akzeptierte erklärt, warum Ruby keine Schnittstellen hat und nicht, wie man sie emuliert.
SystematicFrank

1
Ich stimme zu, diese Antwort hat mir als Java-Entwickler, der zu Ruby wechselte, viel mehr geholfen als die oben akzeptierte Antwort.
Cam

Ja, aber der springende Punkt einer Schnittstelle ist, dass sie dieselben Methodennamen hat, aber die konkreten Klassen müssen diejenigen sein, die das Verhalten implementieren, was vermutlich anders ist. Was soll ich also im gemeinsamen Beispiel testen?
Rob Wise

Ruby macht alles pragmatisch. Wenn Sie dokumentierten und gut geschriebenen Code haben möchten, fügen Sie Tests / Spezifikationen hinzu, und dies ist eine Art statische Tippprüfung.
Dmitry Polushkin

41

Können wir Schnittstellen in Ruby wie in Java verfügbar machen und die Ruby-Module oder -Klassen erzwingen , um die durch die Schnittstelle definierten Methoden zu implementieren?

Ruby verfügt nicht über diese Funktionalität. Im Prinzip werden sie nicht benötigt, da Ruby die sogenannte Ententypisierung verwendet .

Es gibt nur wenige Ansätze, die Sie verfolgen können.

Schreiben Sie Implementierungen, die Ausnahmen auslösen. Wenn eine Unterklasse versucht, die nicht implementierte Methode zu verwenden, schlägt dies fehl

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

Zusammen mit dem oben genannten sollten Sie Testcode schreiben, der Ihre Verträge erzwingt (welcher andere Beitrag hier fälschlicherweise Schnittstelle nennt )

Wenn Sie wie immer leere Methoden schreiben, schreiben Sie ein Hilfsmodul, das dies erfasst

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

Kombinieren Sie nun das Obige mit Ruby-Modulen und Sie sind nah an dem, was Sie wollen ...

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

Und dann kannst du es tun

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

Lassen Sie mich noch einmal betonen: Dies ist rudimentär, da alles in Ruby zur Laufzeit geschieht. Es erfolgt keine Überprüfung der Kompilierungszeit. Wenn Sie dies mit dem Testen verbinden, sollten Sie in der Lage sein, Fehler zu erkennen. Wenn Sie das oben Gesagte weiter ausführen, können Sie möglicherweise eine Schnittstelle schreiben , die beim ersten Erstellen eines Objekts dieser Klasse eine Überprüfung der Klasse durchführt. Machen Sie Ihre Tests so einfach wie das Anrufen MyCollection.new... ja, übertrieben :)


Ok, aber wenn Ihre Collection = MyCollection eine Methode implementiert, die nicht in der Schnittstelle definiert ist, funktioniert dies einwandfrei. Sie können also nicht sicherstellen, dass Ihr Objekt nur die Definitionen der Schnittstellenmethoden enthält.
Joel AZEMAR

Das ist ziemlich großartig, danke. Das Tippen von Enten ist in Ordnung, aber manchmal ist es gut, anderen Entwicklern explizit mitzuteilen, wie sich eine Schnittstelle verhalten soll.
Mirodinho

10

Wie alle hier sagten, gibt es kein Schnittstellensystem für Ruby. Aber durch Selbstbeobachtung können Sie es ganz einfach selbst implementieren. Hier ist ein einfaches Beispiel, das auf viele Arten verbessert werden kann, um Ihnen den Einstieg zu erleichtern:

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

Wenn Sie eine der in Person deklarierten Methoden entfernen oder die Anzahl der Argumente ändern, wird a ausgelöst NotImplementedError.


5

Es gibt keine Schnittstellen auf Java-Weise. Aber es gibt noch andere Dinge, die Sie in Rubin genießen können.

Wenn Sie eine Art von Typen und Schnittstellen implementieren möchten, damit die Objekte überprüft werden können, ob sie einige Methoden / Nachrichten enthalten, die Sie von ihnen benötigen, können Sie sich Rubycontracts ansehen . Es definiert einen Mechanismus ähnlich den PyProtocols . Ein Blog über die Typprüfung in Ruby ist hier .

Bei den genannten Ansätzen handelt es sich nicht um lebende Projekte, obwohl das Ziel zunächst gut zu sein scheint, scheinen die meisten Ruby-Entwickler ohne strenge Typprüfung leben zu können. Die Flexibilität von Ruby ermöglicht jedoch die Implementierung der Typprüfung.

Wenn Sie Objekte oder Klassen (dasselbe in Ruby) um bestimmte Verhaltensweisen erweitern möchten oder die Ruby-Methode der Mehrfachvererbung haben möchten, verwenden Sie den Mechanismus includeoder extend. Mit können includeSie Methoden aus einer anderen Klasse oder einem anderen Modul in ein Objekt aufnehmen. Mitextend können Sie einer Klasse Verhalten hinzufügen, sodass ihre Instanzen die hinzugefügten Methoden haben. Das war jedoch eine sehr kurze Erklärung.

Ich bin der Meinung, dass der beste Weg, um die Notwendigkeit der Java-Schnittstelle zu lösen, darin besteht, das Ruby-Objektmodell zu verstehen (siehe beispielsweise Dave Thomas-Vorlesungen ). Wahrscheinlich werden Sie Java-Schnittstellen vergessen. Oder Sie haben eine außergewöhnliche Bewerbung in Ihrem Zeitplan.


Diese Dave Thomas-Vorträge stehen hinter einer Paywall.
Purplejacket

5

Wie viele Antworten zeigen, gibt es in Ruby keine Möglichkeit, eine Klasse zu zwingen, eine bestimmte Methode zu implementieren, indem sie von einer Klasse, einschließlich eines Moduls , erbt oder ähnlichem . Der Grund dafür ist wahrscheinlich die Verbreitung von TDD in der Ruby-Community, die eine andere Art der Definition der Schnittstelle darstellt. Die Tests geben nicht nur die Signaturen der Methoden an, sondern auch das Verhalten. Wenn Sie also eine andere Klasse implementieren möchten, die eine bereits definierte Schnittstelle implementiert, müssen Sie sicherstellen, dass alle Tests erfolgreich sind.

Normalerweise werden die Tests isoliert mithilfe von Mocks und Stubs definiert. Es gibt aber auch Tools wie Bogus , mit denen Vertragstests definiert werden können. Solche Tests definieren nicht nur das Verhalten der "primären" Klasse, sondern prüfen auch, ob die Stubbed-Methoden in den kooperierenden Klassen vorhanden sind.

Wenn Sie sich wirklich mit Schnittstellen in Ruby befassen, würde ich die Verwendung eines Testframeworks empfehlen, das Vertragstests implementiert.


3

Alle Beispiele hier sind interessant, aber es fehlt die Validierung des Schnittstellenvertrags. Ich meine, wenn Sie möchten, dass Ihr Objekt alle Definitionen der Schnittstellenmethoden implementiert und nur diese, die Sie nicht können. Deshalb schlage ich Ihnen ein schnelles einfaches Beispiel vor (das mit Sicherheit verbessert werden kann), um sicherzustellen, dass Sie genau das haben, was Sie von Ihrer Schnittstelle erwarten (Der Vertrag).

Betrachten Sie Ihre Schnittstelle mit den definierten Methoden wie diesen

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

Dann können Sie ein Objekt mit mindestens dem Schnittstellenvertrag schreiben:

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

Sie können Ihr Objekt sicher über Ihre Schnittstelle aufrufen, um sicherzustellen, dass Sie genau das sind, was die Schnittstelle definiert

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

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

Sie können auch sicherstellen, dass Ihr Objekt alle Definitionen Ihrer Schnittstellenmethoden implementiert

class BadFooImplementation
end

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

2

Ich habe die Antwort von Carlosayam für meine zusätzlichen Bedürfnisse etwas erweitert. Dies fügt ein paar zusätzliche Verstärkungen und Optionen auf die Klasse - Schnittstelle: required_variableund optional_variabledas unterstützt einen Standardwert.

Ich bin mir nicht sicher, ob Sie diese Metaprogrammierung mit etwas zu Großem verwenden möchten.

Wie in anderen Antworten angegeben, schreiben Sie am besten Tests, die das Gesuchte ordnungsgemäß durchsetzen, insbesondere wenn Sie mit der Durchsetzung von Parametern und Rückgabewerten beginnen möchten.

Vorsichtsmaßnahme Diese Methode löst nur beim Aufrufen des Codes einen Fehler aus. Für die ordnungsgemäße Durchsetzung vor der Laufzeit wären noch Tests erforderlich.

Codebeispiel

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

Ich habe die Singleton-Bibliothek für das angegebene Muster verwendet, das ich verwende. Auf diese Weise erben alle Unterklassen die Singleton-Bibliothek, wenn diese "Schnittstelle" implementiert wird.

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

Für meine Bedürfnisse erfordert dies, dass die Klasse, die die "Schnittstelle" implementiert, sie in Unterklassen unterteilt.

class MyPlugin < Plugin

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

  def self.run
    puts 'Do Stuff™'
  end
end

2

Ruby selbst hat keine exakte Entsprechung zu Schnittstellen in Java.

Da eine solche Schnittstelle jedoch manchmal sehr nützlich sein kann, habe ich selbst ein Juwel für Ruby entwickelt, das Java-Schnittstellen auf sehr einfache Weise emuliert.

Es heißt class_interface .

Es funktioniert ganz einfach. Installiere zuerst den Edelstein von gem install class_interfaceoder füge ihn deiner Gemfile hinzu und rundebundle install .

Schnittstelle definieren:

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

Implementierung dieser Schnittstelle:

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

Wenn Sie eine bestimmte Konstante oder Methode nicht implementieren oder die Parameternummer nicht übereinstimmt, wird ein entsprechender Fehler ausgelöst, bevor das Ruby-Programm ausgeführt wird. Sie können den Typ der Konstanten sogar bestimmen, indem Sie einen Typ in der Schnittstelle zuweisen. Wenn null, ist jeder Typ zulässig.

Die Methode "implementiert" muss in der letzten Zeile einer Klasse aufgerufen werden, da dies die Codeposition ist, an der die oben implementierten Methoden bereits überprüft wurden.

Mehr unter: https://github.com/magynhard/class_interface


0

Ich stellte fest, dass ich das Muster "Nicht implementierter Fehler" zu oft für Sicherheitsüberprüfungen von Objekten verwendete, für die ich ein bestimmtes Verhalten wünschte. Am Ende schrieb ich einen Edelstein, der es grundsätzlich erlaubt, eine Schnittstelle wie diese zu verwenden:

require 'playable' 

class Instrument 
  implements Playable
end

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

Es wird nicht nach Methodenargumenten gesucht . Es funktioniert ab Version 0.2.0. Ausführlicheres Beispiel unter https://github.com/bluegod/rint

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.