web-dev-qa-db-fra.com

Qu'est-ce que Java équivalent d'interface dans Ruby?

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

Une façon consiste à utiliser l'héritage et method_missing pour y parvenir, mais existe-t-il une autre approche plus appropriée?

91
crazycrv

Ruby a des interfaces comme n'importe quelle autre langue.

Notez que vous devez faire attention à ne pas confondre le concept de l'interface , qui est une spécification abstraite des responsabilités, garanties et protocoles d'une unité avec le concept de interface qui est un mot-clé dans les langages de programmation Java, C # et VB.NET. Dans Ruby, nous utilisons tout 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 interface. Le interface ne vous dit pratiquement rien d'utile. Rien ne le démontre mieux que les interfaces de marqueur en Java, qui sont des interfaces sans aucun membre: jetez un œil à Java.io.Serializable et Java.lang.Cloneable ; ces deux interface signifient des choses très très différentes, mais elles ont exactement les mêmes signature.

Donc, si deux interface qui signifient des choses différentes, ont la même signature, quel exactement est le interface pair vous garantissant?

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 éléments qui étaient dans la collection avant sont toujours là
  • que element est dans la collection

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

Ceci est une implémentation parfaitement valide de cette 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ù dans Java.util.Set<E> cela signifie-t-il réellement que c'est, vous savez, un ensemble ? Nulle part! Ou plus précisément, dans la documentation. En anglais.

Dans à peu près tous les cas de interfaces, à la fois de Java et .NET, toutes les informations pertinentes sont en fait dans le docs, 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 langues dans lesquelles l'interface peut réellement être décrite d'une 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 de type dépendant, vous pouvez, par exemple, exprimer les propriétés qu'une fonction sort renvoie une collection de la même longueur que l'original, que chaque élément qui se trouve dans l'original est également dans le tri et qu'aucun élément plus grand n'apparaît devant un élément plus petit.

Donc, en bref: Ruby n'a pas d'équivalent à un Java interface. Il possède , cependant, a un équivalent à une interface Java , et c'est exactement la même chose qu'en Java: documentation.

De même, tout comme en Java, Les tests d'acceptation peuvent être utilisés pour spécifier Interfaces ainsi que.

En particulier, dans Ruby, l'interface d'un objet est déterminée par ce qu'il peut faire , pas ce que class est, ni ce que module il 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 Array ou un String au lieu d'un Logger plus compliqué, même si Array et Logger ne partage pas de interface explicite à part le fait qu'ils ont tous deux une méthode appelée <<.

Un autre exemple est StringIO , qui implémente la même interface que IO et donc un grande partie de l'interface de File, mais sans partager aucun ancêtre commun à part Object.

83
Jörg W Mittag

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
47
Jared Beck

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

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

Il existe peu d'approches que vous pouvez adopter.

Écrire 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

En plus de ce qui précède, vous devez écrire du code de test qui applique vos contrats (ce que les autres articles ici appellent incorrectement Interface)

Si vous vous retrouvez à écrire des méthodes nulles comme par-dessus tout le temps, alors é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 vous pouvez 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 une fois de plus: c'est un élément rudimentaire, car tout dans Ruby se produit à l'exécution; il n'y a pas de vérification de l'heure de compilation. Si vous associez cela à des tests, vous devriez être en mesure de reprendre 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 :)

37
carlosayam

Comme tout le monde ici l'a dit, 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 plusieurs façons 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 soulèvera un NotImplementedError.

9
fotanus

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 ne spécifient pas seulement la signatures des méthodes, mais aussi du comportement. Donc si vous voulez implémenter une classe différente, qui implémente une interface déjà définie, vous devez vous assurer que tous les tests passent.

Habituellement, les tests sont définis isolément à l'aide de simulacres et de talons. Mais il existe également des outils comme Bogus , permettant de définir des tests de contrat. 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 concerné par les interfaces dans Ruby je recommanderais d'utiliser un framework de test qui implémente les tests de contrat.

5
Aleksander Pohl

Il n'y a pas d'interfaces de la manière Java. Mais il y a d'autres choses que vous pouvez apprécier dans Ruby.

Si vous souhaitez implémenter une sorte de types et d'interfaces - afin que les objets puissent vérifier s'ils contiennent des méthodes/messages dont vous avez besoin -, vous pouvez alors jeter un œil à rubycontracts . Il définit un mécanisme similaire à PyProtocols . Un blog sur la saisie de type Ruby est ici .

Les approches mentionnées ne sont pas des projets vivants, bien que l'objectif semble être agréable au début, il semble que la plupart des développeurs Ruby peuvent vivre sans vérification de type stricte. Mais la flexibilité de Ruby permet d'implémenter la vérification de type.

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

Je pense que la meilleure façon de résoudre le Java besoin d'interface est de comprendre le Ruby modèle d'objet (voir conférences de Dave Thomas pour Par exemple, vous oublierez probablement les interfaces Java. Ou vous avez une application exceptionnelle à votre horaire.

5
fifigyuri

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 (qui peut être amélioré à coup sûr) pour vous assurer d'avoir exactement ce que vous attendez de 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 [email protected]_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 d'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 définit l'interface

#  > 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 objet 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
3
Joel AZEMAR

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 développé un joyau pour Ruby moi-même, qui émule Java s'interface de manière très simple).

C'est appelé class_interface.

Cela fonctionne tout simplement. Installez d'abord la gemme par gem install class_interface ou ajoutez-le à votre Gemfile et à votre 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 de la constantes en affectant un type dans l'interface. Si nil, 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 sur: https://github.com/magynhard/class_interface

2
magynhard

J'ai étendu un peu la réponse de carlosayam à mes besoins supplémentaires. Cela ajoute quelques mises en œuvre et options supplémentaires à la classe Interface: required_variable et optional_variable qui prend en charge une valeur par défaut.

Je ne suis pas sûr que vous souhaitiez utiliser cette méta-programmation 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 une fois que vous souhaitez commencer à appliquer les paramètres et renvoyer des valeurs.

Caveat cette méthode ne génère qu'une erreur lors de l'appel du code. Des tests seraient toujours requis 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 motif 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

my_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
2
CTS_AE

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 pour lesquels 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 le fait à partir de la version 0.2.0. Exemple plus détaillé sur https://github.com/bluegod/rint

0
BLuEGoD