Qual è l'equivalente dell'interfaccia Java in Ruby?


102

Possiamo esporre le interfacce in Ruby come facciamo in java e applicare i moduli o le classi Ruby per implementare i metodi definiti dall'interfaccia.

Un modo è utilizzare l'ereditarietà e il metodo_missing per ottenere lo stesso risultato, ma è disponibile un altro approccio più appropriato?



6
Dovresti chiederti due volte perché ne hai bisogno. Spesso vengono utilizzate abbastanza interfacce solo per compilare dannatamente cose, il che non è un problema in ruby.
Arnis Lapsa

1
Questa domanda può o non può essere considerata un duplicato di [ In Ruby, qual è l'equivalente di un'interfaccia in C #? ] ( StackOverflow.Com/q/3505521/#3507460 ).
Jörg W Mittag

2
Perché ne ho bisogno? Voglio implementare qualcosa che puoi chiamare "versionable" che renda i documenti / file versionabili ma versionabili usando cosa .... Ad esempio, posso renderlo versionabile usando software di repository esistenti come SVN o CVS. Qualunque sia il meccanismo sottostante che scelgo, dovrebbe fornire alcune funzioni minime di base. Voglio usare un'interfaccia come una cosa per imporre l'implementazione di queste funzioni minime da parte di qualsiasi nuova implementazione del repository sottostante.
crazycrv

Sandi Metz nel suo libro POODR utilizza i test per documentare le interfacce. Vale davvero la pena leggere questo libro. A partire dal 2015 direi che la risposta di @ aleksander-pohl è la migliore.
Greg Dan,

Risposte:


85

Ruby ha interfacce proprio come qualsiasi altra lingua.

Si noti che bisogna stare attenti a non confondere il concetto di Interfaccia , che è una specifica astratta delle responsabilità, garanzie e protocolli di un'unità con il concetto di interfacequale è una parola chiave nella programmazione Java, C # e VB.NET le lingue. In Ruby, usiamo sempre il primo, ma il secondo semplicemente non esiste.

È molto importante distinguere i due. Ciò che è importante è l' interfaccia , non il file interface. Non interfaceti dice praticamente nulla di utile. Niente lo dimostra meglio delle interfacce marker in Java, che sono interfacce che non hanno alcun membro: basta dare un'occhiata a java.io.Serializablee java.lang.Cloneable; quei due interfacesignificano cose molto diverse, ma hanno la stessa identica firma.

Quindi, se due interfaces che le cose medi diverse, hanno la stessa firma, che cosa esattamente è il interfaceanche garantendo te?

Un altro buon esempio:

package java.util;

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

Qual è l' interfaccia di java.util.List<E>.add?

  • che la lunghezza della raccolta non diminuisca
  • che tutti gli elementi che erano nella collezione prima sono ancora lì
  • che elementè nella raccolta

E quale di questi si presenta effettivamente nel interface? Nessuna! Non c'è niente in interfaceciò che dice che il Addmetodo deve anche aggiungere del tutto, potrebbe anche rimuovere un elemento dalla raccolta.

Questa è un'implementazione perfettamente valida di ciò interface:

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

Un altro esempio: dove java.util.Set<E>si dice effettivamente che è, sai, un set ? Da nessuna parte! O più precisamente, nella documentazione. In inglese.

In quasi tutti i casi interfaces, sia da Java che da .NET, tutte le informazioni rilevanti sono effettivamente nei documenti, non nei tipi. Quindi, se i tipi non ti dicono comunque nulla di interessante, perché tenerli affatto? Perché non limitarti alla documentazione? Ed è esattamente quello che fa Ruby.

Si noti che esistono altre lingue in cui l' interfaccia può essere effettivamente descritta in modo significativo. Tuttavia, questi linguaggi in genere non chiamano il costrutto che descrive l' interfaccia " interface", lo chiamano type. In un linguaggio di programmazione tipizzato in modo dipendente, puoi, ad esempio, esprimere le proprietà che una sortfunzione restituisce una raccolta della stessa lunghezza dell'originale, che ogni elemento che è nell'originale è anche nella raccolta ordinata e che nessun elemento più grande appare prima di un elemento più piccolo.

Quindi, in breve: Ruby non ha un equivalente a Java interface. Essa ha , tuttavia, hanno un equivalente ad un Java Interface , ed è esattamente lo stesso come in Java: la documentazione.

Inoltre, proprio come in Java, i test di accettazione possono essere utilizzati anche per specificare le interfacce .

In particolare, in Ruby, l' interfaccia di un oggetto è determinata da ciò che può fare , non da ciò che classè o da cosa modulesi mescola. Qualsiasi oggetto che abbia un <<metodo può essere aggiunto. Questo è molto utile negli unit test, dove puoi semplicemente passare un Arrayo a Stringinvece di un più complicato Logger, anche se Arraye Loggernon condividere un esplicito a interfaceparte il fatto che entrambi hanno un metodo chiamato <<.

Un altro esempio è StringIO, che implementa la stessa interfaccia come IOe quindi una grande porzione della interfaccia di File, ma senza condividere alcun antenato comune oltre Object.


279
Sebbene sia una buona lettura, non trovo che la risposta sia utile. Si legge come una dissertazione sul perché interfaceè inutile, manca il punto del suo utilizzo. Sarebbe stato più facile dire che ruby ​​è digitato dinamicamente e che ha in mente un focus diverso e rende concetti come IOC non necessari / indesiderati. È un cambiamento difficile se sei abituato a Design by Contract. Qualcosa di cui Rails potrebbe trarre vantaggio, cosa che il team principale ha capito come puoi vedere nelle ultime versioni.
goliatone

12
Domanda successiva: qual è il modo migliore per documentare un'interfaccia in Ruby? Una parola chiave Java interfacepotrebbe non fornire tutte le informazioni rilevanti, ma fornisce un luogo ovvio in cui inserire la documentazione. Ho scritto una classe in Ruby che implementa (abbastanza) IO, ma l'ho fatto per tentativi ed errori e non ero troppo soddisfatto del processo. Ho anche scritto più implementazioni di una mia interfaccia, ma documentare quali metodi sono necessari e cosa dovrebbero fare in modo che altri membri del mio team potessero creare implementazioni si è rivelata una sfida.
Patrick

9
Il interface costrutto è effettivamente necessario solo per trattare tipi diversi come uguali nei linguaggi a ereditarietà singola tipizzati staticamente (ad esempio, trattare LinkedHashSeted ArrayListentrambi come a Collection), non ha praticamente nulla a che fare con l' interfaccia come mostra questa risposta. Ruby non è tipizzato staticamente, quindi non è necessario il costrutto .
Esailija

16
Ho letto questo come "alcune interfacce non hanno senso, quindi le interfacce sono cattive. Perché dovresti usare le interfacce?". Non risponde alla domanda e francamente suona come qualcuno che non capisce a cosa servono le interfacce e il loro vantaggio.
Oddman

13
Il tuo argomento sull'invalidità dell'interfaccia List citando un metodo che esegue una rimozione in una funzione chiamata "add" è un classico esempio di un argomento reductio ad absurdum. In particolare è possibile in qualsiasi lingua (compreso ruby) scrivere un metodo che faccia qualcosa di diverso da quello che ci si aspetta. Questo non è un argomento valido contro "interfaccia" è solo un codice non valido.
Justin Ohms

58

Prova gli "esempi condivisi" di rspec:

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

Scrivi una specifica per la tua interfaccia e poi inserisci una riga nelle specifiche di ogni implementatore, ad es.

it_behaves_like "my interface"

Esempio completo:

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

Aggiornamento : otto anni dopo (2020) ruby ​​ora ha il supporto per le interfacce di tipo statico tramite sorbetto. Vedi classi e interfacce astratte nei documenti del sorbetto.


15
Credo che questa dovrebbe essere la risposta accettata. Questo è il modo in cui la maggior parte dei linguaggi deboli di tipo può fornire interfacce simili a Java. Quello accettato spiega perché Ruby non ha interfacce, non come emularle.
SystematicFrank

1
Sono d'accordo, questa risposta mi ha aiutato molto di più come sviluppatore Java che si è trasferito a Ruby rispetto alla risposta accettata sopra.
Cam

Sì, ma il punto centrale di un'interfaccia è che ha gli stessi nomi di metodo ma le classi concrete devono essere quelle per implementare il comportamento, che presumibilmente è diverso. Quindi cosa dovrei testare nell'esempio condiviso?
Rob Wise

Ruby rende tutto pragmatico. Se desideri avere un codice documentato e ben scritto, aggiungi test / specifiche e sarà una sorta di controllo statico della digitazione.
Dmitry Polushkin

41

Possiamo esporre le interfacce in Ruby come facciamo in java e applicare i moduli o le classi Ruby per implementare i metodi definiti dall'interfaccia.

Ruby non ha questa funzionalità. In linea di principio, non ne ha bisogno poiché Ruby usa quella che viene chiamata dattilografia .

Ci sono pochi approcci che puoi adottare.

Scrivere implementazioni che sollevano eccezioni; se una sottoclasse tenta di utilizzare il metodo non implementato, fallirà

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

Insieme a quanto sopra, dovresti scrivere codice di test che applica i tuoi contratti (quale altro post qui chiama erroneamente Interface )

Se ti ritrovi a scrivere sempre metodi void come sopra, scrivi un modulo di supporto che lo catturi

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

Ora, combina quanto sopra con i moduli Ruby e sei vicino a quello che vuoi ...

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

E poi puoi farlo

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

Vorrei sottolineare ancora una volta: questo è un rudimentale, poiché tutto in Ruby avviene in fase di esecuzione; non vi è alcun controllo del tempo di compilazione. Se lo accoppi con il test, dovresti essere in grado di rilevare gli errori. Ancora di più, se si prende il sopra ulteriormente, probabilmente si potrebbe essere in grado di scrivere un interfaccia che esegue il controllo sulla classe prima volta che viene creato un oggetto di quella classe; rendendo i tuoi test semplici come chiamare MyCollection.new... sì, esagerato :)


Ok ma se la tua Collection = MyCollection implementa un metodo non definito nell'interfaccia, funziona perfettamente, quindi non puoi assicurarti che il tuo oggetto abbia solo le definizioni dei metodi dell'interfaccia.
Joel AZEMAR

È davvero fantastico, grazie. La digitazione a papera va bene, ma a volte è utile comunicare esplicitamente ad altri sviluppatori come dovrebbe comportarsi un'interfaccia.
Mirodinho

10

Come tutti hanno detto qui, non esiste un sistema di interfaccia per Ruby. Ma attraverso l'introspezione, puoi implementarlo da solo abbastanza facilmente. Ecco un semplice esempio che può essere migliorato in molti modi per aiutarti a iniziare:

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

La rimozione di uno dei metodi dichiarati su Person o la modifica del numero di argomenti genererà un file NotImplementedError.


5

Non ci sono cose come le interfacce nel modo Java. Ma ci sono altre cose che puoi apprezzare in Ruby.

Se vuoi implementare un qualche tipo di tipo e interfaccia - in modo che gli oggetti possano essere controllati se hanno alcuni metodi / messaggi richiesti da loro -, puoi dare un'occhiata a rubycontracts . Definisce un meccanismo simile ai PyProtocols . Un blog sul controllo del tipo in ruby ​​è qui .

Gli approcci menzionati non sono progetti viventi, anche se l'obiettivo all'inizio sembra essere carino, sembra che la maggior parte degli sviluppatori di ruby ​​possa vivere senza un rigoroso controllo del tipo. Ma la flessibilità di ruby ​​consente di implementare il controllo del tipo.

Se vuoi estendere oggetti o classi (la stessa cosa in ruby) in base a determinati comportamenti o avere in qualche modo il modo ruby ​​dell'ereditarietà multipla, usa il meccanismo includeo extend. Con includepuoi includere metodi di un'altra classe o modulo in un oggetto. Con extendpuoi aggiungere un comportamento a una classe, in modo che le sue istanze abbiano i metodi aggiunti. Quella era però una spiegazione molto breve.

A mio parere, il modo migliore per risolvere la necessità dell'interfaccia Java è comprendere il modello a oggetti ruby ​​(vedere le lezioni di Dave Thomas per esempio). Probabilmente ti dimenticherai delle interfacce Java. Oppure hai un'applicazione eccezionale nel tuo programma.


Quelle lezioni di Dave Thomas sono dietro un paywall.
Purplejacket

5

Come molte risposte indicano, non c'è modo in Ruby di forzare una classe a implementare un metodo specifico, ereditando da una classe, incluso un modulo o qualcosa di simile. La ragione di ciò è probabilmente la prevalenza di TDD nella comunità Ruby, che è un modo diverso di definire l'interfaccia: i test non solo specificano le firme dei metodi, ma anche il comportamento. Quindi, se vuoi implementare una classe diversa, che implementa qualche interfaccia già definita, devi assicurarti che tutti i test passino.

Di solito i test vengono definiti isolatamente utilizzando mock e stub. Ma ci sono anche strumenti come Bogus , che consentono di definire i test del contratto. Tali test non solo definiscono il comportamento della classe "primaria", ma verificano anche che i metodi stub esistano nelle classi cooperanti.

Se sei veramente interessato alle interfacce in Ruby, ti consiglio di utilizzare un framework di test che implementa il test del contratto.


3

Tutti gli esempi qui sono interessanti ma manca la convalida del contratto dell'interfaccia, voglio dire se vuoi che il tuo oggetto implementi tutta la definizione dei metodi dell'interfaccia e solo questo non puoi. Quindi ti propongo un semplice esempio veloce (può essere migliorato di sicuro) per assicurarti di avere esattamente ciò che ti aspetti di avere attraverso la tua interfaccia (il contratto).

considera la tua interfaccia con i metodi definiti in questo modo

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

Quindi puoi scrivere un oggetto con almeno il contratto di interfaccia:

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

Puoi chiamare il tuo oggetto in modo sicuro attraverso la tua interfaccia per assicurarti di essere esattamente ciò che l'interfaccia definisce

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

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

E puoi anche assicurarti che il tuo oggetto implementi tutta la definizione dei metodi dell'interfaccia

class BadFooImplementation
end

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

2

Ho esteso un po 'la risposta di Carlosayam per i miei bisogni aggiuntivi. Questo aggiunge un paio di ulteriori implementazioni e opzioni alla classe Interface: required_variablee optional_variableche supporta un valore predefinito.

Non sono sicuro che vorresti usare questa meta programmazione con qualcosa di troppo grande.

Come hanno affermato altre risposte, è meglio scrivere test che applichino correttamente ciò che stai cercando, soprattutto una volta che desideri iniziare a applicare i parametri e restituire i valori.

Avvertenza, questo metodo genera un errore solo alla chiamata del codice. I test sarebbero ancora necessari per una corretta applicazione prima del runtime.

Esempio di codice

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

Ho usato la libreria singleton per il modello dato che sto utilizzando. In questo modo tutte le sottoclassi ereditano la libreria singleton quando implementano questa "interfaccia".

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

Per le mie esigenze ciò richiede che la classe che implementa l '"interfaccia" la sottoclasse.

class MyPlugin < Plugin

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

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

2

Ruby stesso non ha un equivalente esatto alle interfacce in Java.

Tuttavia, poiché tale interfaccia a volte può essere molto utile, ho sviluppato io stesso una gemma per Ruby, che emula le interfacce Java in un modo molto semplice.

Si chiama class_interface.

Funziona abbastanza semplicemente. Prima installa la gemma gem install class_interfaceo aggiungila al tuo Gemfile e rund bundle install.

Definizione di un'interfaccia:

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

Implementazione di tale interfaccia:

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

Se non si implementa una certa costante o metodo o il numero del parametro non corrisponde, verrà generato un errore corrispondente prima che il programma Ruby venga eseguito. È anche possibile determinare il tipo delle costanti assegnando un tipo nell'interfaccia. Se nullo, è consentito qualsiasi tipo.

Il metodo "implementa" deve essere chiamato nell'ultima riga di una classe, perché quella è la posizione del codice in cui i metodi implementati sopra sono già controllati.

Maggiori informazioni su: https://github.com/magynhard/class_interface


0

Mi sono reso conto che stavo usando troppo il pattern "Errore non implementato" per i controlli di sicurezza sugli oggetti che volevo un comportamento specifico. Ho finito per scrivere una gemma che fondamentalmente permette di utilizzare un'interfaccia come questa:

require 'playable' 

class Instrument 
  implements Playable
end

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

Non controlla gli argomenti del metodo . Funziona a partire dalla versione 0.2.0. Esempio più dettagliato su https://github.com/bluegod/rint

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.