|
最近听到不少人总是叨念着"细节决定成败"这句话,颇像是每天清晨必修的那句"all money go my home"一样。决定成败的因素很多,为什么细节能够成为压倒一切的关键。
拿产品来说吧,产品的细节处很重要,可能客户的取舍在毫厘之间。但这是否是事实之全部。其实客户需要的是满足自己的价值目标,他真的那么在意主要目标之外
的细节(甚至是刻意造作的细节)吗。制造细节的目的只是为了制造与同类产品的不同,这是市场进入完全竞争状况的标志。只是有意思的是,凡是中国人涌入的领
域,很快就能造成完全竞争的局面。就像我们小区里的煎饼摊,今天刚开张,明天周围马上冒出两家一样的,你两元一份,我两元一份加一个鸡蛋,他两元一份加一
个鸡蛋再送一碗豆浆。很快,经过一场血拼,几家都难以维系,最终撤摊了事,于是小区内就再也没煎饼卖了。再过些时日,有人支起一个状元饼的炉子,于是一段
新的血战征程又开始了。最大的利益永远源于创造。当然,创造是需要成本的,现在财大气粗的主多半没有创造的激情,而被创造力冲昏头脑的家伙却多半在社会的
底层挣扎。
项目的细节处也很重要,也许见了领导少哈一个腰,过节的时候少送了一份礼,就将项目引至黑暗的深渊。为什么世界是如此的不稳定,要受到细节的摆布。在国
内,操作多不规范,因为利益关系,两两联系,因人而异的情况很多,缺乏一种外在的制度性的保障,而个人的好恶却能在现有的体系中不断放大。结果做人优先于
做事。
对软件程序来说,细节会决定程序的生死,一个不经意的指针异常就能让整个系统崩溃。但程序员也不总是战战兢兢,如履薄冰。一种职业的素养可以消解细节的危
险。当我们养成良好的编程习惯之后,对这样的细节多半就视而不见了。更理想的方法是引入一种封装机制,例如智能指针使得我们再也不用考虑AddRef和
Release的精确配对了。而java引入的则是新的世界,野指针这个细节在新世界中被消灭了,我们也不需要这方面的什么个人素质了。细节处千变万化,
无一定之规。也许我们最需要的不要应对细节的技巧,而是能够屏蔽和规范细节的规则。细节不是我们的目标,
一组统一简明的游戏规则才决定着所有博弈参与者的成败。
在我看来,软件开发就是一个从二进制指令构造出一些高级结构的过程(from
binary chaos to artificial
intelligence)。这种构造依赖于我们控制各种结构的能力。结构化编程向我们展现了一个机械化的分解与合成的世界,但这个世界与我们的真实世界
却差异良多。于是,面向对象编程试图直接跳跃到真实的世界,依赖于我们对真实世界中结构的控制能力,直接对真实的结构建模。早期面向对象技术的陈述中充斥
着这种乌托邦式的理想图景。但是这种隐喻是含混的,两个世界的巨大差异造成了必然的转换成本,我们只能压缩这种成本而不可能完全抛弃它,我们必须要经历一
系列的中间过程,经历一个对结构问题进行深刻思考的过程。近几年软件技术在控制抽象结构方面有了很大进展,模板,AOP, xml,
SOA等等技术并不是传统意义上的面向对象,而更像是对结构化编程时代的回归。而我们对于面向对象技术的应用也不再仅仅关注于对真实世界的建模,而是将这
种技术作为一种普适的建模方法应用于软件的方方面面。我们在编程的时候不再斤斤计较于一个Class的定义是否反映了事物的本质特征,而仅仅在意它是否有
助于我们对于程序结构的控制。这就象是神经网络和演化计算等所谓人工智能技术,早期的兴起源于一个让人心血澎湃的理想:向生物世界亿万年的智慧学习,但近
几年的发展则越来越明显的表现为一种对数学的回归。
EJB和JSP
Tag都是很好的技术,它们的最终形态都使我们拥有了更强的结构控制能力。但问题是,它们的构造成本过高,而限制了其意义的进一步引申。轻量级容器的兴起
才真正开发了AOP技术的潜力,使得Meta Object
Protocol的思想得到真正的发挥。witrix平台中的tpl模板技术将自定义tag的构建成本降到了最低:没有配置文件,使用tpl自身来构造
tag。这些努力使得tpl技术不再是象是一个帮助库,而成为一种独立的语言。应用tpl模板,我们可以随意的将小段html文本封装为有意义的tag
(这在jsp tag中是被明确反对的实践), 从而获得一种崭新的抽象能力。实际上,我认为JSF基于jsp
tag技术,在基本结构的构造方面成本过高,无论它的IDE怎样发展,最终只能成为一种界面库而不会是真正引领未来方向的技术。只有突破成本阈值,才能发
展出新的天地。
代码复用包括两个方面:概念复用和实现复用。这两者在C++的虚拟函数设计中是合二为一的,结果概念上的模糊往往造成继承机制的滥用。为了复用我们往往在
基类中塞入过多的职责,并在程序中制造了过多的层次。java的interface是纯粹的概念复用机制,实现方面的复用我们一般通过Impls类或者
Utils类来进行,即将代码片断写为静态函数。一般应该避免在类中写特别多的帮助性成员函数,因为成员函数隐含的通过成员变量相关着,比静态函数要更加
难以控制。
类是一个整体的概念,整体概念失效了,类也就不存在了。从这一点上来说,它未必是比静态函数更加稳定。概念与实现是两个不同层面的东西。实际上它们一般也
是多对多的关系。同一个概念可能换用多种不同的实现,而同一段功能代码也可能在多个类中使用。
代码复用的意义不仅仅在于减少工作量。实际上复用是对软件的一种真正的检验,而测试仅仅是一种模拟的检验而已。每一次复用都是对代码的一次拷问。在不断使
用中感受到不同使用环境中的各种压力,才能实现概念的不断精化并确保实现的正确性。
我们开发程序的目的是为了完成业务功能, 理想的情况下程序中的每一条语句都应该是与业务直接相关的,
例如程序中不应该出现连接数据库, 读取某个字段等纯技术性的操作, 而应该是得到用户A的基本信息等具有业务含义的操作. dao(data
access object)层存在的意义在于将与数据持久化相关的函数调用剥离出去, 提供一个具有业务含义的封装层. 原则上说,
dao层与utils等帮助类的功能非常类似, 只是更加复杂一些, 需要依赖更多的对象(如DataSource,
SessionFactory)等. 如果不需要在程序中屏蔽我们对于特定数据持久层技术的依赖, 例如屏蔽对于Hibernate的依赖,
在dao层我们没有必要采用接口设计. 一些简单的情况下我们甚至可以取消整个dao层, 而直接调用封装好的一些通用dao操作函数,
或者调用通用的EntityDao类等.
程序开发的过程应该是从业务对象层开始的, 并逐步将纯技术性的函数调用剥离到外部的帮助类中,
同时我们会逐渐发现一些业务操作的特定组合也具有明确的含义, 为了调用的方便, 我们会把它们逐步补充到service层中. 在一般的应用中,
业务逻辑很难稳定到可以抽象出接口的地步, 即一个service接口不会对应于两个不同的实现, 在这种情况下使用接口往往也是没有必要的.
在使用spring的情况下原则上应该避免使用getBean的调用方式, 应该尽量通过注入来获得依赖对象, 但有时我们难免需要直接获取业务对象, 在不使用接口的情况下可以采用如下方式
class TaskService{
public static TaskService getInstance(){
return (TaskService)BeanLoader.getBean(TaskService.class);
}
}
在程序中我们可以直接使用TaskService.getInstance()来得到TaskService对象.通过命名规范的约定,
我们可以从类名推导出spring配置文件中的对象名, 因而不需要使用一个额外的硬编码字符串名.
jsp模型为web程序提供了page/request/session/application这四个基础性的变量域.
这种变量域的划分很大程度上是纯技术性的, 与我们的业务应用中需要的scope支持相去甚远. 当我们把业务对象的生命周期映射到这些变量域的时候,
经常出现不适应的情况. 例如我们可能被迫选择把与某项业务相关的所有数据放置在session中并在各处硬编码一些资源清理代码.
为了实现与愈来愈复杂的应用开发的契合, 我们需要能够在程序中定义与应用相关的变量域并实现对这些变量域的管理,
即我们需要一种自定义scope的支持而不是使用几个固定的scope.
JBoss的Seam项目 http://www.jboss.com/products/seam 中引入了一种所谓declarative application state management的机制
http://blog.hibernate.org/cgi-bin/blosxom.cgi/Gavin%20King/components.html,
其中的关键是增加了business process和conversation这两个应用直接相关的scope, 它们都是可以根据需要自由创建的.
business process context使用jBPM支持long running的状态保持. 而conversation
context是对session使用的一种精细化, 与beehive项目中的page flow所需的scope支持非常类似 http://beehive.apache.org/docs/1.0m1/pageflow/pageflow_overview.html. 但目前seam中的scope支持仍是非常原始的, 不支持嵌套的context, 这意味着对于复杂应用尚无控制和管理能力.
在无侵入性的前台页面控件设计方案中, 我们需要一种简便的方法迅速定位页面中的某一节点(dom
node). 使用xpath是非常诱人的一个技术选择, 但是在实际使用中, 我们却发现xpath并不是那么方便. xpath的能力非常强大,
它支持绝对定位, 例如//input[@id='3'], 也支持相对定位, 例如 ./input[0], 甚至支持根据节点内容定位,
例如//a[contains(., 'partial text')].
问题是在一个复杂的界面控件中, html节点本身的结构与界面展现结构并不是一致的,例如一个特定效果的边框可能需要多个html元素互相嵌套才能够实现, 因此xpath的相对路径选择能力往往派不上用场(除非是提供 http://www.backbase.com/那
样的界面抽象层), 而根据内容定位的方式过于灵活, 难以维护一个稳定的概念层. 相比较而言,
css的选择符所提供的节点定位方式要比xpath更加简单直观, 它的适用性也早已在大量的实践中得到了证实.
基于css选择符实现behaviour机制是一种更加可行的方案. 参见 http://prototype.conio.net/
在软件设计中分层应该是越少越好, 过度分解一般都是有害的.
虽然说复杂的事物分解之后一般可以得到一些较简单的组成成分, 但这并不是必然有用的. 分析学成功的关键在于分解之后的组分能够出现大量重叠的情况,
参见软件中的分析学 http://canonical.blogdriver.com/canonical/555330.html
当分解到一定程度之后我们未必能够发现可以重用的部分. 而且即使分解后系统中所有的基元都是简单的,
也并不意味着整个系统就是简单的. 生物遗传密码由四种碱基构成, 但是我们理解了ATGC决不意味着我们理解了生命. 在理论上存在一种连接主义,
认为真正的复杂性蕴含在元素之间的关系之中而不在于元素自身的复杂性.
例如神经网络的研究中可调的参数多半是神经元之间的连接权重,而不是神经元本身的模型参数.
在理想中, 我们希望系统的功能划分能够泾渭分明,
一个类负责一个独立的功能实现或者一个功能侧面(aspect). 但是这只是一种乌托邦式的理想. 在物理的世界中,
我们未必能够为每一个我们思维中独立的概念找到一个稳定的物质载体. 就像是水中的漩涡, 看上去它也有固定的形状, 一定的稳定存在时间,
但是你无法说是哪些水分子参与了漩涡的构成, 实际上波的传播掠过了整个水面. 同样, 在软件中功能的归属和聚合很多时候并不是那么稳定的.
敏捷(Agile)开发的灵魂是演化(evolution),其具体的过程表现为迭代(iteration),迭代的每一步就是重构
(refactor),而单元测试(unit test)与持续集成(continuous
integration)模拟了程序生存的环境(约束),是merciless
refactoring的技术保障。从数学上我们知道迭代总有个收敛问题。一些重型方法将变化(无论是正方向还是反方向的)等价于风险,而倾向于消除开发
中的不确定性,其中的迭代是趋于迅速收敛的。敏捷的迭代是开放式的,强调拥抱变化。敏捷编程排斥过度设计,除了过度设计会增加成本之外,另一个原因就是过
度设计会阻碍重构,阻碍变化。敏捷的目标不是僵化的稳定性而是灵活的适应性。当然敏捷迭代本身并不能保证系统持久的适应性,即使是自然界中的迭代和演化,
失败的案例也是比比皆是。大量的生物物种在经历了历史的辉煌之后最终仍然难免被岁月所埋葬。
在哲学上,一个悖论式说法是有存在于无中,或者说简单才能更复杂。杯子是空的,所以能包容万物。现在什么都没做,将来才能根据需要决定如何去做。所谓鱼与
熊掌不可兼得,一旦做出了选择,可能意味着必须放弃将来进行其他选择的机会。简单的目的不仅仅是为了最快的完成当前的任务,而且要为将来保留变化的可能。
过分强调目的性,我想是违背了演化的本质。高手过招,最忌把招数用老。我们所要做的是尽量推迟决定的时刻,并切实的保证自己随时拥有选择的权利。
多样性是在演化中生存的关键。但多样性不是后天的。生物学的实验证实,物种的变异并不是环境变化后发生的,而是始终存在着并隐藏着,环境仅仅起了检选和倍
增的作用。适应性的系统总要允许一定的灰色地带,有时do something for nothing.
Agile批评过度设计(over-engineering)的声音很大,但对于设计不足(under-engineering)同样是持坚决的否定态度
的。修改过度设计的应用比修改设计不足的程序要容易的多。因为简化的途径是明确的,而走向复杂的途径却往往是难以控制的。Refactoring To
Patterns试图引入一些经验,但这些可预见的调整多半只在细节处,其影响是局部的。一个复杂性低层次的设计要支持一个复杂性高的应用,所需的代码量
不是线性的堆砌,而是几何级数式的增长,重构的时候需要做出的改变往往也是影响全局的。而事实上,设计不足是比过度设计更加常见的情况。真实的情况也许
是,在真正需要我们做出创造性设计的地方我们因为无知和无能而设计不足,而在那些渴求简单的地方,我们却自诩为先知而加上很多华丽的设计来维护虚幻的可扩
展性。这里的度是很难把握的。高段位的棋手可以比低段位的棋手预见到更多的步数,而一个优秀的软件架构师也需要比普通的程序员更早的预见到系统发展的障
碍。在我们明确可预见的范围内,当然是要把所有的设计做好,而在我们思维的边界处,"行"就变得比"思"重要了。
大谈"over-engineering"的主多半都有着丰富的过度设计的经验,千万不要把他们回顾时的话语当成是普遍的真理。所谓大巧若拙,精炼的小诗
可比长篇大论难写的多了。有时采用一种简单的处理方式,是因为我们感觉到它不会成为障碍,虽然此时并没有明确的设计过程。你必须有能力进行过度设计,才能
真正理解简单设计的精妙之处
循环结构是imperative
language的重要组成部分,一般也是程序中比较难以理解的部分。特别是没有软件技术背景的朋友,明显对于循环的理解力较弱。在Von
Neumann体系结构中,赋值语句是必须的,因而引出了存储概念,也引入了时间概念,因为我们可以区分出赋值前和赋值后的时刻。引入时间之后,本质性的
影响是程序串行化,而强迫我们从并行思考转入串行处理。很多时候这是一种不自然的情况,在我们的自然思维中,我们看到的或想到的也许只是一组静态结构,但
在程序中表达的时候却往往不可避免的需要引入一个动态过程。而我们控制动态结构的能力总是不足的。最近对于函数式语言及处理风格的越来越强烈的要求可能也
从侧面反映了大家对这种结构失配的不满。 但是串行思维毫无疑问也是我们正常思维模式的一部分(当然这种思维模式在多大程度上是因为Von
Neumann
体系造成的,也是个很有趣的问题)。例如在页面渲染的时候,我们可能希望预先把所有用到的数据都转载到内存中,赋予不同的变量名,然后在页面模板中我们只
要知道如何把这些数据变量表现出来就可以了。先做完A再做B,这是分层的思想,也是典型的串行思维。而基于数据进行处理,也是Von
Nenuman体系的基本思想。但是如果处处要求预先计算并赋值,往往增加了很多额外的步骤(glue
code),并且增大了对内存(计算空间)的需求。分层之后,还存在着一个各个层次之间结构匹配的维护问题。 面向对象在结构表达方面是一种
巨大的进步。经过多年的发展,我们在表达静态结构关系方面已经是驾轻就熟了。通过属性关联,我们可以沿着对象图进行结构遍历。如果使用成员函数,在这种遍
历过程中还可以包容更多的动态特性。而在数据持久化方面,ORM的盛行也在一定程度上证明了对象图的有效性。使用对象图可以大大降低对赋值语句的需求,减
轻了明确建模的压力(每一次赋值都要求着一个明确的变量名,一个概念),也缓解了Von Neuman体系结构的束缚。例如,我们不再需要 var user = loadUser(userId); var userOrgnization = loadOrgnization(user.orgId); var userOrgnizationName = userOrgnization.name 而是直接使用 user.orgnization.name
目前面向对象所表达的大多数结构还是基于数据语义的,而我们对于函数等高阶结构的控制能力仍然较弱。设计模式在这方面提供了一些经验,但还是远远不够的。
在我们经验不多的时候,我们需要依赖于明确的实体数据,而在我们的理解逐步深入之后我们就可以通过Visitor,
Iterator等模式支撑起整个结构。高阶结构比低阶结构难以控制,一方面是因为动态性本身比静态性难以理解,另一方面函数对信息的使用和流动是一种主
动约束,如果约束的不正确,会造成结构的失效。而数据的使用是完全开放的,很多决定都可以延迟到使用时刻决定。当然,开放性带来的问题也早就众所周知了:
不受限制的使用将导致无法控制的困境。在基础的数据层封装方面,一般我并不提倡大量使用domain
model似的具有丰富语义的数据对象。因为数据是共享的,应该存在一个开放的数据层,在其上可以建立业务对象。混杂在一起会限制系统的演化。
|