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?
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
?
element
est dans la collectionEt 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
.
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
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 :)
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
.
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.
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.
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
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.
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.
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
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