19

Java中的方法签名:

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

红宝石中的类似

def get_files_in(directories)

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

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

这似乎很有问题:

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

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

更新

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

4

8 回答 8

30

归根结底,这在 Rubyget_files_in中是个坏名字——让我解释一下。

在 java/C#/C++ 中,尤其是在目标 C 中,函数参数是 name 的一部分。在红宝石中它们不是。
对此的花哨术语是Method Overloading,它由编译器强制执行。

以这些术语考虑它,您只是定义了一个调用的方法get_files_in,而实际上并没有说明它应该获取文件的内容。参数不是名称的一部分,因此您不能依赖它们来识别它。
它应该在目录中获取文件吗?驱动器?网络共享?这为它在上述所有情况下工作提供了可能性。

如果您想将其限制在一个目录中,那么要考虑到这些信息,您应该调用该方法get_files_in_directory。或者,您可以将其作为Directory类上的方法,Ruby 已经为您做了

至于返回类型,暗示get_files您正在返回一个文件数组。您不必担心它是 aList<File>ArrayList<File>,等等,因为每个人都只使用数组(如果他们编写了自定义数组,他们将编写它以从内置数组继承)。

如果您只想获取一个文件,则可以调用它get_fileget_first_file以此类推。如果您正在做一些更复杂的事情,例如返回FileWrapper对象而不仅仅是字符串,那么有一个非常好的解决方案:

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

好歹。您不能像在 java 中那样在 ruby​​ 中强制执行合同,但这是更广泛观点的一个子集,即您不能像在 java 中那样在 ruby​​ 中强制执行任何东西。由于 ruby​​ 更具表现力的语法,您可以更清楚地编写类似英语的代码,告诉其他人您的合同是什么(从而为您节省数千个尖括号)。

我个人认为这是一场净胜。您可以利用新获得的空闲时间编写一些规范和测试,并在一天结束时推出更好的产品。

于 2008-10-07T03:26:12.373 回答
8

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

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

于 2008-10-07T09:08:25.147 回答
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”真正指的是你的:)

于 2008-10-07T03:16:35.107 回答
5

通过鸭子类型进行方法验证:

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 for Behaviordriven development。

于 2008-10-07T03:13:10.043 回答
3

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

方法的正确命名是必不可少的。通过为get_files_in(directory)方法命名,您还可以向用户提示该方法期望获得什么以及它将返回什么。例如,我不希望有一个Potato物体从里面出来get_files_in()——它只是没有意义。仅从该方法获取文件名列表或更合适的 File 实例列表才有意义。至于列表的具体类型,取决于你想要做什么,返回的 List 的实际类型并不重要。重要的是您可以以某种方式枚举该列表中的项目。

最后,您可以通过针对该方法编写单元测试来明确这一点——展示它应该如何工作的示例。因此,如果get_files_in突然返回一个 Potato,测试将引发错误,您就会知道最初的假设现在是错误的。

于 2008-10-07T03:42:18.347 回答
3

契约式设计是一个比仅仅指定参数类型和返回类型更微妙的原则。这里的其他答案主要集中在良好的命名上,这很重要。我可以继续讨论这个名字get_files_in模棱两可的许多方面。但是好的命名只是拥有良好合同并由他们设计的更深层次原则的外在结果。名字总是有点模棱两可,好的语用语言学是好的思考的产物。

您可以将合同视为设计原则,而以抽象的形式表述它们通常既困难又乏味。无类型语言要求程序员真正考虑合同,她对合同的理解比类型约束更深。如果有一个团队,团队成员必须都意味着并遵守相同的合同。他们必须是专注的思考者,并且必须花时间一起讨论具体的例子,以建立对合同的共同理解。

同样的要求也适用于 API 用户:用户必须首先记住文档,然后她才能逐渐理解合约,如果合约精心制作,她就会开始喜欢 API(否则会讨厌它)。

这与鸭子打字有关。无论方法输入的类型如何,合同都必须提供有关发生什么的线索。因此,必须以更深入、更概括的方式来理解合同。这个答案本身可能看起来有点不具体,甚至有点傲慢,对此我深表歉意。我只是想说鸭子不是谎言,鸭子意味着人们在更高的抽象层次上思考自己的问题。设计师、程序员、数学家都是同一种能力的不同名称,而且数学家知道数学有很多层次的能力,在这些层次上,更高层次的数学家很容易解决那些在较低层次上很难解决的问题。鸭子意味着你的编程必须是好的数学,它限制了成功的开发人员和用户只有那些能够这样做的人

于 2014-08-10T12:48:09.590 回答
1

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

您的担忧似乎与任何动态语言都是危险工具有关,它不能强制执行 API 输入/输出合同。事实是,虽然选择 static 可能看起来更安全,但在这两种情况下你可以做的更好的事情是保留一组好的测试,这些测试不仅可以验证返回的数据的类型(这是 Java 编译器唯一可以验证和强制执行),而且它的正确性和内部工作原理(黑盒/白盒测试)。

附带说明一下,我不了解 Ruby,但在 PHP 中,您可以使用 @phpdoc 标签来提示 IDE(Eclipse PDT)关于某个方法返回的数据类型。

于 2008-10-07T03:09:24.573 回答
1

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

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

于 2011-03-22T00:11:05.363 回答