Qu'est-ce que l'interface Java équivalente dans Ruby?

105

Pouvons-nous exposer des interfaces dans Ruby comme nous le faisons en java et appliquer les modules ou classes Ruby pour implémenter les méthodes définies par interface.

Une façon est d'utiliser l'héritage et method_missing pour obtenir la même chose, mais existe-t-il une autre approche plus appropriée disponible?

crazycrv
la source
1
Cliquez sur cette url lawrencesong.net/2008/01/implement-java-interface-in-ruby
Sankar Ganesh
6
Vous devriez vous demander pourquoi vous en avez même besoin. Souvent, assez d'interfaces sont utilisées juste pour faire compiler quelque chose, ce qui n'est pas un problème dans ruby.
Arnis Lapsa
1
Cette question peut ou non être considérée comme un doublon de [ Dans Ruby, qu'est-ce que l'équivalent d'une interface en C #? ] ( StackOverflow.Com/q/3505521/#3507460 ).
Jörg W Mittag
2
Pourquoi j'en ai besoin? Je veux implémenter quelque chose que vous pouvez appeler comme "versionnable" qui rend les documents / fichiers versionnables mais versionnables en utilisant quoi .... Par exemple, je peux le rendre versionnable en utilisant des logiciels de référentiel existants comme SVN ou CVS. Quel que soit le mécanisme sous-jacent que je choisis, il devrait fournir des fonctions minimales de base. Je souhaite utiliser une interface similaire pour imposer l'implémentation de ces fonctions minimales par toute nouvelle implémentation de référentiel sous-jacent.
crazycrv
Sandi Metz dans son livre POODR utilise des tests pour documenter les interfaces. Cela vaut vraiment la peine de lire ce livre. À partir de 2015, je dirais que la réponse @ aleksander-pohl est la meilleure.
Greg Dan

Réponses:

85

Ruby a des interfaces comme n'importe quel autre langage.

Notez qu'il faut faire attention à ne pas confondre le concept d' Interface , qui est une spécification abstraite des responsabilités, garanties et protocoles d'une unité avec le concept de interfacequi est un mot-clé dans la programmation Java, C # et VB.NET langues. Dans Ruby, nous utilisons le premier tout le temps, mais le second n'existe tout simplement pas.

Il est très important de distinguer les deux. Ce qui est important, c'est l' interface , pas le interface. Le ne interfacevous dit quasiment rien d'utile. Rien ne le démontre mieux que les interfaces de marqueurs en Java, qui sont des interfaces qui n'ont aucun membre: il suffit de jeter un œil à java.io.Serializableet java.lang.Cloneable; ces deux interfacesignifient des choses très différentes, mais ils ont exactement la même signature.

Donc, si deux interfaces que les choses moyennes différentes, ont la même signature, ce exactement est la interfacemême vous garantir?

Un autre bon exemple:

package java.util;

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

Quelle est l' interface de java.util.List<E>.add?

  • que la longueur de la collection ne diminue pas
  • que tous les objets qui étaient dans la collection avant sont toujours là
  • qui elementest dans la collection

Et lequel de ceux-ci apparaît réellement dans le interface? Aucun! Il n'y a rien dans le interfacequi dit que la Addméthode doit même ajouter du tout, elle pourrait tout aussi bien supprimer un élément de la collection.

C'est une implémentation parfaitement valide de cela interface:

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

Un autre exemple: où java.util.Set<E>est-il dit qu'il s'agit, vous savez, d'un ensemble ? Nulle part! Ou plus précisément, dans la documentation. En anglais.

Dans presque tous les cas interfaces, à la fois à partir de Java et .NET, toutes les informations pertinentes se trouvent en fait dans la documentation, pas dans les types. Donc, si les types ne vous disent rien d'intéressant de toute façon, pourquoi les garder du tout? Pourquoi ne pas s'en tenir uniquement à la documentation? Et c'est exactement ce que fait Ruby.

Notez qu'il existe d' autres langages dans lesquels l' interface peut en fait être décrite de manière significative. Cependant, ces langages n'appellent généralement pas la construction qui décrit l' interface " interface", ils l'appellent type. Dans un langage de programmation à typage dépendant, vous pouvez, par exemple, exprimer les propriétés qu'une sortfonction renvoie une collection de la même longueur que l'original, que chaque élément qui est dans l'original est également dans la collection triée et qu'aucun élément plus grand apparaît devant un élément plus petit.

Donc, en bref: Ruby n'a pas d'équivalent à un Java interface. Il ne , cependant, ont un équivalent à Java Interface , et il est exactement le même que dans Java: documentation.

De plus, tout comme en Java, les tests d'acceptation peuvent également être utilisés pour spécifier des interfaces .

En particulier, dans Ruby, l' interface d'un objet est déterminée par ce qu'il peut faire , et non par ce qu'il classest, ou ce moduledans quoi il se mélange. Tout objet qui a une <<méthode peut être ajouté. Ceci est très utile dans les tests unitaires, où vous pouvez simplement passer un Arrayou un Stringau lieu d'un plus compliqué Logger, même si Arrayet Loggerne partagez pas un explicite mis interfaceà part le fait qu'ils ont tous les deux une méthode appelée <<.

Un autre exemple est StringIO, qui implémente la même Interface que IOet donc une grande partie de l' Interface de File, mais sans partager d'ailleurs aucun ancêtre commun Object.

Jörg W Mittag
la source
282
Bien qu'une bonne lecture, je ne trouve pas la réponse aussi utile. Il se lit comme une dissertation sur pourquoi interfaceest inutile, manquant le point de son utilisation. Il aurait été plus facile de dire que ruby ​​est typé dynamiquement et qu'il a un objectif différent à l'esprit et rend les concepts comme IOC inutiles / indésirables. C'est un changement difficile si vous êtes habitué à la conception par contrat. Quelque chose dont Rails pourrait bénéficier, ce que l'équipe de base a réalisé comme vous pouvez le voir sur les dernières versions.
goliatone
12
Question complémentaire: quelle est la meilleure façon de documenter une interface dans Ruby? Un mot interface- clé Java peut ne pas fournir toutes les informations pertinentes, mais il fournit un endroit évident pour mettre la documentation. J'ai écrit une classe en Ruby qui implémente (assez de) IO, mais je l'ai fait par essais et erreurs et je n'étais pas trop satisfait du processus. J'ai également écrit plusieurs implémentations de ma propre interface, mais documenter les méthodes requises et ce qu'elles sont censées faire pour que les autres membres de mon équipe puissent créer des implémentations s'est avéré un défi.
Patrick
9
La interface construction n'est en effet nécessaire que pour traiter différents types de la même manière dans les langages à héritage unique à typage statique (par exemple, traiter LinkedHashSetet les ArrayListdeux comme a Collection), elle n'a pratiquement rien à voir avec Interface comme le montre cette réponse. Ruby n'est pas typé statiquement donc il n'y a pas besoin de la construction .
Esailija
16
J'ai lu ceci comme "certaines interfaces n'ont aucun sens, donc les interfaces sont mauvaises. Pourquoi voudriez-vous utiliser des interfaces?". Cela ne répond pas à la question et ressemble franchement à quelqu'un qui ne comprend pas à quoi servent les interfaces et leur avantage.
Oddman
13
Votre argument sur l'invalidité de l'interface List en citant une méthode qui effectue une suppression dans une fonction appelée "add" est un exemple classique d'argument reductio ad absurdum. En particulier, il est possible dans n'importe quel langage (rubis inclus) d'écrire une méthode qui fait quelque chose de différent de ce qui est attendu. Ce n'est pas un argument valable contre "interface", c'est juste un mauvais code.
Justin Ohms
61

Essayez les "exemples partagés" de rspec:

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

Vous écrivez une spécification pour votre interface, puis mettez une ligne dans la spécification de chaque implémenteur, par exemple.

it_behaves_like "my interface"

Exemple complet:

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

Mise à jour : Huit ans plus tard (2020), ruby ​​prend désormais en charge les interfaces de type statique via sorbet. Voir Classes et interfaces abstraites dans la documentation sorbet.

Jared Beck
la source
17
Je pense que cela devrait être la réponse acceptée. C'est ainsi que la plupart des langages faibles de type peuvent fournir des interfaces de type Java. L'accepté explique pourquoi Ruby n'a pas d'interfaces, pas comment les émuler.
SystematicFrank
1
Je suis d'accord, cette réponse m'a beaucoup plus aidé en tant que développeur Java passant à Ruby que la réponse acceptée ci-dessus.
Cam
Oui, mais l'intérêt d'une interface est qu'elle a les mêmes noms de méthode, mais les classes concrètes doivent être celles qui implémentent le comportement, ce qui est probablement différent. Alors, que suis-je censé tester dans l'exemple partagé?
Rob Wise
Ruby rend tout pragmatique. Si vous souhaitez avoir un code documenté et bien écrit, ajoutez des tests / spécifications et ce sera une sorte de vérification de typage statique.
Dmitry Polushkin
42

Pouvons-nous exposer des interfaces dans Ruby comme nous le faisons en java et appliquer les modules ou classes Ruby pour implémenter les méthodes définies par interface.

Ruby n'a pas cette fonctionnalité. En principe, il n'en a pas besoin car Ruby utilise ce qu'on appelle le typage canard .

Il y a peu d'approches que vous pouvez adopter.

Ecrire des implémentations qui déclenchent des exceptions; si une sous-classe tente d'utiliser la méthode non implémentée, elle échouera

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

Avec ci-dessus, vous devez écrire un code de test qui applique vos contrats (quel autre message ici appelle incorrectement Interface )

Si vous vous retrouvez à écrire des méthodes vides comme ci-dessus tout le temps, écrivez un module d'aide qui capture cela

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

Maintenant, combinez ce qui précède avec les modules Ruby et vous êtes proche de ce que vous voulez ...

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

Et puis tu peux faire

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

Permettez-moi de souligner encore une fois: il s'agit d'un élément rudimentaire, car tout dans Ruby se produit au moment de l'exécution; il n'y a pas de vérification au moment de la compilation. Si vous associez cela à des tests, vous devriez être en mesure de détecter les erreurs. Encore plus loin, si vous allez plus loin, vous pourriez probablement être en mesure d'écrire une interface qui vérifie la classe la première fois qu'un objet de cette classe est créé; rendre vos tests aussi simples que d'appeler MyCollection.new... ouais, par dessus :)

carlosayam
la source
Ok mais si votre Collection = MyCollection implémente une méthode non définie dans l'interface, cela fonctionne parfaitement, vous ne pouvez donc pas vous assurer que votre objet n'a que les définitions des méthodes d'interface.
Joel AZEMAR
C'est assez génial, merci. Le typage Duck est très bien, mais il est parfois bon de communiquer explicitement à d'autres développeurs comment une interface doit se comporter.
Mirodinho
10

Comme tout le monde l'a dit ici, il n'y a pas de système d'interface pour ruby. Mais grâce à l'introspection, vous pouvez l'implémenter vous-même assez facilement. Voici un exemple simple qui peut être amélioré de nombreuses manières pour vous aider à démarrer:

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 suppression d'une des méthodes déclarées sur Person ou la modification du nombre d'arguments lèvera un NotImplementedError.

fotanus
la source
5

Il n'existe pas d'interfaces à la manière Java. Mais il y a d'autres choses que vous pouvez apprécier en rubis.

Si vous souhaitez implémenter une sorte de types et d'interface - afin que les objets puissent être vérifiés s'ils ont des méthodes / messages dont vous avez besoin -, vous pouvez alors jeter un œil à rubycontracts . Il définit un mécanisme similaire aux PyProtocols . Un blog sur l'enregistrement de type ruby ​​est ici .

Les abordés mentionnés ne sont pas des projets vivants, même si l'objectif semble être sympa au début, il semble que la plupart des développeurs ruby ​​puissent vivre sans vérification de type stricte. Mais la flexibilité de ruby ​​permet de mettre en œuvre la vérification de type.

Si vous voulez étendre des objets ou des classes (la même chose dans ruby) par certains comportements ou avoir quelque peu la manière ruby ​​de l'héritage multiple, utilisez le mécanisme includeou extend. Avec, includevous pouvez inclure des méthodes d'une autre classe ou module dans un objet. Avec extendvous pouvez ajouter un comportement à une classe, afin que ses instances aient les méthodes ajoutées. C'était une explication très courte cependant.

Je pense que la meilleure façon de résoudre le besoin d'interface Java est de comprendre le modèle objet ruby ​​(voir les conférences de Dave Thomas par exemple). Vous oublierez probablement les interfaces Java. Ou vous avez une application exceptionnelle sur votre emploi du temps.

fifigyuri
la source
Ces conférences de Dave Thomas sont derrière un paywall.
Purplejacket
5

Comme de nombreuses réponses l'indiquent, il n'y a aucun moyen dans Ruby de forcer une classe à implémenter une méthode spécifique, en héritant d'une classe, y compris un module ou quelque chose de similaire. La raison en est probablement la prévalence du TDD dans la communauté Ruby, qui est une manière différente de définir l'interface - les tests spécifient non seulement les signatures des méthodes, mais aussi le comportement. Ainsi, si vous souhaitez implémenter une classe différente, qui implémente une interface déjà définie, vous devez vous assurer que tous les tests réussissent.

Habituellement, les tests sont définis de manière isolée à l'aide de simulacres et de stubs. Mais il existe aussi des outils comme Bogus , permettant de définir des tests contractuels. De tels tests définissent non seulement le comportement de la classe "primaire", mais vérifient également que les méthodes stubbed existent dans les classes coopérantes.

Si vous êtes vraiment préoccupé par les interfaces dans Ruby, je recommanderais d'utiliser un cadre de test qui implémente les tests de contrat.

Aleksander Pohl
la source
3

Tous les exemples ici sont intéressants mais il manque la validation du contrat d'interface, je veux dire si vous voulez que votre objet implémente toutes les définitions de méthodes d'interface et seulement celles-ci, vous ne pouvez pas. Je vous propose donc un exemple simple et rapide (peut être amélioré à coup sûr) pour vous assurer que vous avez exactement ce que vous attendez d'avoir via votre interface (le contrat).

considérez votre interface avec les méthodes définies comme ça

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

Ensuite, vous pouvez écrire un objet avec au moins le contrat Interface:

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

Vous pouvez appeler votre objet en toute sécurité via votre interface pour vous assurer que vous êtes exactement ce que l'interface définit

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

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

Et vous pouvez également vous assurer que votre Object implémente toutes vos définitions de méthodes d'interface

class BadFooImplementation
end

#  > FooInterface.new(BadFooImplementation.new)
# => NotImplementedError: BadFooImplementation must implement the method foo
Joël AZEMAR
la source
2

J'ai prolongé un peu la réponse de carlosayam pour mes besoins supplémentaires. Cela ajoute quelques applications et options supplémentaires à la classe Interface: required_variableetoptional_variable qui prend en charge une valeur par défaut.

Je ne suis pas sûr que vous souhaitiez utiliser cette métaprogrammation avec quelque chose de trop grand.

Comme d'autres réponses l'ont indiqué, il vaut mieux écrire des tests qui appliquent correctement ce que vous recherchez, en particulier lorsque vous souhaitez commencer à appliquer des paramètres et à renvoyer des valeurs.

Attention, cette méthode ne génère une erreur que lors de l'appel du code. Des tests seraient toujours nécessaires pour une application correcte avant l'exécution.

Exemple de code

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

J'ai utilisé la bibliothèque singleton pour le modèle donné que j'utilise. De cette façon, toutes les sous-classes héritent de la bibliothèque singleton lors de l'implémentation de cette "interface".

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

mon_plugin.rb

Pour mes besoins, cela nécessite que la classe implémentant "l'interface" la sous-classe.

class MyPlugin < Plugin

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

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

Ruby lui-même n'a pas d'équivalent exact aux interfaces en Java.

Cependant, comme une telle interface peut parfois être très utile, j'ai moi-même développé une gemme pour Ruby, qui émule les interfaces Java de manière très simple.

Ça s'appelle class_interface.

Cela fonctionne tout simplement. Installez d'abord la gemme gem install class_interfaceou ajoutez-la à votre Gemfile et rund bundle install.

Définition d'une interface:

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

Implémentation de cette interface:

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

Si vous n'implémentez pas une certaine constante ou méthode ou si le numéro de paramètre ne correspond pas, une erreur correspondante sera générée avant l'exécution du programme Ruby. Vous pouvez même déterminer le type des constantes en attribuant un type dans l'interface. Si nul, tout type est autorisé.

La méthode "implements" doit être appelée à la dernière ligne d'une classe, car c'est la position du code où les méthodes implémentées ci-dessus sont déjà vérifiées.

Plus d'informations sur: https://github.com/magynhard/class_interface

magynhard
la source
0

J'ai réalisé que j'utilisais trop le modèle "Erreur non implémentée" pour les contrôles de sécurité sur les objets dont je voulais un comportement spécifique. J'ai fini par écrire un joyau qui permet essentiellement d'utiliser une interface comme celle-ci:

require 'playable' 

class Instrument 
  implements Playable
end

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

Il ne vérifie pas les arguments de méthode . Il fait à partir de la version 0.2.0. Exemple plus détaillé sur https://github.com/bluegod/rint

BLUEGOD
la source