我用OO技术第一ơ设计Y件的时候,犯了一个设计者所能犯的所有错误。那是一个来自国外的外包目Q外方负责功能设计,我们公司负责E序设计、编码和试?
W一个重要的错误是,我没有认真的把设计说明书看明白。功能点设计实有一些问题,按照他们的设计,一个重要的程是无法实现的。于是我在没有与投资Ҏ通的情况下,擅自改动了设计,把一个原本在Linuxpȝ上开发的模块改到了Windowspȝ上。结果流E确实是实现了,但是很不q,Ҏ不符合他们的需要,比v原先的设计差的更多。在询问了这个流E的设计意图之后Q我也清楚了q一炏V对方的工程师承认了错误Q但是问题是Q?#8220;Z么不早说啊,我们都跟领导讲过了品的构架Q也保证了交货时间了Q现在怎么去说啊?”。他们设计的是一个苹果,而我造了一个桔子出来。最后和工程师商议的l果是:先把桔子Ҏ设计书上的苹果,按时交货Q然后再悄悄的改成他们真正需要的香蕉。的q时候距M货的旉已经不三天了,于是我每天加班工作到天明Q把代码逐行抽出来,用gcc~译调试。好在大部分都是体力z,没有什么技术含量,即在深夜大脑半休眠的情况下仍然可以接着qӀ?
目中出现的另外一个错误是Q我对工作量的估计非常的不准。在W一个阶D늚时候,按照功能设计说明书中的一个流E,我做了一个示例,用上了投资方规定的所有的技术。当我打开览器,看到面上出C数据库里?#8220;TomQJerryQ王帅”Q就愉快的跑到走廊上d怺一口新鲜空气,然后乐观的认为:设计书都已经写好了,CZ也做出来了,剩下的事情肯定就象砍瓜切菜一样了。不是把大家召集v来讲讲设计书Q看看示例,然后扑上d工,然后大功告成。我为每个画面分配的~码工作量是三个工作日。结果却是,他们的设计ƈ不完,我的理解也ƈ不正,大家的思想也ƈ不一致。于是我天天召集开会,朝o夕改Q不断返工。最后算了一下,实际上写完一个画面用的时间在十个工作日以上。编码占用了太多的时_试在匆忙中草草了事Q质?#8230;…能掩盖的问题也就只好掩盖一下了Q性能更是无暇֏了?
q有一个方面的问题是出在技术上的,q方面是我本文要说的重点。按照投资方的方案,pȝ的主体部分需要用J2EE框架Q选择的中间g是免费的JBoss。再加上Tomcat作ؓWeb服务器,Struts作ؓ表示层的框架。他们对于这些东西的使用都是有明目的,但是我ƈ不了解这些技术。新手第一ơ进行OO设计Q加上过多的新式技术,于是出现了一大堆的问题。公司原本安排了一个牛人对我进行指|他熟悉OO设计Qƈ且熟悉这些开源框Ӟ曄读Tomcat和Struts源代码。可是他实太忙Q能指导我的旉非常有限?
投资方发来设计书以后Q很快就z来了两个工E师对这个说明书q行讲解。这是一个功能设计说明书Q包括一个数据库设计说明书,和一个功能点设计说明。功能点说明里面叙述了每一个工作流E,画面设计和数据流E。两位工E师向我们简单的说明了品的构想Q然后花了一个多星期的时间十分详l的说明了他们的设计Q包括数据表里每一个字D늚含义Q画面上每一个控件的业务意义。除了这些功能性的需求以外,他们q有一些技术上的要求?
Z减少客户的拥有成本,他们不想品绑定在特定的数据库和操作系l上Qƈ且希望用免费的q_。于是他们选择了Java作ؓ开发语aQƈ且用了一pd免费的^台。选用的中间g是JBossQ用Entity Bean作ؓ数据库访问的方式。我们对Entity Bean的效率不攑ֿQ因为猜他q用了大量的反射技术。在l过一D|间的技术调查之后,我决定不采用Entity BeanQ而是自己写出一大堆的Value ObjectQ每个Value Object对应一个数据库表,Value Object里面只有一些setter和getterҎQ只保存数据Q不做Q何事情。Value Object的属性与数据库里面的字段一一对应。与每个Value Object对应Q做一个数据表的GatewayQ负责把数据从数据库里面查出来塞到这些Value Object里面Q也负责把Value Object里面的数据塞回数据库?
按照q样的设计,需要ؓ每一个数据表写一个Gateway和一个Value ObjectQ这个数量是比较庞大的。因此我们做了一个自动生成代码的工具Q到数据库里面遍历每一个数据表Q然后遍历表里面的每一个字D,把这些代码自动生成出来?
q等于自己实C一个ORM的机制。当时我们做q些事情的时候,ORMq是一个很陌生的名词,Hibernateq样的ORM框架q没听说q。接着我们q是需要解决系l在多种数据库上q行的问题。Gateway是用JDBCq接数据库的Q用SQL查询和修Ҏ据的。于是问题就是:要解决不同数据库之间SQL的微差别。我是这样干的:我做了一个SqlParser接口Q这个接口的作用是把ANSI SQL格式的查询语句{化成各种数据库的查询语句。当然我没必要做的很全面Q只要支持我在项目中用到的查询方式和数据cd够了。然后再开发几个具体的Parser来{换不同的数据库SQL格式?
到这个时候,数据库里面的数据成功转化成了E序里面的对象。非常好Q按道理_剩下的OO之\p理成章了。但是,很不q,我不知道该怎样用这些Value ObjectQ接下来我就怀着困惑的心情把q程式的代码嫁接在这个OO的基上了?
我ؓ每一个画面设计出了一个Session BeanQ在q个Session Bean里面装了画面所兌的一切业务流E,让这个Session Bean调用一大堆Value Object开始干zR在Session Bean和页面之_我没有让他们直接调用Q因为据公司的牛Q?#8220;面直接调用业务代码不好Q耦合性太强?#8221;q句话没错,但是我对“业务代码”的理解实在有问题Q于是就生生的造出一个Helper来,L在页面和Session Bean中间Q充当了一个传声筒的角艌Ӏ?
于是在开发中出C下面q副景象Q每当设计发生变_我就要修Ҏ据库的设计,用代码生成工具重新生成Value ObjectQ然后重C改Session Bean里面的业务流E,按照新的参数和返回g改Helper的代码,最后修攚w面的调用代码Q修攚w面样式?
实际情况比我现在说v来复杂的多。比如Value Object的修改,E序规模来大以后Q我Z避免出现内存的大量占用和效率的下降,不得不把一些数据库查询的逻辑写到了Gateway和Value Object里面Q于是在发生变更的时候,我还要手工修改代码生成工L成的Gateway和Value Object。这Ll护十分ȝQ这使我困惑OO到底有什么好处。我在这个项目中用OO方式解决了很多问题,而这些问题都是由OO本n造成的?
另一个比较大的问题出在Struts上。投资方为系l设计了很灵zȝ界面Q界面上的所有元素都是可以配|出来,包括位置、数据来源、读写属性。ƈ且操作员的权限可以精到每一个查看、修改的动作Q可以控制每一个控件的d操作。于是他们希望用Struts。Struts框架的每一个Action恰好对应一个操作,只需要自己定义Action和权限角色的关系Q就可以实现行ؓ的权限控制。但是我错误的理解了Struts的用法,我ؓ每一个页面设计了一个ActionQ而不是ؓ每一个行计一个ActionQ这h本就无法做到他们惌的权限控制方式。他们很快发C我的问题Q于是发来了一个说明书Q向我介lStruts的正用方式。说明书打印出来厚厚的一本,我翻了一天,l于知道了错在什么地斏V但是一大半画面已经生米煮成熟饭Q再加上我的Session Bean里面的流E又是按画面来封装的Q于是只能改造小部分能改造的画面Q权限问题另扑֊法解决了?
下面是q个pȝ的全貌,场面看上去还是蔚为壮观的Q?
pȝl历q数ơ较大的修改Q这个框架不但没有减d更的压力Q反而得变更困隑֊大了。到后来Q因Z务流E的变更的越来越复杂Q现有流E无法修改,只得用一些十分曲折的方式来实玎ͼq行效率来低。由于结构过于复杂,Ҏ没有办法q行性能上的优化。ؓ了^衡效率的延缓Q不得不把越来越多的Value Object攑֜了内存中~存hQ这又造成了内存占用的急剧增加。到后期调试E序的时候,服务器经常出?#8220;Out of memory”异常Q各cd象庞大繁多,pȝ~译部v一ơ需?0多分钟。投资方原先是希望我们用JUnit来进行单元测试,但是q样的流E代码测试v来困N重,要花费太多的旉和h手,也只得作|。此外他们设计的很多功能其实都没有实玎ͼq且g以后也很隑ֆ实现了。设计中预想的很多优U特点在这h架中一一消失Q大家无奈的接受一个失望的局面?
在我d公司两年以后Q这个系l仍然在持箋开发中。新的模块不断的dQ框架上不断d新的功能炏V有一ơ遇C然在公司工作的同事,他们_“q是原来那个框架Q前台加上一个个的JSPQ然后后台加上一个个的Value ObjectQ中间的Session Bean装来多的业务流E?#8221;
我的W一个OOpȝ的设计,表面上用了OO技术,实际上分析设计还是过E主导的方式。设计的时候过多、过早、过深入的考虑了需要做哪些画面Q画面上应该有哪些功能点Q功能点的数据流E。再加上一个复杂的OO框架Q名目繁多的对象Q不仅无法做到快速的开发,灉|的适应需求的变化Q反而ɾpȝ变得更加复杂Q功能修Ҏ加的ȝ了?
在面条式代码的时代,很多人用汇编代码写出了一个个优秀的程序。他们利用一些工P或者共同遵守一些特别的规约Q采用一致的变量命名方式Q规范的代码注释Q可以一个庞大的开发团队运行的井井有条。h如果有了先进的思想Q工具在q些人的手中可以发挥出越时代的能量。而我设计的第一个OOpȝQ恰好是一个相反的例子?
实际上,面向对象的最独特之处Q在于他分析需求的方式。按照这L方式Q不要过分的U缠于程序的画面、操作的q程Q数据的程Q而是要更加深入的探烦需求中的一些重要概c下面,我们通过一个实例看一看,怎样L住需求中的这些重要概念,q且q用OOҎ把他融合到程序设计中。也看看OO技术是如何帮助开发h员控制程序的复杂度,让大家工作的更加单、高效?
我们来看看一个通信公司的̎务系l的开发情c最开始,开发h员找到电信公司的职员询问需求的情况。电信公司的职员是这栯的:
“账务pȝ主要做这样几件事情:每个?日凌晨按照用户用情는成̎单,然后用预存冲销q个账单。还要受理用L~费Q缴费后可以自动冲销Ơ费的̎单,Ơ费用户~清费用之后要发指oC换上Q开启他的服务。费用缴清以后可以打印发,发票是下面q个样子?#8221;
l过一番调查,开发h员设计了下面几个主要的流E:
1?̎Q根据一个月内用L消费情况生成账单Q?
2?销账:冲销用户账户上的余额和̎单;
3?~费Q用户向自己的̎户上~费Q缴清欠费后打印发票?
弄清了流E,接着p计用L面来实现q样的流E。下面是其中一个数据查询界面,分ؓ两个部分Q上半部分是~费信息Q记录了用户的缴费历Ԍ下半部分是̎单信息,昄账单的费用和销账情c?
界面上的数据一眼看h很复杂,其实l合̎、缴贏V销账的程讲解一下,是比较容易理解的。下面简单说明一下?
~费的时候,在缴费信息上d一条记录,记录下缴贚w额。然后查找有没有Ơ费的̎单,如果有就做销账。冲抉|费的金额记录?#8220;Ơ费金额”的位|。如果欠Ҏ间较长,p滞U金Q记录在“滞纳?#8221;的位|上。冲销Ơ费以后Q剩余的金额记录?#8220;预存?#8221;的位|上?#8220;其他费用”q个位置是预留的Q目前没有作用?
每个月出账的时候,在̎单信息里面加上一条记录,记录下̎单的应收和优惠,q两部分相减是账单的总金额。然后检查一下̎户上有没有余额,如果有就做销账。销账的时候,预存Ƒֆ销的部分记录在“预存划拨”的位|,如果不以冲抉|费,账单暂时处?#8220;未缴”状态。等Cơ缴费的时候,冲销的金额再记录?#8220;C?#8221;的位|。等到所有费用缴清了Q̎单状态变?#8220;已缴”?
销账的程p栯合在~费和出账的q程中?
看v来一切成功搞定了Q最重要的几个流E很明确了,剩下的事情无疑就像砍瓜切菜一栗无非是l着q几个流E,设计出其他更多的程。现在有个小问题Q打印发的时候,发票的右侧需要有上次l余、本ơ实~、本ơ话贏V本ơ结余这几个金额?
上次l余Q上个月账单销账后剩下来的金额Q这个容易理解;
本次l余Q当前的账单销账后剩下的金额,q个也不难;
本次话费Q这是̎单的费用Q还是最后一ơ完全销账时的缴费,应该用哪一个呢Q?
本次~费Q这个和本次话费有什么区别,他在哪里出来?
带着问题Q开发者去问电信公司的职员。开发者把他们设计的界面指点给用户看,向他说明了自q设计的这几个程Q同时也说出了自q疑问。用h有直接回{这个疑问,却提Z另一个问题:
“~费打发这个流Eƈ不Lq样的,~费以后不一定立刻要打印发票的。我们的用户可以在银行、超市这L地方~话费,几个月以后才来到我们q里打印发票。ƈ且缴费的旉和销账的旉可以相距很长的,可以先缴U一W话费,后面几个月的账单都用q笔钱销账;也可以几个月都不~费Q然后缴U一W费用冲销q几个̎单。你们设计的q个界面不能很好的体现用L~费和消Ҏ况,很难看出来某一ơ缴Ҏ在什么时候用完的。必MW一ơ、或者最后一ơ缴费余额推这个历Ԍ太麻烦了。还有,‘预存划拨’?#8216;C?#8217;q两个概忉|们以前从来没有见q,对用戯释v来肯定是很麻烦的?#8221;
开发h员^静了一下自己沮丧(或愤怒)的心情,仔细想一惻Iq样的设计确实很不合理。如果一个会计记L账本来,他肯定会被老板开除的?
看v来流E要改,比先前设计的更加灉|Q界面也要改。就好像原先盖好的一栋房子忽然被捅了几个H窿Q变得四处透风了。还有,那四个数值到底应该怎样计算出来呢?我们先到走廊上去呼吸两口新鲜I气Q然后再回来x吧?
现在Q让我们先忘记这几个变化多端的流E,׃Ҏ间想一x基本的几个概念吧。系l里面最显而易见的一个概忉|什么呢Q没错,是̎PAccountQ。̎户可以缴费和消费。每个月消费的情冉|记录在一个̎单(BillQ里面的。̎户和账单之间是一对多的关pR此外,账户q有另一个重要的相关的概念:~费QDepositQ。̎户和~费之间也是一对多的关pR在我们刚才的设计中Q这些对象是q样的:
q个设计看来有些问题Q用了一些用户闻所未闻的概念(预存划拨Q新交款Q。ƈ且他分离了缴费和消费Q表面上很清楚,实际上账单的查询变得困难了。在实现一些功能的时候确实比较简单(比如~费和销账)Q但是另一些功能变得很困难Q比如打印发)。问题到底在什么地方呢Q?
涉及到̎务的行业有很多,最Ҏ惛_的也许就是银行了。从银行w上Q我们是不是可以学到什么呢Q下面是一个银行的存折Q这是一个委托收Ƅ账号。用户在账户上定期存钱,然后他的消费会自动从q里扣除。这个情景和我们需要实现的需求很怼。可以观察一下这个存折,存入和支取都是记录在同一列上的,在支出或者存入的右侧记录当时的结余?
有两ơ̎户上的金额被扣除?Q这时候金额已l被全部扣除了,但是消费q没有完全冲销。等到再ơ存入以后,会l支取。这U记账的方式是最基本的流水̎Q每一条存入和支出都要记录Z条̎目(EntryQ。程序的设计应该是这P
q个l构看上d刚才那个g没有什么不同,其实差别是很大的。上面的那个Deposit只是~费记录Q这里的Entry是̎目,包括~费、扣贏V滞U金……所有的费用。销账扣费的q程不应该记录在账单中,而是应该以̎目的形式记录下来。Account的代码片D如下:
Entry有很多种cdQ存入、支取、滞U金、赠送费Q,可以考虑可以为每一U类型创Z个子c,像q样Q?
搞成父子关系看v来很复杂、麻烦,q且目前也看不出这些类型作为Entry的子cL哪些好处。所以我们决定不q样做,只是单的把这几种cd作ؓEntry的一个属性。Entry的代码片D如下:
Entry是一个枚丄型,代码如下Q?br />
下面的界面显C的是刚才那个账户的̎目。要昄q个界面只需要调用Account的GetEntreesҎQ得到所有的账目Q然后按旉序昄出来。这个界面上的消Ҏ况就明确多了Q用户很Ҏ弄明白某个缴Ҏ在哪几个月䆾被消Ҏ的?
q且Q发上的那几个一直搞不明白的数g有了{案。比?005q?月䆾的发,我们先看?005q?月䆾销账的所有̎目(W六行、第八行Q,q两ơ一共扣?3.66元,q个金额是本次消费Q两ơ扣除之间存?00元,q个是本次实缴Q第五行的结余是17.66元,q就是上ơ结余;W八行上的结余是144元,q个是本次l余?
用户查了q个设计Q觉得这L费用昄明确多了。尽一些措辞不W合习惯的业务词汇,但是他们的概念都是符合的。ƈ且上ơ还有一个需求没有说Q有时候需要把多个月䆾的发合在一h印。按照这L账目表达方式Q合q的发票数g比较Ҏ搞清楚了。明了q样的对象关p,实现q个需求其实很Ҏ?
面向对象的设计就是要q样Q不要急于定pȝ需要做哪些功能点和哪些界面Q而是首先要深入的探烦需求中出现的概c在具体的流E不甚清楚的情况下,先把q些概念搞清楚,一个一个的开发出来。然后只要把q些做好的零件拿q来Q千变万化的程其实变得很单了Q一番搭U木式的装配可以比较轻杄实现?
另一个重要的cd也渐渐清晰的现在我们的眼前Q̎单(BillQ。他的代码片D如下:
BillcL两个与滞U金有关的方法,q开发者想C原先忽略的一个流E:计算滞纳金。经q与电信公司的确认,军_每个月进行一ơ计滞U金的工作。开发h员写了一个脚本,先得到系l中所有的Ơ费账单Q然后一一调用他们的CaculatePenaltyҎ。每个月这个脚本执行一ơ,可以完成滞U金的计工作?
Bill对象中有账户的基本属性和各账目的金额和销账的情况Q要打印发票Q只有这些数值是不够的。还要涉及到上次l余、本ơ结余和本次实缴Q这三个数值是需要从账目中查到的。ƈ且发有严格的格式要求,也不需要显C用的l节Q只要显CZU和二的费用类可以了。应该把q些东西另外装成一个类Q发(InvoiceQ:
通信公司后来又提Z新的需求:有些账号和银行签订了托收协议Q每个月通信公司打印些̎L托收单交l银行,银行从个人结̎户上扣除q笔钱,再把一个扣费单交给通信公司。通信公司Ҏq个扣费单冲销用户的欠贏V于是开发h员可以再做一个托收单QDeputyBillQ:
账单中的GetFeeҎ的返回值类型是FeeQFeecd包含了费用的名称、金额和他包含的其他费用。例如下面的情况Q?
我们可以用这L一个类来表C用(FeeQ,一个费用可以包含其他的费用Q他的金额是子费用的金额和。代码片D如下:
现在开发者设计出了这么一堆类Q构成Y件系l的主要零gp么制造出来了。下面要做的是把这些零件串在一Pd现需要的功能。OO设计的重点就是要扑ֈq些零g。就像是设计一辆汽车,仅仅知道油\、电路、传动的各项程是不够的Q重要的是知道造一辆汽车需要先刉哪些零件。要x的把这些零件设计出来不是一件容易的事情Q很有开发者一开始就了解pȝ的需求,设计出合理的对象关系。根本的原因在于领域知识的乏,开发者和用户之间也缺乏必要的交流。很多h在Y件开发的q程中才渐渐意识到原来的设计中存在一些难受的地方Q然后探索下去,才知道了正确的方式,q就是业务知识的一个突破。不q的是,当这个突破到来的时候,E序员经常是已经忙得热火朝天Q快把代码写完了。要把一切恢复到正常的轨道上Q需要勇气,旉Q有q见的领D,也需要有q气?/p>