我们可以像在java中那样公开Ruby中的接口,并强制执行Ruby模块或类来实现接口定义的方法。
一种方法是使用继承和method_missing来实现相同的目标,但还有其他更合适的方法吗?
答案 0 :(得分:82)
Ruby拥有接口,就像任何其他语言一样。
请注意,您必须小心不要混淆 Interface 的概念,这是一个单位的责任,保证和协议的抽象规范,具有{{1}的概念。这是Java,C#和VB.NET编程语言中的关键字。在Ruby中,我们一直使用前者,但后者根本就不存在。
区分这两者非常重要。重要的是接口,而不是interface
。 interface
告诉你几乎没什么用处。没有什么能比Java中的标记接口更好地证明这一点,它们是完全没有成员的接口:只需看看java.io.Serializable
和java.lang.Cloneable
;这两个interface
意味着非常不同的东西,但它们具有完全相同的签名。
那么,如果两个interface
表示不同的东西,具有相同的签名,那么完全是interface
甚至可以保证你吗?
另一个好例子:
interface
package java.util;
interface List<E> implements Collection<E>, Iterable<E> {
void add(int index, E element)
throws UnsupportedOperationException, ClassCastException,
NullPointerException, IllegalArgumentException,
IndexOutOfBoundsException;
}
的接口是什么?
java.util.List<E>.add
在集合哪些实际出现在element
?没有! interface
中没有任何内容表明interface
方法甚至必须添加,它也可能从中移除元素集合。
这是Add
的完全有效的实现:
interface
另一个例子:在java.util.Set<E>
中,它实际上是说它是集吗?无处!或者更确切地说,在文档中。用英语。
在几乎所有来自Java和.NET的class MyCollection<E> implements java.util.List<E> {
void add(int index, E element)
throws UnsupportedOperationException, ClassCastException,
NullPointerException, IllegalArgumentException,
IndexOutOfBoundsException {
remove(element);
}
}
的情况下,所有相关的信息实际上都在文档中,而不是在类型中。那么,如果类型不告诉你任何有趣的东西,为什么要保留它们呢?为什么不坚持文档?而这正是Ruby所做的。
请注意,其他语言中可以实际以有意义的方式描述接口。但是,这些语言通常不会调用描述接口“interfaces
”的构造,他们称之为interface
。例如,在依赖类型的编程语言中,您可以表达type
函数返回与原始函数长度相同的集合的属性,即原始函数中的每个元素也在已排序的集合中并且在较小的元素之前不会出现更大的元素。
因此,简而言之:Ruby没有Java sort
的等价物。然而,它 具有与Java Interface 相同的功能,它与Java中的完全相同:文档。
此外,就像在Java中一样, Acceptance Tests 也可用于指定 Interfaces 。
特别是在Ruby中,对象的接口取决于它可以做什么,而不是interface
是什么,或者是什么{{ 1}}它混入。任何具有class
方法的对象都可以附加到。这在单元测试中非常有用,您可以简单地传入module
或<<
而不是更复杂的Array
,即使String
和{{1}除了他们都有一个名为Logger
的方法之外,不要共享一个明确的Array
。
另一个示例是StringIO
,它实现与Logger
相同的接口,因此interface
的接口的很大一部分除了<<
之外,没有共享任何共同的祖先。
答案 1 :(得分:45)
尝试rspec的“共享示例”:
https://www.relishapp.com/rspec/rspec-core/v/3-5/docs/example-groups/shared-examples
您为您的界面编写规范,然后在每个实施者的规范中加上一行,例如。
it_behaves_like "my interface"
完整示例:
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
答案 2 :(得分:34)
我们可以像在java中那样公开Ruby中的接口吗?强制执行 Ruby模块或类来实现接口定义的方法。
Ruby没有这个功能。原则上,它不需要它们,因为Ruby使用了所谓的duck typing。
您可以采取的方法很少。
编写引发异常的实现;如果一个子类试图使用未实现的方法,它将失败
class CollectionInterface
def add(something)
raise 'not implemented'
end
end
除此之外,你应该编写执行合同的测试代码(这里的其他帖子错误地称之为接口)
如果您发现自己一直在编写类似上面的void方法,那么编写一个捕获该模块的辅助模块
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
现在,将上面的内容与Ruby模块结合起来,你就可以得到你想要的了......
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
然后你可以做
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
让我再次强调:这是一个基本的,因为Ruby中的所有内容都在运行时发生;没有编译时间检查。如果您将此与测试相结合,那么您应该能够发现错误。更进一步,如果你进一步采用上述内容,你或许可以编写一个接口,它在第一次创建该类的对象时执行对类的检查;让你的测试像调用MyCollection.new
一样简单......是的,超过顶部:)
答案 3 :(得分:9)
正如大家所说,红宝石没有接口系统。但是通过内省,你可以很容易地自己实现它。这是一个简单的例子,可以通过多种方式进行改进,以帮助您入门:
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
删除在Person上声明的方法之一或更改其参数数量将引发NotImplementedError
。
答案 4 :(得分:4)
Java方式中没有接口这样的东西。但是你可以在红宝石中享受其他的东西。
如果要实现某种类型和接口 - 以便可以检查对象是否有他们需要的某些方法/消息 - 那么您可以查看rubycontracts。它定义了一种类似于PyProtocols的机制。关于ruby中的类型检查的博客是here。
上面提到的不是生活项目,虽然一开始目标看起来不错,但似乎大多数红宝石开发人员都可以在没有严格类型检查的情况下生活。但ruby的灵活性使得能够实现类型检查。
如果您想通过某些行为扩展对象或类(在ruby中相同的东西)或者有多重继承的ruby方式,请使用include
或extend
机制。使用include
,您可以将另一个类或模块中的方法包含到对象中。使用extend
,您可以向类添加行为,以便其实例具有添加的方法。这是一个非常简短的解释。
我认为解决Java接口需求的最佳方法是了解ruby对象模型(例如,参见Dave Thomas lectures)。可能你会忘记Java接口。或者你的日程表上有一个特殊的应用程序。
答案 5 :(得分:4)
正如许多答案所表明的那样,Ruby无法通过继承类(包括模块或类似的东西)来强制类实现特定的方法。其原因可能是Ruby社区中TDD的普遍存在,这是定义接口的一种不同方式 - 测试不仅指定了方法的签名,还指定了行为。因此,如果要实现一个实现某些已定义接口的不同类,则必须确保所有测试都通过。
通常,测试是使用模拟和存根单独定义的。但也有像Bogus这样的工具,允许定义合同测试。这些测试不仅定义了&#34;主要&#34;的行为。 class,还要检查协作类中是否存在stubbed方法。
如果您真的关心Ruby中的接口,我建议使用实现合同测试的测试框架。
答案 6 :(得分:3)
这里的所有示例都很有趣,但是缺少接口契约的验证,我的意思是如果你希望你的对象实现所有的接口方法定义,而只有你不能这样做。所以我建议你一个简单的例子(可以肯定地改进),以确保你有完全符合你期望通过你的接口(合同)。
使用已定义的方法(例如
)考虑您的界面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
然后你可以编写一个至少包含接口契约的对象:
class FooImplementation
def foo
puts('foo')
end
def bar
puts('bar')
end
end
您可以通过界面安全地调用对象,以确保您完全符合界面的定义
# > FooInterface.new(FooImplementation.new).foo
# => foo
# > FooInterface.new(FooImplementation.new).bar
# => FooInterface::NotDefinedMethod: bar doesn't belong to Interface definition
您还可以确保您的Object实现所有的接口方法定义
class BadFooImplementation
end
# > FooInterface.new(BadFooImplementation.new)
# => NotImplementedError: BadFooImplementation must implement the method foo
答案 7 :(得分:2)
我已经对carlosayam的答案进行了一些扩展,以满足我的额外需求。这为Interface类添加了一些额外的强制执行和选项:required_variable
和optional_variable
,它们支持默认值。
我不确定你是否想要将这个元编程用于太大的东西。
正如其他答案所述,你最好编写能够正确执行所需内容的测试,特别是一旦你想开始强制执行参数并返回值。
警告此方法仅在调用代码时抛出错误。在运行之前,仍然需要进行测试才能正确执行。
<强> 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 强>
我使用单例库来处理我正在使用的给定模式。这样,任何子类在实现此“接口”时都会继承单例库。
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 强>
根据我的需要,这要求实现“interface”的类将其子类化。
class MyPlugin < Plugin
@name = 'My Plugin'
@description = 'I am a plugin'
@safe = true
def self.run
puts 'Do Stuff™'
end
end
答案 8 :(得分:2)
Ruby本身与Java接口不完全相同。
但是,由于这样的接口有时可能非常有用,所以我自己为Ruby开发了gem,它以非常简单的方式模拟Java接口。
它叫class_interface
。
它非常简单。首先通过gem install class_interface
安装gem或将其添加到您的Gemfile并运行bundle install
。
定义界面:
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
实现该界面:
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
如果您未实现某个常量或方法,或者参数编号不匹配,则在执行Ruby程序之前会引发相应的错误。您甚至可以通过在接口中分配类型来确定常量的类型。如果为nil,则允许任何类型。
必须在类的最后一行调用“ implements”方法,因为这是上面已实现的方法已被检查的代码位置。
答案 9 :(得分:0)
我意识到我正在使用模式&#34;未实现错误&#34;对于我想要特定行为的对象的安全检查太多了。结束编写一个基本上允许使用这样的界面的宝石:
require 'playable'
class Instrument
implements Playable
end
Instrument.new #will throw: Interface::Error::NotImplementedError: Expected Instrument to implement play for interface Playable
它不检查方法参数。它的版本为0.2.0
。
https://github.com/bluegod/rint