Ruby和duck打字:按合同设计是不可能的



Java中的方法签名:

public List<String> getFilesIn(List<File> 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中,函数参数是名称的一部分。在红宝石中,它们不是
这方面的奇特术语是方法重载,它是由编译器强制执行的。

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

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

至于返回类型,get_files暗示您正在返回一个文件数组。您不必担心它是List<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更具表达性的语法,您可以更清楚地编写类似英语的代码,告诉其他人您的合同是什么(这样可以节省数千个尖括号)。

就我个人而言,我认为这是一场净胜球。你可以利用新发现的业余时间编写一些规格和测试,并在一天结束时推出更好的产品。

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

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

虽然我在写Java代码时喜欢静态键入,但没有理由不在Ruby代码(或任何类型的代码)中坚持深思熟虑的先决条件。当我真的需要坚持方法params(在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"真正指的是你的:)

通过鸭子类型验证方法:

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

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

方法的正确命名至关重要。通过给一个方法命名get_files_in(directory),您也向用户提供了一个提示,告诉用户该方法期望得到什么以及它将返回什么。例如,我不希望get_files_in()中出现Potato对象——这是没有意义的。只有从该方法中获得文件名列表,或者更恰当地说,获得文件实例列表才有意义。至于列表的具体类型,取决于您想要做什么,返回的list的实际类型实际上并不重要。重要的是,你可以以某种方式列举列表中的项目。

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

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

你可以把契约视为设计原则,用抽象的形式来表述它们通常很难,也很无聊。非类型化的语言要求程序员真实地思考契约,她对契约的理解不仅仅是类型约束。如果有一个团队,团队成员都必须遵守相同的合同。他们必须是敬业的思想家,必须花时间一起讨论具体的例子,以便建立对合同的共同理解。

同样的要求也适用于API用户:用户必须首先记住文档,然后才能逐渐理解合同,并开始喜欢API(如果合同是经过深思熟虑的)。

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

这绝不是维护噩梦,而是另一种工作方式,它要求API和良好的文档保持一致。

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

顺便说一句,我不知道Ruby,但在PHP中,您可以使用@phpdoc标记向IDE(Eclipse PDT)提示某个方法返回的数据类型。

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

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

相关内容

  • 没有找到相关文章

最新更新