【问题标题】:Ruby and duck typing: design by contract impossible?红宝石和鸭子打字:合同设计不可能?
【发布时间】:2010-09-15 16:18:36
【问题描述】:

Java 中的方法签名:

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

ruby 中类似的一个

def get_files_in(directories)

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

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

这似乎很有问题:

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

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

更新

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

【问题讨论】:

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


    【解决方案1】:

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

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

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

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

    【讨论】:

      【解决方案2】:

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

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

      【讨论】:

        【解决方案3】:

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

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

        【讨论】:

          【解决方案4】:

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

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

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

          【讨论】:

            【解决方案5】:

            归根结底,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&lt;File&gt;ArrayList&lt;File> 等等,因为每个人都只使用数组(如果他们编写了自定义数组,他们将编写它以继承内置数组)。

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

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

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

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

            【讨论】:

            • +1:在使用静态类型语言长大并转向更动态的语言(Ruby、一些 PHP)之后,需要改变的不仅仅是语法——这是一种新的思维方式
            • 我完全同意马特的观点。你将不得不适应这种新的思维方式。然而,这不会在第一天发生。没有“在 24 小时内从 [在此处插入静态类型语言] 到 Ruby” 您会发现自己首先使用 ruby​​ 语法编写 java 惯用代码。如果你不断地审查和美化你的代码,你会很快开始编写 ruby​​ 惯用代码。
            【解决方案6】:

            通过鸭子类型验证方法:

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

            【讨论】:

            • 很抱歉,我必须对 SO 资历比我高得多的成员的答案投反对票。但这是不是鸭式打字。或者,更确切地说,它是某种类型的鸭子打字,它经常掩盖新手对鸭子打字的更深层次的整体概念。出于这个原因,我们通过 #respond_to?#instance_of? 将鸭子类型称为“错误”类型的鸭子类型或“鸡类型”,尽管从技术上讲它是一种鸭子类型。将鸭子类型引入到新手是说这是一个根本不检查类型并等待错误的代码字=)
            • 没有不好的感觉。自从我现在做 ruby​​ 已经很长时间了,这篇文章是 6 年的比特腐烂。 (我觉得自己老了)。最佳实践发生变化,文档和支持系统应反映这一点。
            • 我没有检查日期。事实上,6 年前这是最前沿的答案。我也觉得老了=)
            【解决方案7】:

            虽然我在编写 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”确实是指你的:)

            【讨论】:

              【解决方案8】:

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

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

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

              【讨论】:

                猜你喜欢
                • 2013-06-17
                • 1970-01-01
                • 2012-05-16
                • 1970-01-01
                • 2010-09-06
                • 1970-01-01
                • 2015-01-21
                • 1970-01-01
                • 2015-12-23
                相关资源
                最近更新 更多