跟极限编程创始人Kent Beck学编程


跟极限编程创始人Kent Beck学编程

www.chinamaker.net 2012-09-28 19:48:04 admin

跟极限编程创始人Kent Beck学编程

 

 
 

我、Stig、Krzysztof、Jakub和Kent Beck在2012年的Iterate Code Camp花了一个星期时间一起做了个项目,进行最佳实践性的编程学习。我们想和大家分享一下我们在这个过程中学习到的宝贵经验及教训,以使我们能成为更好的程序员(或者至少我们是这样认为的)。

编程风格背后的价值观

在整个实践过程中,我们渐渐学会了遵守编程最基本的三个价值观:沟通、简单和灵活(按照重要性进行排序),这三个价值观应该被所有开发人员所铭记。在下面我们将会对它们进行简单介绍,另外你也可以在Kent的《实现模式》(Implementation Patterns)一书中找到更详细的说明以及一些基本的实践。

沟通

程序的可读性往往高于程序如何去写,任何一个程序员都能写出机器读得懂的代码,而一个优秀的程序员应该写出别人可以理解的代码。代码作为传达设计理念的工具,在编写的时候应该清楚地表达出设计思想。(就拿典型的企业系统来说,在5——15年内代码都会被大量修改,而每次去执行修改的人肯定不会是同一个人,因此代码要更加通俗易懂,这样修改起来才轻松。

简单

消除不必要的复杂性,各级应用都应该以简单为原则。简单可以使程序更容易理解和更利于沟通;通过沟通更容易实现简单。

灵活

现在做出一个可行的决定,并保留可以将来修改它的灵活度,这是好的软件开发的关键。——Kent Beck的《Implementation Patterns》

程序应该灵活地修改,使一些常见的修改变得简单,至少要比现在简单。复杂性通常来自于过度的灵活性,但是如果没有灵活性,那么它就如同垃圾。最好的一种灵活性就是来自简单广泛的测试。试图通过设计phases之类行为来增加灵活度,最终得到的通常是“复杂的灵活”。大多数时候,对于程序哪些地方需要被修改,在没有足够信息的前提下很难做出正确的决定。所以适当地推迟决定,直到最后判断正确后再做出一个有效和有用的决策。

概括

编写简单、通俗易懂的代码,会让你的工作伙伴或者未来的系统维护人员花很少的精力就可以轻易代码。(当然,这也需要一定的技能等级和专业知识。)你无法预见需求变化,所以保持代码简单、灵活,使其演变可以跟上需求的变化和时间的推移。

学习要点1 你并不需要它

今天将演示什么?测试开始时做什么?

在开始之前,我们设立了一个非常有趣和富有挑战的主题。我们决定尽最大的努力尝试一个高伸缩性的、分布式数据库。我们花了几个小时讨论如何实现,毕竟这里有许多事情需要考虑:同步策略、哈希一致、集群成员自动发现、冲突解决方案等等。如果只有5天时间,你需要计划如何用最简单的方式实现。需要做什么?如何跳过?对吗?错,Kent只是问我们在最后一天将演示什么和如何测试。

这最终会被证明是个非常聪明的做法,因为我们实际上可实现的功能只有一个小小的子集,对迄今遇到的问题进行总结,然后再进一步做出更加理智的决策。我们发现,任何超过10分钟以上的讨论90%的时间都被浪费啦!但这并不意味那个计划就是失败的(虽然产生的计划一般都是无用的),它只是意味着我们要做的会比实际需要多。

因此,我们宁愿选择不断接受反馈和经验来完善前期计划。问问自己:在下次我将要展示什么?写什么样的反馈能够反映自己,指导我今后的开发工作。

学习要点2 编写高级测试来指导开发

我们第二天的目标是通过编写一个实例进行同步和演示,忘掉第一个实例然后直接从第二个实例中解读(复制)数据。我们仅仅跟随着这些步骤编写相应的测试。

  1. List<Graft> grafts = Graft.getTwoGrafts();  
  2. Graft first = grafts.get(0);  
  3. Graft second = grafts.get(1);  
  4. first.createNode().put("key", "value")  
  5. first.kill();  
  6. assertNotNull(second.getNodeByProperty("key", "value")); 

(当然,这个API之后会变得普通)

现在有意思的是,这并不是个单元测试,它是个基本的集成测试,少用点技术术语就是一个故事驱动的高层次功能测试,是客户比较喜欢的功能。如果是一个单元测试,那么它会告诉你,这个类和预期打算的一样,而如果是一个故事测试,那么结果会是:“此功能和预期的一样”。

我一直认为TDD只适合单元/类级别的测试,这样的观点被证明是错误的,TDD还适合更高级别的测试。它包含了一些非常有意思的属性:

  1. 衡量项目进度,给“客户”的练习都是非常有意义的,因此每次得到的反馈信息也更真实可靠。
  2. 这可以帮助你专注于提供业务功能
  3. 它很有可能保持不变并且比单元测试或大多数代码存在的更长久,因为它是一个概念层次的东西。

现在,根据测试金字塔来看,故事测试用例明显要比单元测试要少,而且故事测试用例并不会测试所有可能性。难道这意味着需要在测完所有的故事测试用例后,再在小规模的单元测试中重复一遍?答案是否定的,那是完全没有意义的。回到灵活性原则和改变方式上,只有当你需要的时候才会构建额外的单元测试,例如在这几种情况下:第一个故事测试没有完全捕捉正确、发现一个非常重要的特殊案例、你想专注于整个解决方案里面的一部分。猜测故障点跟猜测设计一样,完全是在浪费时间。

学习要点3 单元测试的最佳实践

通常,会为了验证某个想法而开始一段测试,但我们并没有十足的把握能确保成功。因此,提供一个最佳实践来指导或帮助你实现最终结果。首先,提出一个要求,然后在集中精力去考虑如何实现。这就是我们在Graft项目中进行同步策略测试的最佳实践方法。

在测试里面写实现方法

确定功能后再开始编写测试用例,而不是提前思考如何组织(创建哪些类?在哪里进行整合?是否使用一个工厂类或工厂方法),为什么不直接在测试方法里面写代码?上面提到的那些因素可以以后分析。这样你就可以一心一意地撰写功能测试报告。此外,通过推迟内部组织实施,你将会有更多的时间去思考和决定,最后你会得到一个非常好的解决方案。

关键原则:重点、避免过早决策

自底向下的设计

避免:

  • 过早过多假设
  • 思维固定在某个特定的、不成熟的设计中
  • 限制思维(通常会以第一个计划来结束设计)

从小到大,先从一小部分的功能开始实现,然后再一步步整合形成一个复杂的模块。不要因为各个模块间的依赖关系而感到心烦意乱,在真正实现整合和替代之前先给它们进行简单地备份。使用这种技术就无需在最初的时候与设计方案相绑定。在这个过程中,不仅需要经验还需根据点直觉,连同TDD会有更好的设计和实现方案展现出来。

在没有形成最终解决方案的时候,我们发现这个方法非常有用。在开发Graft时,我们并没有预先设计好整个应用程序。在第一天我们只是挑选了一个用户案例去实现,然后在接下的日子里,我们也是在挑选案例然后进行实现。

行动与要求

我们的Graft数据库有一个接受用户命令的telnet-like接口。参照下面测试addComment的两个简单地变化:

  1. // Test 1  
  2. Graft db = ...; this.subject = new CommandProcessor(db);  
  3. subject.process("addComment eventId my_comment");  
  4.  
  5. assertThat(subject.process("getComments eventId")).isEqualTo("my_comment");  
  6.  // Test 2 (same setUp)  
  7. subject.process("addComment eventId my_comment");  
  8.  
  9. assertThat(db.getComments("eventId")).containsOnly("my_comment"); 

在Test1里面,当对addComment命令进行测试时,直接使用getComments这个命令来检查结果状态。在整个测试过程中,只使用了一个单独的API入口点——subject。Test2是直接访问底层数据库实例并且使用其API来获取数据。与此同时,subject这个API也访问底层数据库。

因此Test1并不是真正意义上的“单元”测试,因为它还依赖另一个测试类。而Test2则更加专注并且写法更加简单,它直接访问目标数据结构,对源代码进行正确检查。

我们将继续讨论像Test1那样的测试,在同等级上,它会完成所有相同的操作,即基于同一级别的公共API对象测试会更好。这里的“更好”是指容易理解、更加重要和稳定可维护,因为它们不是耦合到内部实现的功能测试。

像Test2的这种测试则更常见,无论是直接访问底层(对象、属性、数据库……)还是通过模拟来直接对产生的副作用进行验证。这些技术往往会导致耦合且对测试难于维护,这种技术应该限制在“私有单元测试”中,切勿混合公共和私有单元测试”。

  • 把后面的任务用列表弹出来,而不是立即去做
  • 专注于修复测试——无论多么差劲和简单(或者重构)
  • 专注于当前需求——切勿过早提取

还有一件值得我们的注意的事情,Kent任何时候都会关注自己在做什么。专注意味着你会一心一意的做一件事情,不会受其他事情所影响,无论那件事情有多么重要或者只是简单修复。(备注:永远不要说永远)如果你在修复一个Bug的过程中还发现其他的事情,比如给一个类起更好的名字、删除已死的代码、修复一个未提交的Bug等。这些你都可以用列表记下来,等到手头的Bug修复成功后再去落实。

并行(Parallel)设计

并行设计意味着当改变一个设计的时候,你要尽可能的保持在原来的需求上渐渐添加新元素,直至转向新设计。在这个过程要不断地确保正确性,这需要花费更大的精力,当然这样做也会让其更加安全和容易实现可恢复重构。

一个典型的并行设计案例是使用NoSQL数据库替换RDBMS。开始时,你会把实现好的代码写入新的数据库中,然后再把它同时写进新旧这两个系统里面,并且从新的里面读取数据(也许这样就可以与旧的相比较进行验证),同时还在使用旧的数据库数据。接下来你将开始实际使用NoSQL数据库的数据,同时从旧DB读/写数据(这样你可以方便地切换回来)。只有当新的DB被证明正确无误时,才可以逐步删除旧的DB。

一个小型的并行设计案例是用对象替换方法参数,例如notifuComment方法:

  1. - public void notifyComment(String message, String eventName, String user) {  
  2. -    notifications.add(user + ": commented>
    ---  
  3. + public void notifyComment(Edge target) {  
  4. +    notifications.add(target.getTo().getId() + ": commented>	

    代码的对称性表现在代码中,所有表达同一想法的地方都用相同的方法来表达。

    想象一下代码所要表达的想法,比如“从数据库中获取最后的更新文件”代码需要执行几次。如果方法名称和执行顺序都不同且彼此有很大的差别,那么这样的代码就是不对称的。问问自己:“这个方法是用来干嘛的”。一个对称代码的例子是保持代码在抽象层次上的一致性,比如方法。细心的读者可能已经注意到,一致性是对称的抽象层次,例如一致性的方法命名。但是,对称是抽象的,它涉及更多的想法,而不是规则(如“驼峰式”里的类名和方法命名规则)。

    • 如何管理你的时间,保持精力——注意休息和保持旺盛的体力,在劳累之前必须停下来。一个精力充沛的开发人员比熬夜劳累过度的程序员效率要高很多。(JB Rainsberger在《经济学中的软件设计》中分享过于劳累的工作会使其完全徒劳无功)
    • 结对编程是一种技能,必须自觉地学习(它可能是更具挑战性的一些人格类型,应得到尊重)
    • 与手动更改相比,更喜欢使用IDE重构——f.ex.我们之前从未使用过的“内联”重构,而Kent一直在使用。一旦你掌握了如何重构,你会发现,它明显比手动重构更有效率。更重要的是,避免打破东西(记住, Murphy 所说的——what can break will break)

    你可以在GitHub下载我们此次在俱乐部所开发的源码。

    总结

    我非常希望你们(亲爱的读者)能在平时的工作实践中运用这些价值观和设计原则,希望它们对你有用!

来源:苏州远鼎官网


相关标签 TAG :  创始人  极限  


苏州远鼎

运用前沿科学技术,苏州远鼎信息技术有限公司以开源管理软件产品为核心,为企业和政府组织提供软件及服务,是OpenERP(Odoo)专业服务商,中国开源管理软件服务市场的领跑者。

Read More

远鼎产品

联系远鼎

  • 苏州工业园区星湖街328号22栋301
  • +86-0512-69361217
  • odoo@chinamaker.net
  • www.chinamaker.net