我正在研究和尝试更多使用Groovy,试图理解在Java中不能或不会实现的东西采用Groovy的利弊。对于动态编程,由于我一直深入静态和强类型语言,它仍然只是一个概念。
Groovy使我能够进行鸭子类型,但我真的看不到其价值所在。鸭子类型比静态类型更具生产力吗?我在我的代码练习中可以做哪些事情来帮助我理解其优点?
我关注Groovy时提出了这个问题,但我知道这不一定是一个Groovy问题,所以欢迎每个编码者的回答。
我正在研究和尝试更多使用Groovy,试图理解在Java中不能或不会实现的东西采用Groovy的利弊。对于动态编程,由于我一直深入静态和强类型语言,它仍然只是一个概念。
Groovy使我能够进行鸭子类型,但我真的看不到其价值所在。鸭子类型比静态类型更具生产力吗?我在我的代码练习中可以做哪些事情来帮助我理解其优点?
我关注Groovy时提出了这个问题,但我知道这不一定是一个Groovy问题,所以欢迎每个编码者的回答。
对我来说,Groovy似乎也是如此。当然,您可以编写非常简洁的代码,并且确实有一些漂亮的语法糖,使我们可以使用属性、集合等...但是不知道到底传递了什么成本只会变得越来越糟糕。在某个时候,您会扪心自问为什么项目已经变成了80%的测试和20%的工作。这里的教训是,“更小”并不能使代码“更易读”。对不起各位,这是简单的逻辑-您需要凭直觉知道的越多,理解该代码的过程就越复杂。这就是为什么GUI在这些年中已经不再过分地使用图标-看起来很漂亮,但正在发生什么并不总是显而易见。
在那个项目中,人们似乎很难“抓住”所学到的知识,但是当您的方法返回类型T的单个元素、T的数组、ErrorResult或null时,这变得相当明显。
然而,与Groovy一起工作给了我一个好处 - 惊人的可计费时间!
鸭子类型会削弱大多数现代IDE的静态检查能力,这些能力可以在您输入时指出错误。一些人认为这是一个优点。我希望IDE/编译器尽快告诉我我犯了一个愚蠢的程序员错误。
我最近最喜欢用来反对鸭子类型的论据来自于一个Grails项目DTO:
class SimpleResults {
def results
def total
def categories
}
results
的内容类似于Map<String, List<ComplexType>>
,只有通过在不同类中跟踪方法调用的轨迹才能发现它的创建位置。对于那些好奇心强烈的人来说,total
是List<ComplexType>
大小的总和,而categories
则是Map
的大小。
对于最初的开发者来说可能很清楚,但是可怜的维护人员(我)为此花费了很多精力。
results
是一个 Map<String, List<ComplexType>>
,而 total
和 categories
都是 int
。然而,这并没有告诉我如何使用这个类。 - Ben在您使用鸭子类型之前,有点难以看出它的价值。一旦您习惯了它,就会意识到不必处理接口或担心某些东西的确切类型有多么轻松自在。
接下来,哪个更好:EMACS还是vi?这是一个长期争论的话题。
可以这样想:任何一个程序如果是正确的,在静态类型语言中也会是正确的。静态类型让编译器在编译时就有足够的信息来检测类型不匹配,而不是在运行时才发现。如果你正在进行增量式编程,这可能会很烦人,但我认为如果你对程序思考清楚了,这并不重要;另一方面,如果你正在构建一个非常大的程序,例如操作系统或电话交换机,并且有几十个、几百个或成千上万个人在其中工作,或者具有非常高的可靠性要求,那么编译器能够在不需要测试用例来执行恰当的代码路径的情况下,为您检测大量问题是很有用的。
动态类型并不是什么新鲜事物:例如,C语言实际上就是动态类型,因为我总是可以将foo*
强制转换为bar*
。这意味着作为C程序员,从此我就有责任永远不使用适用于bar*
的代码,当地址实际上指向foo*
时。但由于大型程序的问题,C语言发展出了像lint(1)这样的工具,用typedef
加强了其类型系统,并最终在C++中开发出了一个强类型变体。(当然,C++又开发出了各种类型的转换、泛型/模板和运行时类型识别等方法来规避强类型。)
不过还有一件事——不要将“敏捷编程”与“动态语言”混淆。 敏捷编程是关于项目中人们如何合作的方式:项目是否能够适应变化的需求以满足客户的需求,同时为程序员提供人性化的环境?可以使用动态类型语言来完成,并且通常也会这样做,因为它们可能更具生产力(例如Ruby、Smalltalk),但它也可以在C甚至汇编语言中成功地完成。事实上,Rally Development甚至使用敏捷方法(尤其是SCRUM)来进行营销和文档编写。
通过使用TDD+100%代码覆盖率 + IDE工具来不断运行我的测试,我已经不再感觉需要静态类型。没有强类型,我的单元测试变得非常容易(只需使用Maps创建模拟对象)。特别是当您使用泛型时,您可以看到不同之处:
//Static typing
Map<String,List<Class1<Class2>>> someMap = [:] as HashMap<String,List<Class1<Class2>>>
vs
//Dynamic typing
def someMap = [:]
在我看来,鸭子类型的优势在于遵循一些约定时会更加突出,比如以一致的方式命名变量和方法。以Ken G的例子为例,我认为最好是这样写:
class SimpleResults {
def mapOfListResults
def total
def categories
}
假设您定义了一个涉及名为'calculateRating(A,B)'的操作的合同,其中A和B遵循另一个合同。 伪代码如下:
Long calculateRating(A someObj, B, otherObj) {
//some fake algorithm here:
if(someObj.doStuff('foo') > otherObj.doStuff('bar')) return someObj.calcRating());
else return otherObj.calcRating();
}
public interface MyService {
public int doStuff(String input);
}
public long calculateRating(MyService A, MyServiceB);
doStuff()
调用。不需要特定的契约定义。这对你有利,但也可能对你不利。缺点是你必须特别小心,以保证你的代码在其他人更改它时不会出错(即,其他人必须意识到方法名称和参数的隐式契约)。请注意,在Java中,语法不如它本应该的那么简洁(例如,与Scala相比)。一个反例是Lift框架,他们说框架的源代码行数与Rails类似,但测试代码的行数较少,因为他们不需要在测试中实现类型检查。class BookFinder {
def searchEngine
def findBookByTitle(String title) {
return searchEngine.find( [ "Title" : title ] )
}
}
现在进行单元测试:
void bookFinderTest() {
// with Expando we can 'fake' any object at runtime.
// alternatively you could write a MockSearchEngine class.
def mockSearchEngine = new Expando()
mockSearchEngine.find = {
return new Book("Heart of Darkness","Joseph Conrad")
}
def bf = new BookFinder()
bf.searchEngine = mockSearchEngine
def book = bf.findBookByTitle("Heart of Darkness")
assert(book.author == "Joseph Conrad"
}
鸭子类型并不比静态类型更高效,只是不同而已。使用静态类型时,您总是需要担心数据是否为正确的类型,在Java中,这通过将其强制转换为正确的类型来显示。使用鸭子类型时,只要具有正确的方法,类型就无关紧要,因此它真正消除了许多强制转换和类型之间的转换的麻烦。