使用动态语言编写大型代码库时,更难以维护。至少这就是将Play框架带到LinkedIn的首席开发人员Yevgeniy Brikman在JaxConf 2013(第44分钟)上录制的视频演示中说的。

他为什么这么说?原因是什么?

评论

您是否与其他来源进行了交叉检查?

我是问题中提到的Play框架演讲的作者。我本来打算写一个答复,但下面的埃里克·利珀特(Eric Lippert)的回答比我想像的要好,所以我投票支持它,建议所有人阅读。

这里有很多偏见,因为静态语言的样板太多,以至于它们最终都会变大。我对此有第一手的经验:stackoverflow.com/questions/5232654/java-to-clojure-rewrite

@Zubair Static不表示样板代码。您检查了Scala吗?您尝试过Clojure;这就是为什么您的观点有偏见的原因。
这种比较是否说明了以下事实:使用静态类型的语言编写时,大型代码库往往会变得更大。换句话说,比较Java与Ruby的一百万行代码库是一个有偏见的比较,因为Ruby可能做得更多。正确的比较可能是一百万行的Ruby代码库与五百万行的Java代码库。 Java代码库是否仍更具可维护性?我想不是。

#1 楼


动态语言使维护大型代码库变得更加困难


注意:我没有看过演示文稿。

我曾在以下设计委员会任职JavaScript(一种非常动态的语言),C#(一种主要是静态的语言)和Visual Basic(它既是静态的又是动态的),因此我对此有很多想法;

让我首先说很难维护大型代码库。无论使用什么工具,都很难编写大代码。您的问题并不意味着以静态类型的语言维护大型代码库是“容易的”;相反,该问题仅以动态语言而不是静态语言来维护大型代码库为前提。这就是为什么使用动态语言维护大型代码库的工作要比静态类型语言的工作大一些的原因。我将在这篇文章中探讨其中的一些。

但是我们正在超越自己。我们应该明确定义“动态”语言的含义; “动态”语言是指“静态”语言的反义词。

“静态类型”语言是一种语言,旨在通过只能访问源代码而不访问程序运行状态的工具来促进自动正确性检查。该工具推导的事实称为“类型”。语言设计人员针对使程序“类型安全”的内容制定了一套规则,该工具旨在证明程序遵循这些规则。相比之下,“动态类型化”语言并非旨在促进此类检查的语言。只有在程序运行时才能通过检查轻松确定存储在任何特定位置的数据的含义。

(我们也可以在动态范围的语言和词法范围的语言之间进行区分,但是出于讨论的目的,我们不要去那里。动态类型的语言不需要动态范围化,而静态类型的语言也不需要词法范围,但是有通常这两者之间是相关的。)

所以我们现在有了我们的术语,让我们来谈谈大型代码库。大型代码库通常具有一些共同的特征:



它们太大,以至于任何人都无法理解每个细节。
它们通常由人员随时间推移而变化的大型团队来进行。
它们通常需要很长时间,具有多个版本。

所有这些特征都阻碍了了解代码,因此会妨碍正确更改代码。简而言之:时间就是金钱;时间就是金钱。由于这些理解障碍的性质,对大型代码库进行正确的更改非常昂贵。

由于预算是有限的,并且我们想尽我们所能使用资源,因此维护大型代码库旨在通过减轻这些障碍来降低进行正确更改的成本。大型团队可以减轻这些障碍的一些方法是:模块化:将代码分解为某种“模块”,其中每个模块都有明确的职责。无需用户理解其实现细节,就可以记录和理解代码的操作。

封装:模块在“公共”表面积和“私有”实现细节之间进行区分,以便可以改进后者,而不会影响整个程序的正确性。

重用:一次正确解决问题后,便会一直解决;该解决方案可以在创建新解决方案时重复使用。诸如制作实用程序函数库或在可以由派生类扩展的基类中进行功能之类的技术,或鼓励组合的体系结构,都是用于代码重用的技术。再次,重点是降低成本。

注释:例如,对代码进行注释以描述可能会包含在变量中的有效值。

自动检测错误:一个从事大型程序工作的团队明智的做法是构建一种设备,该设备可以及早确定何时发生编程错误,并告知您有关错误的信息,以便可以在错误与更多错误复合之前迅速解决该错误。诸如编写测试套件或运行静态分析器之类的技术就属于此类。

静态类型的语言就是后者的一个示例。您会在编译器本身中获得一个查找类型错误的设备,并在将损坏的代码更改检入存储库之前通知您。显式类型的语言要求在存储位置上标注可以放入存储的事实。

因此,仅鉴于此原因,动态类型的语言使维护大型代码库变得更加困难,因为现在,必须以编写测试套件的形式来完成由编译器“免费”完成的工作。如果要注释变量的含义,则必须提供一个用于这样做的系统,并且如果新的团队成员意外违反了变量,则必须在代码检查中,而不是由编译器进行捕获。

现在,这是我一直在构建的关键点:动态键入的语言与缺少所有其他功能的语言之间存在很强的关联性,这使得降低维护大型代码库的成本变得更加容易,这就是关键使用动态语言维护大型代码库更加困难的原因。同样,在静态类型化的语言和具有使更大范围的编程变得更容易的功能之间存在关联。

以JavaScript为例。 (从1996年到2001年,我在Microsoft从事JScript的原始版本的开发。)JavaScript的设计目的是当您将鼠标悬停在其上时使猴子跳舞。脚本通常是一行。我们认为十个行脚本非常正常,一百个行脚本非常庞大,而数千个行脚本则闻所未闻。该语言绝对不是为大型编程而设计的,而我们的实现决策,性能目标等均基于该假设。

由于JavaScript是专门为程序设计的,因此一个人可以在一个页面上看到全部内容,因此JavaScript不仅是动态键入的,而且缺少编程时通常使用的许多其他功能。大而言之:没有模块化系统;没有类,接口,甚至没有名称空间。这些元素是用其他语言来帮助组织大型代码库的。
继承系统-原型继承-既脆弱又难以理解。开箱即用,如何正确构建深层次结构原型(船长是海盗,海盗是人,人是物...),这绝不是显而易见的。 JavaScript。
没有任何封装;每个对象的每个属性都取决于for-in构造,并且可以在程序的任何部分随意修改。
没有办法注释任何存储限制。任何变量都可以具有任何值。

不仅仅是缺少使大型编程变得容易的功能。还有一些功能使它变得更难。



JavaScript的错误管理系统的设计假设脚本正在网页上运行,可能会导致失败,并且会增加失败的代价。级别低,看到故障的用户是修复故障的能力最弱的人:浏览器用户,而不是代码的作者。因此,尽可能多的错误会在无提示的情况下失败,并且程序会不断尝试混淆。考虑到语言的目标,这是一个合理的特征,但是它无疑使编写更大的程序变得更加困难,因为这增加了编写测试用例的难度。如果没有任何失败,那么编写检测失败的测试就变得更加困难!
代码可以根据用户输入通过eval之类的功能来修改自身,或者将新的script块动态添加到浏览器DOM中。任何静态分析工具都可能甚至不知道程序由什么代码组成!
等等。

很显然,可以克服这些障碍并使用JavaScript来构建大型程序。现在存在许多百万行的JavaScript程序。但是,构建这些程序的大型团队使用工具并有纪律来克服JavaScript妨碍您使用的障碍:


他们为程序中曾经使用的每个标识符编写测试用例。在一个拼写错误被忽略的世界中,这是必要的。这是一种成本。
他们使用类型检查语言编写代码,然后将其编译为JavaScript,例如TypeScript。
他们使用鼓励编程的框架,使其更易于分析,更易于模块化,并且不太可能产生常见错误。
他们在命名约定,职责分工,给定对象的公共外观是什么等方面都有良好的纪律。同样,这是一个成本;这些任务将由编译器以典型的静态类型化语言执行。

总而言之,不仅仅是类型化的动态性质会增加维护大型代码库的成本。仅此一项就增加了成本,但这远非故事的全部。我可以为您设计一种语言,该语言是动态类型的,但也具有名称空间,模块,继承,库,私有成员等等-实际上,C#4是这种语言,并且这种语言既动态又高度适用于大型程序。

动态语言经常丢失的其他所有内容也增加了大型代码库的成本。动态语言还包括用于良好测试,模块化,重用,封装等的功能,当在大型程序中进行编程时,确实可以降低成本,但是许多常用的动态语言都没有内置这些​​功能。它们,这会增加成本。

评论


@ThiagoSilva:语言是专用的。如果您正在为大型编程构建语言,则很有可能会增加使大型编程变得更便宜的所有功能,这将带来很多“仪式”并限制您编写的内容。如果您要建立一种语言,例如将鼠标悬停在上面时,让猴子跳舞,那么您希望单行程序成为一行。在这种情况下,动态类型化是很自然的,因为它为开发人员提供了很大的灵活性。

–埃里克·利珀特
13年12月18日在15:53

虽然我一般都同意您的意见,但您离这里很远。现代JavaScript与Microsoft在1996-2001年所做的工作非常不同。您有模块系统(AMD,CommonJS),并按惯例(如Python)(或闭包,但我认为没有必要)进行封装,有一些方法可以对变量的存储进行注释(例如,使用getters / setter方法),与13年前相比,对继承制度的理解要好得多。如今,用JavaScript构建强大而强大的应用程序并不容易。您的示例应显示为“以2001年的JavaScript为例”。

–本杰明·格林巴姆
2014年1月28日上午8:22

@BenjaminGruenbaum:您的批评是有根据的;但是,我建议您仔细研究许多大型现代JS代码库,可以找到许多我列举的这类问题的示例。仅仅因为存在纪律并不意味着每个人都知道并使用它们。而且,您可能会对JS循环包含braek的次数感到惊讶。或cotninue;声明-完全合法!不执行预期的操作。仍已签到。

–埃里克·利珀特
2014年1月28日14:57

本杰明,如果您正在谈论使用IDE,静态分析工具和CI服务器的成熟开发团队,则可以肯定地解决动态类型语言的局限性。但是,在有些团队尚未使用DVCS和CI服务器工具的情况下,在编写代码时让语言强制执行规则是非常有用的。 jslint很有用,但它永远无法像针对静态类型语言的静态分析工具一样强大,仅仅是因为没有足够的类型信息来进行分析。

–解决方案瑜伽士
2014年1月28日下午16:45

我认为这篇文章遗漏了静态类型化的主要优点-重构能力。尝试在一百万行的JS应用程序中重命名变量或查找变量的所有引用。没有静态分析,您的IDE可能无法通过这种操作有效地完成工作。

– MgSam
14年6月29日在13:39

#2 楼

因为他们故意放弃了编程语言提供的一些工具来断言您对代码的了解。

最著名,最明显的例子是严格/强/强制/显式键入(请注意术语存在很大争议,但大多数人同意某些语言比其他语言更严格)。如果使用得当,它就可以永久表示您希望在特定位置出现的值的种类,这可以简化对行,例程或模块的可能行为的推理,仅是因为可能的情况较少。如果您只打算将某人的名字当作字符串使用,那么许多编码人员都愿意键入声明,不对该规则进行例外处理,并愿意在他们不经意间接受偶尔的编译错误(

其他人则认为这会限制其创造力,减慢开发速度,并引入编译器应做的工作(例如,忘记了引号)通过类型推断)或根本不需要(他们只会记住坚持使用字符串)。这样做的一个问题是,人们很难预测他们将犯什么样的错误:几乎每个人都经常高估自己的能力。更阴险的是,代码库越大,问题就变得越严重-实际上,大多数人都可以记住,客户名是一个字符串,但是将78个其他实体添加到了组合中,全部带有ID,有些带有名称,有些带有序列号“数字”,其中一些确实是数字(需要进行计算才能完成),但其他一些则需要存储字母,过一会儿就很难记住您正在阅读的字段是否确实可以保证评估是否为int。

因此,许多适合快速原型项目的决策在大型生产项目中效果不佳-往往没有人注意到临界点。这就是为什么没有一种适合所有人的语言,范式或框架(以及为什么争论哪种语言更好是愚蠢的)的原因。

评论


我想限制性语言和非限制性语言都有其高点和低点。但是,以我的经验,拥有更大的自由度就像在汇编中进行编程一样,您可以做所有事情,但是很难执行任何复杂的程序。好答案。

–尼尔
2013年12月17日在11:24



您要查找的单词是静态类型。静态类型的语言可以是严格的或松散的,它们可以是强壮的或脆弱的,可以是强制性的或可选的,并且可以是显式的或隐式的,但是它们的共同之处在于类型是可以从文本中推论得出的事实。程序,而不实际运行它。之所以称为“动态”语言,是因为有时直到程序实际运行时才知道有关该程序的事实。

–埃里克·利珀特
2013年12月17日15:49

我想补充一点,Java(允许对类型系统进行简单且危险的强制转换为对象的违反)和Haskell(平均每五个或五个类型需要一个显式的类型签名)之间存在区别。六个功能,其余的功能可以推断出来,但是如果您尝试任何有趣的操作都会打屁股)。

– Karl Damgaard Asmussen
2013年12月17日16:10

“其他人认为这限制了他们的创造力,减慢了开发速度……”-我将在列表中添加“在设计中引入人为的复杂性”。 “静态类型”语言可以迫使您应对的两种不同类型复杂性的有力例子是:(a)Haskell中存在的monad; (b)彼得·诺维格(Peter Norvig)指出,该流行书的23种设计模式中,有16种在“动态类型”语言中是“不可见”或更简单的,而在其他语言中,它们大多是在解决静态检查限制方面肿:norvig.com/设计模式

–蒂亚戈·席尔瓦(Thiago Silva)
2013年12月17日在16:24



@ThiagoSilva:Monad本身并不是复杂性的一个例子。许多人发现它们很难学习,但是随着抽象的发展,它们非常简单-困难在于它们也非常抽象。实际上,单子通常通过使设计更加明确来简化设计:它们只是突出显示了其他语言中不可思议的不可思议的事物。而且Norvig的设计模式文章与静态类型的功能语言完全无关。通常,它不是关于静态类型的评论,而是关于Java样式的类型系统(我们都同意这是一团糟)。

– Tikhon Jelvis
2014年1月27日21:51

#3 楼

您为什么不问那个演讲的作者呢?毕竟,这是他的主张。他应该备份它。

有很多用动态语言开发的非常大,非常复杂,非常成功的项目。用静态类型的语言(例如FBI虚拟案例文件)编写的项目有很多引人注目的失败。

用动态语言编写的项目往往比以静态语言编写的项目要小。类型的语言,但这是一个红色的鲱鱼:大多数用静态类型的语言编写的项目都倾向于用Java或C这样的语言编写,它们的表达能力不是很高。多数以动态语言编写的项目往往以非常有表现力的语言编写,例如Scheme,CommonLisp,Clojure,Smalltalk,Ruby,Python。

那些项目较小的原因不是因为您可以“不要用动态语言编写大型项目,这是因为您不需要用表达性语言编写大型项目…………只需用更少的代码行,就可以用一种更具表达性的语言来完成相同的事情。 >
例如,用Haskell编写的项目也往往很小。不是因为您不能在Haskell中编写大型系统,而是因为您不必这样做。

但是,让我们至少看看静态类型系统为编写大型系统所提供的功能:类型系统阻止您编写某些程序。那是它的工作。您编写了一个程序,将其提供给类型检查器,然后类型检查器说:“不,您不能编写,抱歉。”特别是,类型系统的设计方式应使类型检查器阻止您编写“不良”程序。有错误的程序。因此,从这个意义上讲,是的,静态类型系统有助于开发大型系统。

但是,有一个问题:我们有暂停问题,赖斯定理和许多其他不完全性定理,这些定理基本上告诉我们一件事:不可能编写类型检查器来始终确定程序是否是类型安全的。总是会有无数类型的程序无法确定类型安全的程序。对于类型检查器,只有一件理智的事情:拒绝这些程序是不安全的。实际上,这些程序中有无数不是类型安全的。但是,也有无数个此类程序是类型安全的!其中一些甚至会有用!因此,类型检查器只是因为无法证明其类型安全性而使我们无法编写有用的类型安全程序。

IOW:类型系统的目的是限制表达性。

但是,如果那些被拒绝的程序之一实际上以一种优雅,易于维护的方式解决了我们的问题,该怎么办?那么我们就不能编写该程序。

我要说的是基本的做法:静态类型的语言限制您编写不良程序,但有时会阻止您编写良好的程序。动态语言不会阻止您编写良好的程序,也不会阻止您编写不良的程序。

对于大型系统的可维护性而言,更重要的方面是可表达性,这仅仅是因为您不会首先需要创建一个大型而复杂的系统。

评论


您还应该包括Scala,它具有出色的类型推断功能。

– Jus12
2013年12月17日12:38在

一种有趣的查看方式。就个人而言,我发现对于大型项目,类型检查器可以节省生命。它们隐式地提供了一种非常基本的单元测试(如果我们可以称其为它-只是在测试结构是否一致)。这种测试是我没有时间手动编写的,但是需要在项目扩展到您难以承受的范围时进行。我怀疑这种情况对于大多数系统来说都相当快,无论表现力如何。在动态世界中通常如何解决此问题?

–丹尼尔B
2013年12月17日12:47

我只是想补充一下上面的评论:许多(中/大型)问题的性质是,您将需要数百个实体来建模,无论是否需要表达语言。富有表现力的代码可能会将代码缩减10倍甚至更多倍,但是如果没有其他工具,代码仍然无法管理。我想知道这个工具是什么。

–丹尼尔B
2013年12月17日在12:49

@DanielB:取决于语言。记住,动态的!=弱。例如,在python中,“ 1”!= 1,如果尝试互换使用它们,则在运行时会出现类型错误。不过,大多数情况下,最接近的是鸭子类型-如果类型错误并调用方法,则运行时异常。它没有像适当的静态类型系统那样强大,但是它不是未类型化的。

– Ph子
2013年12月17日14:02

我是问题中提到的Play框架演讲的作者。我本来打算写一个答复,但埃里克·利珀特(Eric Lippert)的答复说比我想像的要好,因此我投票赞成,建议所有人阅读。另外,请记住,“大型代码库”可以在多个维度上“大型”,包括代码行,有多少人同时进行处理以及处理了多长时间。所有这些因素都会增加“代码腐烂”。静态类型化不是灵丹妙药,而是一种减少代码腐烂的工具。

–叶夫根尼·布里克曼(Yevgeniy Brikman)
2014年2月9日在21:06



#4 楼

显式静态类型是一种通用的,可以保证正确形式的文档,在动态语言中不可用。如果这不能得到补偿,那么您的动态代码将更难以阅读和理解。

评论


您是什么补偿?我曾经在C语法语言和Smalltalk中工作过,这恰恰相反。

–robject
2014年2月11日下午5:59

您可以为动态语言添加类型检查。例如,请参见如何针对python完成此操作:pypi.python.org/pypi/optypecheck

–卡洛·皮雷斯(Carlo Pires)
2014年6月30日14:44

Python具有自动测试的内联单元测试(称为doc测试)。文档测试在提供文档方面比类型更进一步,因为它们为您提供了代码用法示例。

–aoeu256
18年5月26日在14:54



适用于Python的现代ide(Pydev,Pycharm)可以使用类型推断来告诉您事物的类型,而无需显式键入。还有一些方法可以记录以前对函数/方法的调用,尽管它不是主流。如果在函数中设置断点,则可以访问Python REPL中的locals()(Pydev和Pycharm将REPL连接到当前上下文),并在其运行时开发应用程序不仅允许您访问所有类型,但值。

–aoeu256
18年5月26日在15:39

#5 楼

考虑一个包含数据库绑定和丰富的
测试套件的大型代码库,让我重点介绍静态语言相对于动态语言的一些优点。 (某些示例可能是特质的,
不适用于任何静态或动态语言。)

正如其他人指出的那样,总体思路是类型系统是
程序的“维度”,它向处理程序的自动工具(编译器,代码分析
工具等)公开一些信息。使用动态语言,此信息基本上会被剥离,因此不可用。使用静态语言,此
信息可用于帮助编写正确的程序。

修复错误时,您将从对您的
编译器来说不错的程序开始,但是逻辑错误。修正错误后,您进行编辑
在本地修复程序的逻辑(例如,在一个类中),但
在其他地方破坏了此逻辑(例如,与前一个版本合作的类) )。由于用静态语言编写的程序
比使用动态语言编写的程序
向编译器提供更多的信息¹,因此编译器将帮助您找到逻辑中断的其他地方。不仅仅是动态
语言的编译器所能做的。这是因为本地修改会破坏程序在其他地方的类型正确性,从而迫使您
全局修复类型正确性,然后才有机会再次运行
程序。

静态语言可以强制执行程序的类型正确性,并且您可以
假设在程序上工作时遇到的所有类型错误
都将与假设中的运行时错误相对应该程序以动态语言进行翻译,因此前者的错误要少于后者。结果,它需要更少的覆盖率测试,更少的
单元测试和更少的错误修正,总之,
维护起来更容易。

当然,这是一个折衷方案:虽然有可能在类型系统中公开很多信息,从而趁此机会编写可靠的程序,但可能很难将其与
结合使用。 br /> flexible〜API。

以下是一些可以在类型
system中进行编码的信息示例:

—编译器可以保证的常量正确性将值
“只读”传递给过程。²

-数据库模式,编译器可以保证将
程序绑定到数据库的代码与该数据库相对应定义。当此定义更改时,此
非常有用。 (维护!)

-系统资源,编译器可以保证使用
系统资源的代码仅在资源处于正确的
状态时才执行。例如,可以在类型系统中对文件的属性close
open进行编码。

¹区分编译器和
解释器是没有用的在这里,如果存在这种差异。

评论


“在程序上工作时遇到的所有类型错误都将与运行时失败相对应”:事实并非如此(这是动态类型支持程序的一个重要论点)。但是,如果没有运行时失败,则出于非显而易见的原因,需要记录在案。除了以注释的形式记录文档之外,您还可以通过编译器理解和检查的方式对其进行记录。我要说的是,在程序上工作时遇到的所有类型错误都将对应于运行时故障或维护噩梦。

–吉尔斯'所以-不再是邪恶的'
2013年12月17日的16:07

#6 楼

因为静态类型可以提供更好的工具,所以当程序员尝试理解,重构或扩展现有的大型代码库时,可以提高生产率。

例如,在大型程序中,我们很可能拥有具有相同名称的几种方法。例如,我们可能有一个add方法,该方法向集合中添加内容,另一个方法添加两个整数,另一个方法将钱存入银行帐户,...)。在小型程序中,这种名称冲突不太可能发生。在由几个人共同处理的大型程序中,它们自然发生。

在静态类型的语言中,此类方法可以通过它们所操作的类型来区分。特别是,开发环境可以针对每个方法调用表达式发现正在调用的方法,从而使其能够显示该方法文档中的工具提示,查找该方法的所有调用位置或支持重构(例如方法内联,方法重命名,修改参数列表,...)。

评论


使一切全球化?这不是动态语言的必要,我称这是一个编写不良的代码库...

–伊兹卡塔
2013年12月17日下午16:42

也许我应该提到我在谈论动态分配的面向对象编程语言。这样的方法不是全局的,但是要弄清楚将调用哪种实现需要了解接收器的类型。

– Meriton
2013年12月17日下午16:47

在鼠标悬停时显示工具提示是动态语言IDE的标准功能,早于使用静态语言的程序员拥有IDE甚至是鼠标。自动重构工具是用动态语言发明的,哎呀,IDE是在那里发明的。用于动态语言的重构工具仍然可以完成例如Eclipse,IDEA或Visual Studio无法使用,例如已经部署了重构代码帽子或尚未编写的重构代码。

–Jörg W Mittag
2013年12月17日在17:27

我并不是说工具提示,IDE或鼠标是用静态类型的语言发明的。我仅声称,在面向对象的语言中,函数的名称通常不足以标识该函数,因此工具无法知道正在调用哪个函数,并且无法显示正确的工具提示或内联正确的函数,依此类推-至少并非没有询问用户。

– Meriton
2013年12月17日18:43



当程序像静态语言程序一样使用时,用于动态语言的现代IDE可以使用类型推断来生成此信息。可选类型也可以帮助提示IDE。从理论上讲,您可以使用动态语言记录前一个函数调用的参数和返回值,并将此信息用于类型推断。如果使程序在断点处停止运行,它不仅可以告诉您类型,还可以告诉所有对象的值。 Pydev和Pycharm允许Python REPL访问本地范围。

–aoeu256
18年5月26日在16:00