Can we expose interfaces in Ruby like we do in java and enforce the Ruby modules or classes to implement the methods defined by interface.
One way is to use inheritance and method_missing to achieve the same but is there any other more appro开发者_运维百科priate approach available ?
Ruby has Interfaces just like any other language.
Note that you have to be careful not to conflate the concept of the Interface, which is an abstract specification of the responsibilities, guarantees and protocols of a unit with the concept of the interface
which is a keyword in the Java, C# and VB.NET programming languages. In Ruby, we use the former all the time, but the latter simply doesn't exist.
It is very important to distinguish the two. What's important is the Interface, not the interface
. The interface
tells you pretty much nothing useful. Nothing demonstrates this better than the marker interfaces in Java, which are interfaces that have no members at all: just take a look at java.io.Serializable
and java.lang.Cloneable
; those two interface
s mean very different things, yet they have the exact same signature.
So, if two interface
s that mean different things, have the same signature, what exactly is the interface
even guaranteeing you?
Another good example:
package java.util;
interface List<E> implements Collection<E>, Iterable<E> {
void add(int index, E element)
throws UnsupportedOperationException, ClassCastException,
NullPointerException, IllegalArgumentException,
IndexOutOfBoundsException;
}
What is the Interface of java.util.List<E>.add
?
- that the length of the collection does not decrease
- that all the items that were in the collection before are still there
- that
element
is in the collection
And which of those actually shows up in the interface
? None! There is nothing in the interface
that says that the Add
method must even add at all, it might just as well remove an element from the collection.
This is a perfectly valid implementation of that interface
:
class MyCollection<E> implements java.util.List<E> {
void add(int index, E element)
throws UnsupportedOperationException, ClassCastException,
NullPointerException, IllegalArgumentException,
IndexOutOfBoundsException {
remove(element);
}
}
Another example: where in java.util.Set<E>
does it actually say that it is, you know, a set? Nowhere! Or more precisely, in the documentation. In English.
In pretty much all cases of interfaces
, both from Java and .NET, all the relevant information is actually in the docs, not in the types. So, if the types don't tell you anything interesting anyway, why keep them at all? Why not stick just to documentation? And that's exactly what Ruby does.
Note that there are other languages in which the Interface can actually be described in a meaningful way. However, those languages typically don't call the construct which describes the Interface "interface
", they call it type
. In a dependently-typed programming language, you can, for example, express the properties that a sort
function returns a collection of the same length as the original, that every element which is in the original is also in the sorted collection and that no bigger element appears before a smaller element.
So, in short: Ruby does not have an equivalent to a Java interface
. It does, however, have an equivalent to a Java Interface, and it's exactly the same as in Java: documentation.
Also, just like in Java, Acceptance Tests can be used to specify Interfaces as well.
In particular, in Ruby, the Interface of an object is determined by what it can do, not what class
it is, or what module
it mixes in. Any object that has a <<
method can be appended to. This is very useful in unit tests, where you can simply pass in an Array
or a String
instead of a more complicated Logger
, even though Array
and Logger
do not share an explicit interface
apart from the fact that they both have a method called <<
.
Another example is StringIO
, which implements the same Interface as IO
and thus a large portion of the Interface of File
, but without sharing any common ancestor besides Object
.
Ruby 3 (2021)
Ruby 3.0 introduced a type system called RBS, which supports interfaces.
interface _IntegerConvertible
def to_int: () -> Integer
end
Source: https://blog.appsignal.com/2021/01/27/rbs-the-new-ruby-3-typing-language-in-action.html
Sorbet (2020)
Stripe built a static type checker called Sorbet, which supports interfaces. See Abstract Classes and Interfaces in the sorbet docs.
RSpec (Original answer, 2012)
Try rspec's "shared examples":
https://www.relishapp.com/rspec/rspec-core/v/3-5/docs/example-groups/shared-examples
You write a spec for your interface and then put one line in each implementer's spec, eg.
it_behaves_like "my interface"
Complete example:
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
Can we expose interfaces in Ruby like we do in java and enforce the Ruby modules or classes to implement the methods defined by interface.
Ruby does not have that functionality. In principle, it does not need them as Ruby uses what is called duck typing.
There are few approaches you can take.
Write implementations that raise exceptions; if a subclass attempts to use the unimplemented method, it will fail
class CollectionInterface
def add(something)
raise 'not implemented'
end
end
Along with above, you should write testing code that enforces your contracts (what other post here incorrectly call Interface)
If you find yourself writing void methods like above all the time, then write a helper module that captures that
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
Now, combine the above with Ruby modules and you are close to what you want...
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
And then you can do
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
Let me emphasise once again: this is a rudimentary, as everything in Ruby happens at runtime; there is no compile time checking. If you couple this with testing, then you should be able to pick up errors. Even further, if you take the above further, you could probably be able to write an Interface that performs checking on the class first time an object of that class is created; making your tests as simple as calling MyCollection.new
... yeah, over the top :)
As everyone here said, there is no interface system for ruby. But through introspection, you can implement it yourself quite easily. Here is a simple example that can be improved in many ways to help you get started:
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
Removing one of the methods declared on Person or change it number of arguments will raise a NotImplementedError
.
As many answers indicate, there is no way in Ruby to force a class to implement a specific method, by inheriting from a class, including a module or anything similar. The reason for that is probably the prevalence of TDD in Ruby community, which is a different way of defining the interface - the tests not only specify the signatures of the methods, but also the behavior. Thus if you want to implement a different class, that implements some already defined interface, you have to make sure that all the tests pass.
Usually the tests are defined in isolation using mocks and stubs. But there are also tools like Bogus, allowing for defining contract tests. Such tests not only define the behavior of the "primary" class, but also check that the stubbed methods exist in the cooperating classes.
If you are really concerned with interfaces in Ruby I would recommend using a testing framework that implements contract testing.
There are no such things as interfaces in the Java way. But there are other things you can enjoy in ruby.
If you want to implement some kind of types and interface - so that the objects can be checked whether they has some methods/messages you require from them -, you can then take a look at rubycontracts. It defines a mechanism similar to the PyProtocols. A blog about type checking in ruby is here.
The mentioned approached are not living projects, although the goal seems to be nice at first, it seems that most of the ruby developers can live without strict type checking. But the flexibility of ruby enables to implement type checking.
If you want to extend objects or classes (the same thing in ruby) by certain behaviors or somewhat have the ruby way of multiple inheritance, use the include
or extend
mechanism. With include
you can include methods from another class or module into an object. With extend
you can add behavior to a class, so that its instances will have the added methods. That was a very short explanation though.
I my opinion the best way to resolve the Java interface need is to understand the ruby object model (see Dave Thomas lectures for instance). Probably you will forget about Java interfaces. Or you have an exceptional application on your schedule.
All examples here are interesting but missing the validation of the Interface contract, I mean if you want your object implement all Interface methods definition and only this ones you can't. So I propose you a quick simple example (can be improved for sure) for ensure you have exactly what you expect to have through your Interface (The contract).
consider your Interface with the defined methods like that
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
Then you can write a object with at least the Interface contract:
class FooImplementation
def foo
puts('foo')
end
def bar
puts('bar')
end
end
You can call your Object safely through your Interface for ensuring you are exactly what the Interface define
# > FooInterface.new(FooImplementation.new).foo
# => foo
# > FooInterface.new(FooImplementation.new).bar
# => FooInterface::NotDefinedMethod: bar doesn't belong to Interface definition
And you can as well ensure your Object implement all your Interface methods definition
class BadFooImplementation
end
# > FooInterface.new(BadFooImplementation.new)
# => NotImplementedError: BadFooImplementation must implement the method foo
I've extended a bit on carlosayam's answer for my additional needs. This adds a couple additional enforcements and options to the Interface class: required_variable
and optional_variable
which supports a default value.
I'm not sure that you would want to use this meta programming with something too large.
As other answers have stated, you're best off writing tests that properly enforce what you're looking for, especially once you want to start enforcing parameters and return values.
Caveat this method only throws an error upon the calling of the code. Tests would still be required for proper enforcement before runtime.
Code Example
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
I used the singleton library for the given pattern I am utilizing. This way any subclasses inherit the singleton library when implementing this "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
For my needs this requires that the class implementing the "interface" subclasses it.
class MyPlugin < Plugin
@name = 'My Plugin'
@description = 'I am a plugin'
@safe = true
def self.run
puts 'Do Stuff™'
end
end
Ruby itself has no exact equivalent to interfaces in Java.
However, since such an interface can sometimes be very useful, I developed a gem for Ruby myself, which emulates Java interfaces in a very simple way.
It's called class_interface
.
It works quite simply. First install the gem by gem install class_interface
or add it to your Gemfile and rund bundle install
.
Defining an 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
Implementing that 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
If you don't implement a certain constant or method or the parameter number doesn't match, a corresponding error will be raised before the Ruby program is executed. You can even determine the type of the constants by assigning a type in the interface. If nil, any type is allowed.
The "implements" method must be called at the last line of a class, because that is the code position where the implemented methods above are checked already.
More at: https://github.com/magynhard/class_interface
I realised that I was using the pattern "Not implemented error" too much for safety checks on objects that I wanted a specific behaviour. Ended up writing a gem that basically allows to use an interface like this:
require 'playable'
class Instrument
implements Playable
end
Instrument.new #will throw: Interface::Error::NotImplementedError: Expected Instrument to implement play for interface Playable
It does not check for method arguments. It does as of version 0.2.0
.
More detailed example at https://github.com/bluegod/rint
精彩评论