静态/强类型和重构
Posted
技术标签:
【中文标题】静态/强类型和重构【英文标题】:Static/strong typing and refactoring 【发布时间】:2010-10-27 04:41:39 【问题描述】:在我看来,静态/强类型编程语言最宝贵的地方在于它有助于重构:如果/当您更改任何 API 时,编译器会告诉您该更改破坏了什么。
我可以想象用运行时/弱类型语言编写代码……但我无法想象没有编译器的帮助就可以进行重构,也无法想象没有重构就可以编写数万行代码。
这是真的吗?
【问题讨论】:
您仍然需要重构,您只需使用单元测试作为 IDE/编译器的替代品。习惯了就真的没那么坏了。 【参考方案1】:我认为您将检查类型与检查方式混为一谈。运行时类型不一定很弱。
静态类型的主要优点正是您所说的:它们是详尽的。只需让编译器执行此操作,您就可以确信所有调用站点都符合该类型。
静态类型的主要限制是它们受限于它们可以表达的约束。这因语言而异,大多数语言具有相对简单的类型系统(c、java),而其他语言则具有极其强大的类型系统(haskell、cayenne)。
由于这种限制,类型本身是不够的。例如,在 java 中,类型或多或少地仅限于检查类型名称是否匹配。这意味着您要检查的任何约束的含义都必须编码到某种命名方案中,因此 java 代码常见的过多间接和样板。 C++ 稍微好一点,因为模板允许更多的表现力,但不能接近你可以用依赖类型做的事情。我不确定更强大的类型系统的缺点是什么,但显然肯定会有一些或更多的人会在工业中使用它们。
即使您使用的是静态类型,也可能不足以检查您关心的所有内容,因此您也需要编写测试。静态类型是否比样板代码节省了更多的精力,这是一个长期激烈的争论,我认为并不是所有情况都有一个简单的答案。
关于你的第二个问题:
我们如何在运行时类型语言中安全地重构?
答案是测试。您的测试必须涵盖所有重要的情况。工具可以帮助您衡量测试的详尽程度。覆盖检查工具让您知道测试是否覆盖了代码行。测试变异工具(jester、heckle)可以让您知道您的测试是否在逻辑上不完整。验收测试让您知道您编写的内容符合要求,最后回归和性能测试确保产品的每个新版本都保持上一个版本的质量。
进行适当的测试与依赖复杂的类型间接相比,其中一个好处是调试变得更加简单。运行测试时,您会在测试中获得特定的失败断言,这些断言清楚地表达了它们在做什么,而不是钝的编译器错误语句(想想 c++ 模板错误)。
无论您使用什么工具:编写您有信心的代码都需要付出努力。它很可能需要编写大量测试。如果错误的惩罚非常很高,例如航空航天或医疗控制软件,您可能需要使用正式的数学方法来证明您的软件的行为,这使得这种开发非常昂贵。
【讨论】:
您能否详细说明我真正想要的静态类型系统缺少什么?据我所知,我并没有感觉到任何可以归因于类型系统的缺乏,自从我多年前学习 C++ 以来也没有。我很可能遗漏了某些东西而没有注意到它,在这种情况下,您向我指出这一点非常有价值。 @John:Lambda the Ultimate 的某个人可能会更好地回答你,但根据我有限的理解,C++ 类型的最大弱点似乎是对聚合的限制。 C++ 模板 hacking a la alexandrescue 可以让你走得很远,但我认为仍然有相当多的容器/数组相关代码诉诸运行时检查。例如,如果我想限制向量的形状怎么办?或者将字符串的长度限制在一个范围内?依赖类型可以表达这些东西。 我没有使用“依赖类型”的语言......我的“运行时类型”概念更像 javascript! 感谢您向我介绍“突变测试”的概念。【参考方案2】:我完全同意你的观点。动态类型语言应该擅长的灵活性实际上是使代码很难维护的原因。真的,如果数据类型以非平凡的方式更改而不实际更改代码,是否有程序继续工作?
同时,您可以检查传递的变量的类型,如果它不是预期的类型,则以某种方式失败。你仍然需要运行你的代码来根除这些情况,但至少会告诉你一些事情。
我认为 Google 的内部工具实际上会对其 Javascript 进行编译和类型检查。我希望我有这些工具。
【讨论】:
你说的不是 Google Web Toolkit (code.google.com/webtoolkit)? “真的,如果数据类型以非平凡的方式更改而不实际更改代码,程序是否会继续工作?”是的,在任何支持结构类型(又名鸭子类型)的语言中,您都会经常看到这一点。这不一定是静态的或动态的。例如,c++ 泛型函数只关心它们使用的类型的特定方面,因此可以对参数类型进行许多更改,而无需更改函数。【参考方案3】:首先,我是一名本地 Perl 程序员,因此一方面我从未使用静态类型网络进行过编程。 OTOH 我从来没有和他们一起编程,所以我不能谈论他们的好处。我能说的是重构是什么样的。
我不认为缺少静态类型是重构的问题。我发现一个问题是缺少重构浏览器。动态语言的问题是,在实际运行代码之前,您并不真正知道代码真正要做什么。 Perl 比大多数都拥有这一点。 Perl 的另一个问题是语法非常复杂,几乎无法解析。结果:没有重构工具(尽管they're working very rapidly on that)。最终结果是我必须手动重构。这就是引入错误的原因。
我有测试来捕捉它们……通常情况下。我确实发现自己经常面对一堆未经测试和几乎无法测试的代码,面临着鸡/蛋问题,即必须重构代码才能测试它,但必须测试它才能重构它。伊克。在这一点上,我必须编写一些非常愚蠢的高级“程序输出是否与以前相同”之类的测试,以确保我没有破坏某些东西。
静态类型,正如 Java、C++ 或 C# 中所设想的那样,实际上只能解决一小部分编程问题。它们保证您的接口传递带有正确标签的数据位。但是仅仅因为你得到一个 Collection 并不意味着 Collection 包含你认为它包含的数据。因为你得到一个整数并不意味着你得到了正确的整数。您的方法需要一个用户对象,但该用户是否已登录?
经典示例:public static double sqrt(double a)
是 signature for the Java square root function。平方根不适用于负数。签名的什么地方这么写的?它没有。更糟糕的是,它在哪里说明该功能甚至是做什么的?签名只说明它采用什么类型以及返回什么。它没有说明中间发生的事情,这就是有趣的代码所在的地方。有些人试图通过使用 design by contract 来捕获完整的 API,这可以广泛地描述为嵌入对函数的输入、输出和副作用(或缺少)的运行时测试......但这是另一个展示。
API 不仅仅是函数签名(如果不是,您将不需要 Javadocs 中的所有描述性散文),重构甚至不仅仅是更改 API。
静态类型、静态编译、非动态语言给您的最大重构优势是能够编写重构工具来为您进行相当复杂的重构,因为它知道对您的方法的所有调用在哪里。我很羡慕IntelliJ IDEA。
【讨论】:
“API 不仅仅是函数签名” ...如果我在不更改其签名的情况下更改方法的含义,那么我只需更改方法的名称:那么编译器将帮助/要求我在程序的其余部分中找到所有使用旧名称并期待旧含义的地方。【参考方案4】:我想说重构超出了编译器可以检查的范围,即使在静态类型语言中也是如此。重构只是改变程序的内部结构而不影响外部行为。即使在动态语言中,您仍然可以预期会发生和测试一些事情,您只是失去了编译器的一点帮助。
【讨论】:
【参考方案5】:在 C# 3.0 中使用 var 的好处之一是您可以经常更改类型而不会破坏任何代码。类型需要看起来仍然相同 - 具有相同名称的属性必须存在,具有相同或相似签名的方法必须仍然存在。但是你真的可以改变成一个非常不同的类型,即使不使用 ReSharper 之类的东西。
【讨论】:
以上是关于静态/强类型和重构的主要内容,如果未能解决你的问题,请参考以下文章