Ruby和duck打字:合同设计不可能?

时间:2008-10-07 02:56:10

标签: java ruby oop interface design-by-contract

Java中的方法签名:

public List<String> getFilesIn(List<File> directories)

ruby​​中的类似内容

def get_files_in(directories)

对于Java,类型系统向我提供有关方法期望和传递的信息。在Ruby的情况下,我有没有线索我应该传递的内容,或者我希望收到的内容。

在Java中,对象必须正式实现接口。在Ruby中,传入的对象必须响应此处定义的方法中调用的任何方法。

这似乎很成问题:

  1. 即使有100%准确,最新的文档,Ruby代码也必须公开其实现,打破封装。除了“OO纯度”,这似乎是一场维护噩梦。
  2. Ruby代码给了我没有线索返回的内容;我必须基本上进行实验,或者阅读代码以找出返回对象将响应的方法。
  3. 不打算讨论静态打字与鸭子打字,而是希望了解如何维护一个几乎没有能力按合同设计的生产系统。

    更新

    没有人真正通过此方法所需的文档来解决方法内部实现的暴露问题。由于没有接口,如果我不期望某个特定类型,我不必逐条列出我可能调用的每个方法,以便调用者知道可以传入什么内容吗?或者这只是一个没有真正出现的边缘案例?

8 个答案:

答案 0 :(得分:30)

归结为get_files_in在Ruby中是一个坏名称​​ - 让我解释一下。

在java / C#/ C ++中,特别是在目标C中,函数参数是名称的一部分。在红宝石中它们不是。
这个奇特的术语是Method Overloading,它由编译器强制执行。

在这些方面考虑它,你只是定义了一个名为get_files_in的方法,而你实际上并没有说它应该将文件放入什么。参数是不是的一部分这个名字,所以你不能依靠他们来识别它 它应该在目录中获取文件吗?开车?网络共享?这开启了它在上述所有情况下工作的可能性。

如果要将其限制为目录,那么要考虑此信息,您应该调用方法get_files_in_directory。或者,您可以在Directory类上创建一个方法Ruby already does for you

对于返回类型,从get_files暗示您要返回一组文件。您不必担心它是List<File>ArrayList<File&gt;等等,因为每个人都只使用数组(如果他们编写了自定义数组,他们就会写它继承了内置数组。)

如果您只想获得一个文件,则可以将其称为get_fileget_first_file等。如果你正在做一些更复杂的事情,例如返回FileWrapper个对象而不仅仅是字符串,那么有一个非常好的解决方案:

# returns a list of FileWrapper objects
def get_files_in_directory( dir )
end

无论如何。你不能像在java中那样强制执行ruby中的契约,但这是更广泛的一部分,即你不能像在java中那样在ruby中强制执行任何。由于ruby的语法更具表现力,你可以更清楚地编写类似英语的代码,告诉其他人你的合同是什么(其中有几千个尖括号)。

我相信这是一场净胜利。您可以使用新发现的空闲时间来写一些specs and tests,并在一天结束时推出更好的产品。

答案 1 :(得分:8)

我认为虽然Java方法为您提供了更多信息,但它并没有为您提供足够的信息以便轻松编程。
例如,字符串列表只是文件名还是完全限定路径?

鉴于此,您对Ruby没有提供足够信息的论点也适用于Java 您仍然依赖于阅读文档,查看源代码,或调用方法并查看其输出(当然还有不错的测试)。

答案 2 :(得分:6)

虽然我在编写Java代码时喜欢静态类型,但是没有理由不能坚持Ruby代码(或任何类型的代码)的深思熟虑的先决条件。当我真的需要坚持方法参数的前提条件(在Ruby中)时,我很乐意编写一个可能引发运行时异常的条件来警告程序员错误。我甚至通过写作给自己一个静态类型的外表:

def get_files_in(directories)
   unless File.directory? directories
      raise ArgumentError, "directories should be a file directory, you bozo :)"
   end
   # rest of my block
end

在我看来,这种语言无法阻止你按合同进行设计。相反,在我看来,这取决于开发人员。

(顺便说一下,“bozo”真的是指你的:)。

答案 3 :(得分:5)

通过duck-typing验证方法:

i = {}
=> {}
i.methods.sort
=> ["==", "===", "=~", "[]", "[]=", "__id__", "__send__", "all?", "any?", "class", "clear", "clone", "collect", "default", "default=", "default_proc", "delete", "delete_if", "detect", "display", "dup", "each", "each_key", "each_pair", "each_value", "each_with_index", "empty?", "entries", "eql?", "equal?", "extend", "fetch", "find", "find_all", "freeze", "frozen?", "gem", "grep", "has_key?", "has_value?", "hash", "id", "include?", "index", "indexes", "indices", "inject", "inspect", "instance_eval", "instance_of?", "instance_variable_defined?", "instance_variable_get", "instance_variable_set", "instance_variables", "invert", "is_a?", "key?", "keys", "kind_of?", "length", "map", "max", "member?", "merge", "merge!", "method", "methods", "min", "nil?", "object_id", "partition", "private_methods", "protected_methods", "public_methods", "rehash", "reject", "reject!", "replace", "require", "respond_to?", "select", "send", "shift", "singleton_methods", "size", "sort", "sort_by", "store", "taint", "tainted?", "to_a", "to_hash", "to_s", "type", "untaint", "update", "value?", "values", "values_at", "zip"]
i.respond_to?('keys')
=> true
i.respond_to?('get_files_in')  
=> false

一旦你理解了这种推理,方法签名就没有意义,因为你可以动态地在函数中测试它们。 (这部分是由于无法进行基于签名匹配的功能调度,但这更灵活,因为您可以定义无限的签名组合)

 def get_files_in(directories)
    fail "Not a List" unless directories.instance_of?('List')
 end

 def example2( *params ) 
    lists = params.map{|x| (x.instance_of?(List))?x:nil }.compact 
    fail "No list" unless lists.length > 0
    p lists[0] 
 end

x = List.new
get_files_in(x)
example2( 'this', 'should', 'still' , 1,2,3,4,5,'work' , x )

如果您想要更可靠的测试,可以尝试RSpec进行行为驱动的开发。

答案 4 :(得分:3)

简答:自动化单元测试和良好的命名实践。

正确命名方法至关重要。通过将名称get_files_in(directory)提供给方法,您还可以向用户提供有关方法期望获得的内容以及它将返回的内容的提示。例如,我不希望Potato对象来自get_files_in() - 它只是没有意义。只获取文件名列表或更合适的方法,从该方法获取文件实例列表才有意义。至于列表的具体类型,根据您想要做的事情,返回的List的实际类型并不重要。重要的是你可以以某种方式枚举该列表中的项目。

最后,通过针对该方法编写单元测试来明确说明 - 显示它应该如何工作的示例。因此,如果get_files_in突然返回一个马铃薯,测试将引发错误,你会知道最初的假设现在是错误的。

答案 5 :(得分:3)

按合同设计是一个更简单的原则,而不仅仅是将参数类型指定为返回类型。这里的其他答案主要集中于良好的命名,这很重要。我可以继续讨论名称get_files_in含糊不清的许多方法。但良好的命名只是更好的合同和设计的更深层原则的外在结果。名字总是有点含糊不清,良好的语用语言学是良好思考的产物。

您可以将合同视为设计原则,并且通常以抽象的形式陈述难以理解和无聊的陈述。一种无类型的语言要求程序员考虑真实的合同,她将它们理解为更深层次,而不仅仅是类型约束。如果有一个团队,团队成员必须全都意味着并遵守相同的合同。他们必须是专注的思想家,必须花时间在一起讨论具体的例子,以便建立对合同的共同理解。

相同的要求适用于API用户:用户必须首先记住文档,然后她能够逐渐理解合同,并且如果合同经过精心设计(或者如果不这样,则讨厌它)就开始喜欢API。

这与鸭子打字有关。无论方法输入的类型如何,合同必须提供关于发生的事情的线索。因此,必须以更深入,更一般化的方式理解合同。这个答案本身可能看起来有点混乱,甚至傲慢,我为此道歉。我只是想说鸭子是not a lie,鸭子意味着人们在更高的抽象层次上思考一个人的问题。设计师,程序员,数学家都是all different names for the same capability,数学家知道数学中有很多层次的能力,下一个更高层次的数学家很容易解决下层难以解决的问题。鸭子意味着您的编程必须是良好的数学,并且它限制成功的开发人员和用户仅限于那些who are able to do so

答案 6 :(得分:1)

这绝不是维护噩梦,只是另一种工作方式,需要API中的一致性和良好的文档。

您的担忧似乎与以下事实有关:任何动态语言都是一种危险的工具,无法强制执行A​​PI输入/输出合同。事实上,虽然选择静态可能看起来更安全,但在两个世界中你可以做的更好的事情是保持一组良好的测试,不仅验证返回的数据类型(这是Java编译器可以验证的唯一内容,执行),但它的正确性和内部工作(黑盒子/白盒测试)。

作为旁注,我不了解Ruby,但在PHP中,您可以使用@phpdoc标签来暗示IDE(Eclipse PDT)关于某种方法返回的数据类型。

答案 7 :(得分:1)

几年前,我对像dbc for Ruby这样的东西进行了半生不熟的尝试,可能会给人们一些关于如何推进更全面的解决方案的想法:

https://github.com/justinwiley/higher-expectations