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

Java中的方法签名:

public List getFilesIn(List directories) 

类似的ruby

 def get_files_in(directories) 

对于Java,类型系统为我提供了有关该方法所期望和提供的信息。 在Ruby的情况下,我知道我应该传递什么,或者我期望收到什么。

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

这似乎很成问题:

  1. 即使拥有100%准确,最新的文档,Ruby代码也必须公开其实现,打破封装。 除了“OO纯度”,这似乎是一个维护噩梦。
  2. Ruby代码让我知道返回了什么; 我必须基本上进行实验,或者阅读代码以找出返回对象将响应的方法。

不打算讨论静态打字与鸭子打字,而是希望了解如何维护一个几乎没有合同设计能力的生产系统。

更新

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

它归结为get_files_in 在Ruby中是一个坏名字 – 让我解释一下。

在java / C#/ C ++中,特别是在目标C中,函数参数是名称的一部分 。 在ruby中,它们不是。
这个奇特的术语是方法重载 ,它由编译器强制执行。

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

如果要将其限制为目录,那么要考虑此信息,应调用方法get_files_in_directory 。 或者你可以把它作为Directory类的一个方法, Ruby已经为你做了 。

至于返回类型,从get_files暗示你要返回一个文件数组。 您不必担心它是ListArrayList >等,因为每个人都只使用数组(如果他们编写了自定义数组,他们会将其写为inheritance自内置数组)。

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

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

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

我相信这是一场净胜利。 您可以使用新发现的业余时间编写一些规格和测试,并在一天结束时提供更好的产品。

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

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

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

 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 

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

(BTW,“bozo”真的是指你的:)

通过duck-typingvalidation方法:

 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 

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

  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进行行为驱动的开发。

简短回答:自动化unit testing和良好的命名实践。

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

最后,通过针对该方法编写unit testing来明确说明 – 显示它应该如何工作的示例。 因此,如果get_files_in突然返回一个Potato,那么测试将引发错误并且您将知道最初的假设现在是错误的。

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

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

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

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

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

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

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

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

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

Interesting Posts