2009年2月25日星期三

对底层的需求就是高层的设计么?

“对底层的需求就是高层的设计。”


我把这句话和很多软件方面的专家,系统方面的牛人还有软件开发的经理确认过,几乎所有的人都不约而同的认同这个说法。这句话听起来似乎有道理。如果上层是这样设计的,那么下层就要满足这些设计,这样这些上层的设计就成了下层的需求。


然而我认为,这是个致命的错误


“为领导服务还是为人民服务?”


Requirement和Design是软件开发的两个维度,Design用来承载Requirement的实现。就像体积和重量一样,两者不能互相转换,设计也不能变成需求。需求经过分析,可以被细化,分解。并且需求分析也可以有很多选择。但无论如何细化,如何分解,其结果还是需求。同样的,设计也有高层架构和底层实现,也有很多可选的方案。但不论是多么高层次的设计,它总还是于设计,成不了需求。


之所以说这是个致命的错误,是因为正是因为这个错误的理念,软件的设计者在不断的人为创造“需求”。架构的设计者做了大量的BDUF(Big Design Up-Front,预先的大量设计),然后把它们交给下面的工程师当做需求去实现。这时大部分的工程师工作的目的已经不再是满足客户需求了,而是满足这些人为创造的所谓“需求”。这样一来,一方面,产生了大量无用的代码及其相关工作;另一方面,如此设计并不一定能满足最终需求,往往要等到最后的集成测试才突然发现其实设计没有满足需求,这时往往已经大势晚矣。


在我们动手设计之前,需求分析应该已经完成了,否则设计什么东西?当然,除非是为了设计而设计。在迭代开发的模式下,在进入每一个迭代之前,需求一定已经是清楚的,并且在这一个迭代中要花时间为今后的迭代进行需求分析。现在流行的描述需求的方法是用user story,它有这样的格式:



做为<某用户角色>,我需要<某功能>,因为<某原因>。


这样,需求就明显的和设计区分开了。


设计应该是“进化”式的,随着需求不断的被实现,系统的架构与设计逐渐的浮现出来。而不是先有了架构再向里面加东西。软件设计和建筑设计不同,对于软件这样复杂而充满未知的系统,其设计只能是“长(grow)”出来的,而不是“计划(plan)”出来


诚然,有时系统的设计会影响到需求的细分。举一个例子:一个软件需要从网络自动下载网页进行处理,这个需求比较大,需要把它分解开才能进行迭代开发。那么根据系统架构,可以把需求分解为,1.从模拟接口(stub)下载网页进行处理;2.从真正的网络下载网页进行处理。这样做的好处不单单是让需求变小了,可以分成两次做甚至由不同的team来做。并且,它解开了网页处理和网络之间的依赖关系,方便实现依赖倒置







感谢Craig Larman的耐心指正,让我理清了对需求与设计的混乱概念。总之,需求就是需求,设计就是设计。


2009年2月19日星期四

天山童姥与大蝌蚪

金庸小说《天龙八部》中有一个奇怪的女人,名叫天山童姥。之所以说她奇怪,并不是因为她武功高强性格变态,而是因为她身材永如女童。这并不是一篇讲萝莉的文章,如果你是对那个话题有兴趣再读下去可能会浪费你宝贵的时间。
因为我又要扯到软件设计上来了。
我奇怪的是,当她练到更多的武功,需要更多的feature的时候,这个老萝莉却停止了架构的变化。比方说蝌蚪吧,长成那个样子是为了适应当前的需求——在水中生活。当需求发生变化,需要新的feature时,蝌蚪逐渐的发生了架构上的变化——变成了青蛙——两栖动物。设想蝌蚪仍用原来的架构来应对增加的新需求,像个鼻涕虫一样趴在荷叶上可能没问题,想跳起来就有难度了。
软件当时的架构往往是为了支撑当时的需求的,当不断有新的需求增加进来时架构要随之调整。可是我们的软件模块有时就像老萝莉或者大蝌蚪一样,用一成不变的架构,笨拙的应付着新的功能。
有点意思的是,我很久以前曾经工作过的一个软件模块,是在更早以前用一个当时著名的芬兰美女的名字命名的,她那年20岁。十六年过去了,当年的美女依然美丽动人,而且有了洽当耦合的结果——已是孩子她妈了,相夫教女,非常美满。而那个模块如今却已是臃肿不堪了。早就违反了Single  Responsibility Principle,并且不断的违反着Open/Closed Principle,十六年间和无数其它模块发生了理也理不清的耦合关系。不知道是不是该把美女的名字还给人家,改名叫做天山童姥的好。
Craig Larman对我的大蝌蚪的比喻提出了批评(我当然没办法把天山童姥的事和他说,说了他也不懂)。他说蝌蚪是要注定变成青蛙的,但软件需求的变化却是不可预知的。他更喜欢园丁的比喻——园中的植物不断的生长,就像系统不断的成长一样。园丁们要不断的修剪,不能让花园变成杂乱的荒园。他说,这个比喻将会出现在他的下一本书中。

附上美女的照片,名字就不提了。为了强调本文的重点,把那个模块描写的比较不堪。其实和事实相比有很大的夸大成份。并且一直一来围绕这个模块工作的同仁们都很努力,让我多多少少有点心里不安。



2009年2月18日星期三

Suitably Coupled

“May all your objects be suitably coupled.”
                  -- Craig Larman

这是Craig Larman帮我在他的书上签名时的留言,并说当我读完他这本《Applying UML and Patterns》以后会有更多的体会。看来我要先放下手上的《Head First Design Patterns》了。不过《Head First》图文并茂,放下这么一本书而去读另一本满是小字的大厚书真是件痛苦的事。
不知道Craig怎么知道我最近在研究coupling和cohesion?但我想这只是个巧合吧。“suitably coupled”的确是一个很好的祝愿,不论是对software object还是生活中的方方面面。没有耦合,软件只是一盘散沙,实现不了任何完整的功能。没有耦合,人只是孤零零的个体,失去了生活的意义。而过度的耦合让软件难于维护。过度的耦合让生活陷入一片混乱。
没有耦合就像光棍没老婆,又没工作,没爱好,没亲戚,没朋友,然而。。。。。。
如果有一个老婆,又偷偷的去和N个女人有关系统。。。。。。
如果有一份工作,又在上班的时间炒炒股票、读读小说。。。。。。
如果有几个同事,他们同时又是你的亲戚。。。。。。
如果你的朋友要贷款,又要找你来担保。。。。。。
如朋友找你担保贷款买你们公司的股票,而你老婆的表弟是你的下属一直在掏公司的墙角,还和你的小秘勾勾搭搭,而小秘就是找你担保的那个朋友。。。。。。

也许我对耦合的理解还很有限。所以我要暂停前面一直在写的“解耦合手段”系列,先读读这本书,然后再来继续。

"通才"还是"专才"

在采用繁捷开发的组织当中,一个经常让人困惑的问题是“我们到底需要通才(generalist)还是专才(specialist)?”

传统的软件开发组织模式中(尤其是大型软件产品)通常按照产品的部件来组织的(component team)。Team中所有的人都是该部件的专才。这种组织方式的核心问题是每个team都是围绕组件工作,而不是真正的围绕customer value。从而产生很多问题,例如过份的设计,专门的测试部门,无法解开的依赖关系,资源的浪费等等。
在繁捷开发中主张功能团队(feature team)。Team是多功能的,他们围绕着完整的customer feature做所有需要的工作,不论完成这个feature需要用到哪些component,哪些技能(需求分析、设计、编码、测试)。并且到底做什么feature是由客户的优先级决定的,而不是team本身的能力范围。这样就可以实现迭代开发。很多人,包括很多宣扬繁捷开发的人,会简单的把这种情况描述为“所有的人能做所有的事”。也就是说把所有的人都变成generalist。
然而事实往往远非这样的简单。
软件产品是复杂的(complex),这种复杂性存在于软件各个组件的细节当中,任何对这种复杂的不敬和无知都早晚有一天会反扑回来,使得软件变得混乱(chaotic)(软件的复杂理论)。我的经验是,在一个大型的软件产品当中寥寥几个胆敢声称“所有的人能做所有的事”的人,要么是天才,要么是受了“达克效应”的影响。
并且,在Capers Jones的研究中发现,“专才”的表现远远超越了“通才”。
看来,又是一个需要中庸之道出马的问题。“专才”或是“通才”也许都不是解决问题的好方法,需要一个平衡点。
在Bas Vodde和Craig Larman合著的新书《Scaling Lean & Agile Development》中有专门关于组建feature team的一章,有得下载。其中给出了很多实用的建议。虽然,我认为Bas本身已经有点落入了“所有的人能做所有的事”的魔道,但也不过是离“中庸”左了一点。
书中提到“generalizing specialist”,即承认specialist的重要性,又鼓励扩展范围,同时关注customer value,尽量提高人员的利用率。并且提出在大型的项目中,按照相近的需求能力范围,以requirement area的方式来进行较高层次的组织。这样,尽量的发挥feature team的能力,减少人力空置的浪费,又减少了不断切换知识的浪费。

从管理的角度来讲,这也有很重要的指导意义。要为新人创造成为某一方面specialist的机会,鼓励specialist扩展技能。同时又要引导员工关注customer value。并不是所有的技能都会永远是创造客户价值的热点,而人的时间和精力是有限的。员工往往希望成为某方面的specialist从而获得职业安全感,这并无可厚非,也有积极一面。然而为了创造更多的客户价值往往需要进行取舍。最终还是要把对员工作的要求提高到“craftsman”的水平上来。

这样就又带来了一个新的管理问题:“要不要事先计划人员的能力分布呢?”如果不计划,就会出现对热点需求缺少准备的问题;如果计划了,就违反了Lean Thinking中“减小库存”的要求(把员工事先学好的能力想像成库存)。关于这个问题,Craig Larman认为,这种事先的计划还是必要的,这是一种必要的付出。然而Bas Vodde却对此有很极端的看法,他认为事先学习和用到了再学是一样的。对于Bas的想法很难理解,下次遇到他要再次好好请教一下,然后来专门来写写这个主题。

最后,不一定恰当的引用Craig Larman对“关注客户价值”的一个比喻:
“Watch the baton, not the runners.”
(解释一下:baton:接力棒,runner:接力运动员。在接力比赛中baton就是我们要deliver的value,提高每个runner的利用率对最后的结果并没什么帮助,每个人只跑一百和每个人都跑400是一样的。)





2009年2月12日星期四

色即是空,空即是色,TDD,Software Design

有幸成为Craig Larman大师的host,在大师来访的这三个星期安排大师的行程,聆听大师的教诲。Craig Larman是全球敏捷方法与软件分析设计领域颇有名气的大师。大师言行非常严谨,讲究研究与调查,从不空谈所谓“opinion”。这里几天打算随笔罗列些大师的名言,作为记录。其中自然会隐去一些和公司产品相关的内容,以免不必要的麻烦。

这天和一些在用TDD(Test-Driven Development)方法进行开发的工程师做讨论,说到了软件设计的问题。我们是否应该在开始实现,或者说开始写代码之前做Design?
Craig Larman引用禅宗至理(Zen Buddhism)来比喻软件设计:
“Before enlightenment, mountains are mountains and rivers are rivers. During enlightenment, mountains are no longer mountains and rivers are on longer rivers. After enlightenment, mountains are still mountains and rivers are still rivers.” (高深吧-_-!)

回去查了一下,“enlightenment”即佛家所讲的“悟”,孙悟空的悟。汗一个先。我想那意思就是说:我们刚接触Software Design时,感觉它也就是那么点事情,很自然。可当我们悟出一些软件的道理后,忽然发现Design已经不是Design了,变得非常复杂让我们无能为力,所谓“色即是空”。这时我们不是过份Design就是没了Design。等我们大彻大悟之时,发现,Design其实还是那么点事情,即所谓“空即是色”。这似乎与前几天在电视上见到的刚刚去逝的禅宗大师圣严法师的“本来面目”有异曲同工之妙:

过份的设计,或者叫BDUF(Big Design Up-Front),往往是有害而无宜的。BDUF一方面否定了软件设计是演化而来的,容易产生僵死的框架;另一方面为现在用不到的功能花精力,违反了lean的原则。例如冗长的软件设计文档、长达三个月的软件设计而不写一行代码,等等。都是我们在领悟软件工程的过程中附会到Design上去的东西。
而完全依靠测试驱动的开发方式,或者用户需求驱动的开发方式也不会自然而然的产生好的软件设计。如大师所言:“TDD can also generate really bad design”。因此,依据设计原则进行合理的Design Up Front也是必须的。

所以我想大师是要我们在纷乱的敏捷方法、软件理念之余还原软件设计的本来面目。例如在开始实现一段功能前开个简短的Design workshop,画画图,写一个快速原型或者伪代码等等。

另外,也有人讲“Before enlightenment chop wood and carry water. After enlightenment, chop wood and carry water.”:-)

2009年2月8日星期日

解耦合手段之六:并发






什么?并发也是一种解耦合的手段么?
是的。
一般的程序都是顺序执行的,在这种情况下,程序的执行和执行的时间是相互依赖的或者说是耦合的。当这种耦合成为一种阻碍时我们就需要并发(Concurrency)。
"Concurrency is a decoupling strategy. It helps us decouple what gets done from when it gets done."
 --Clean Code, Robert C. Martin

一个很常见的错误观点是并发总是能够提高性能。这在大多数情况下,尤其是单核系统中往往是不成立的,就像1+1+1+1+1和1*4都等于4一样。
我能想象到的采用并发的理由总结一下有以下几点:
  • 解开what和when的耦合以后,程序的结构更容易被理解。例如用一个大的循环来处理所有用户的聊天请求,就不如用不同的线程来处理每个用户的请求那么清晰,而且也更方便扩展对用户的服务。
  • 需要提高响应输入的速度,或及时的输出中间结果。
  • 需要利用多核,多CPU,多个机算机甚至网络的计算能力。
并发就需要用到多线程或者多进程。而在多线程或多进程编程的时候我们往往又需要或不得不把其它耦合性带回到程序里去。例如对共享数据的操作需要互斥,有些业务逻辑要求有顺序,同时还要避免可能发生的死锁。因此,结果可能是我们引入了更多的耦合性。例如前面提到的聊天程序。因为每个请求都很简单,处理得很快,因此即使是上千用户一起使用单一线程的服务也是感觉不到有什么问题。然而采用多线程处理就要引入很多其它负载,反而可能降低性能或引入更多问题。但如果每个用户的服务内容很复杂,单一线程又会变得很难理解或很难处理。
这个时候,合理的选择串行和并发以及并发的手段就成为降低耦合性的关键。如果我们不得不选择并发,那么提高每个进程的内聚性仍是降低整体耦合性的有效手段。

现在有很多并发语言,例如传统的电信行业中使用的SDL,还有新的语言如Erlang, Stackless Python。它们往往会提供高效的进程调度机制,方便的进程间通信手段以及合理的互斥方法。但无论使用什么语言,在多么先进的框架下工作,并发编程都是非常复杂的,并且和串行的编程所需要用到的设计策略往往是完全不同的。


2009年2月5日星期四

对DRY原则的补充






为了证明代码重复对软件质量的影响,我对一些真实软件中的代码进行了分析。

分析的工具为PMD。PMD是开源软件,主要功能是对Java进行静态分析以找出bug。它的一个功能是CPD(Copy/Paste Detector),也就是找出重复代码,也部分支持C语言。以下是我对一些C语言程序进行分析的结果。由于CPD工具本身的限制,不能很完美的找出C语言中的全部重复,例如对于常量的改动无法识别。因此代码中实际的重复应该比下表中的值要大一些。不过相信做为相对值还是有意义的。
















































































Source Code NLOC LOCdup60 LOCduP30 RATE60 RATE30
SW1(Too many bugs found) 44325 9314 26504 21% 60%
SW2(Too many bugs found) 39164 7264 19010 19% 49%
SW3(Too many bugs found) 109807 18903 49264 17% 45%
SW4(Many bugs found) 20050 2881 6910 14% 34%
SW5(A few bugs found) 12471 270 2141 2% 17%






Python1.5(Core) 19418 1072 3023 6% 16%
Python2.5(Core) 35797 1656 6441 5% 18%
Python3.0(Core) 40737 3460 9076 8% 22%
Apache(server) 18693 1114 2553 6% 14%
其中:
NLOC:净代码行数。
LOCdup60/30:超过60/30个token重复的代码行数。
RATE60/30:重复代码占总代码的比例。

其中的SW1~SW5是来源于同一个产品的5个不同模块(其中SW5的bugs found数目还有待收集)。通过比较可以看出,无论是RATE60还是RATE30都和代码中BUG的数量有一定的相关性。代码重复得越多就越容易产生BUG。当然,BUG的产生可能有很多原因,代码重复只是众多的原因之一,但它显然是很重要的原因。代码的过度重复往往意味着缺少谨慎的设计和及时的重构,这种代码往往是在匆忙之中或压力之下产生的,也可能是写代码的人不够投入。维护过度重复的代码难度很高,而且再次引入新BUG的风险会高到超出100%(这是真的,平均每改一个错误,引入两个错误)。

我同时也对一些流行的开源软件进行了同样的统计。Python和Apache都是相当流行的开源软件,它们的成功必然依赖于其较高的可维护性。这里我只分析了他们的一部分核心代码,一方面是为了方便,另一方面也是为了和前面的软件模块的大小有可比性。可以很容易的看出,Python和Apache的代码重复是相对很低的。
其中Python1.5、Python2.5和我取的Apache源码都是久经考验的成熟版本,他们的RATE60都在6%左右,RATE30也只有16%左右。Python3.0最近才刚刚发布,代码是原来的114%,重复代码却是原来的209%!(这里忽略了Python的另一个主要版本2.6,因为2.6也是刚发布不久,而且主要目的是为了向3.0过渡)可见,Python3.0的质量让人有点小小的担心啊。





2009年2月4日星期三

解耦合手段之五:Dependency Inversion Principle

通常,我们认为上层模块理所当然的依赖于下层模块的服务,例如业务模块对数据库的依赖,通信模块对网络的依赖。按照这种逻辑,如果要设计一款电视的话,也许会是这样:
class TVSet{
   private Program program = LocateProgramFromRadiobBroadcast();
   void Play() {
       Show(Program);
   }
};
 因为电视依赖于节目源,所以电视自己创建了节目源,或者说自己来定位节目源。在上面的例子中是定位一个无线广播节目。但这样的电视有个问题,它只能播广播节目。生产厂商要对电视进行测试也要依赖于电视台的广播。因此,我们的电视通常都不是这样的,而是专门有接口,由使用者提供节目源:
class TVSet{
   void Play(Program program) {
       Show(Program);
   }
};
这样,不论是测试用的简单信号发生器,还是DVD,电脑都可以通过电视播放了。
这种依赖关系的倒置被称为Dependency Inversion,又被称为控制反转(Inversion of Control)。这种原则又被戏称为“好莱坞原则”——“Don't call us, we'll call you.”。Uncle Bob是这样描述DIP(Dependency Inversion Principle)的:
A. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
B. 抽象不应该依赖于细节,细节应该依赖于抽象。

依赖倒置使得系统的耦合性降低,模块的可重用性提高,并能提高系统的可测性和可用性。尤其是对单元测试有至关重要的作用,因为这样就可以用mock object来代替真实的对象。
实现依赖倒置的技术被称为“依赖注入”(Dependency Injection)。通常,当一个模块需要一种服务的时候,它要么直接持有对此服务的索引,要么通过一个服务定位器(ServiceLocator)来获得对此服务的索引。而采用依赖注入的方法,则是从外界传入对此服务的索引。例如通过传入参数,或者回调函数来设置服务。
Martin Fowler把依赖注入的方法分为三类:
  • Interface injection
  • Setter injection
  • Constructor injection

甚至有很多框架工具可以支持我们方便的在代码中实现Dependency Injection,例如google Guice。