案例分析:面向对象得失论
缘起
最近一段时间,在博客园关于面向对象的讨论比较热烈,你来我往的,好不热闹。不完全归纳一下,大约有以下几种意见比较受欢迎:
A. 面向对象需要组织、团队支持,需要一种环境;
B. 面向对象比面向过程编程要复杂,需要花很大代价才能掌握;
C. 面向对象不是必须的;
D. 面向对象存在一定的性能损失。
其实每一个意见都有的一定的依据,不过这些依据都是站在自己的背景下才能站得住脚,并不是放之四海而皆准的。
面向对象是一组思维方法、分析方法和编程方法的集合,当然不是最终的结果。前段时间有一个“在项目中摸爬多年”的看似极富经验的博友,曾经对面向对象颇有微词,把项目失败中的种种罪过归咎于对面向对象的追求。当然,面向对象并不是医治百病的良药,不当的追求会导致难以预料的恶果。面对这样的“风险”,再加上面向对象貌似曲高和寡,难怪大量的技术负责人(当然包括园子里的那些技术负责人)勿宁放弃了。其实放弃也就放弃了,原因也并不一定就在于面向对象本身,不必在这种背景下对面向对象产生诸多抱怨。
案例
前段时间某个项目中有这样一个需求:将某些Word文档生生导入到系统的数据库中。这个需要是合理的,该文档是流程中的一个重要附件。重复录入不仅意味着用户需要多输入一遍,而且还增加了录入错误的风险。所以,我很乐意支持这个需求,它将让我们的系统增加一份稳定性和正确性。
其中有一个文档是这样的:
我估计很多人会选择放弃,因为他们选择结构化分析方法,很难入手。我设计这个框架用了一天,实现用了一天,测试并重构它用了两天,其中有一个夜班加到半夜四点。最后做总测试的时候居然一次成功,连我都没有想到。
看看我的思路,也许你可以从中找到在业务越来越复杂的今天,让你无法摆脱面向对象思维的众多支撑点。
A. 由于需要对Word文档事先做一个描述,所以,通过一个XML来定义文档格式是必不可少的(XML真是奇妙,让你轻松地发明一种自己的“语言”可以描述你任何想描述的东西)。
B. XML文档读出到系统中成为一个你事先设计好的对象模型,用这个模型可以对每一个Word文档实例进行检验并读出其中的敏感内容。
C. 找一个性能良好的读出器,把Word文档的内容变成另一个体系的对象模型。
D. 把以上两个文档对象模型一一匹配,并由其中的某些特殊的元素完成数据读出,且放置到事先设计好的指定地方。
单独完成A、B、C都是容易的,完成这每个部分并确认其完全可靠以后,再完成D其实只需要很少的代码了。
XML文档设计了以下元素节点:
<types>:这是一组简单类型的定义,在一个<type>中包含多个<property>节点。<property>节点则说明其类型和意义,类型可以是string/int/float/datetime/bool这五种简单类型,也可以是已经定义的<type>,还可以是某个可重复的数组。如果你愿意,可以把整个文档的数据包含在一个单一的<type>中。
<sources>:这是一组导入源的节点。可以一次导入多个相关的Word文档来完成一次导入活动。在每个<source>元素中通过<features>节点定义一组特征。特征包括:<paragraph>是段落;<table>是表格。在段落和表格中再包含子元素。最末级的叶元素是<checkbox><key>等节点。
<arguments>:这是一组必须传入的参数,这些参数的类型必须是<types>中定义的某种类型。
<extractions>:这是需要萃取敏感内容的节点。这个节点定义一组有上下级关系的<extraction>元素,将<features>中的末级元素与<type>中的<property>连接到一起。
实现读出DOM非常简单,但校验它们稍麻烦一些。几乎每一个文档节点都需要为之设计一个对象,并确保每个对象之间的完整派生关系。
读出Word文档之前,我定义了Word的文档对象模型。并分清哪些元素是用来校验文档的、哪些元素是需要从中读出内容的。每个文档都包括在一个单一的对象中。读出Word文档,需要定义一个接口,以保证可以选择多种文档读入方式。
{
DocEntry ReadDoc(System.IO.Stream source);
}
实现这个接口,我选择采用Syncfusion组件。其中的Syncfusion.DocIO.ReaderWriter非常高效,读出的结果也非常方便整理。实现这个方法我仅用了72行代码。
最后实现的Importer对象,只有一个构造器和两个方法需要暴露:
{
// 用XML定义文档创建导入器
public Importer(Stream config)
{
…
}
// 一次性加载所有的导入源
public CheckResult LoadDocuments(params Stream[] sources)
{
…
}
// 一次性将结果导入到指定的结构中
public CheckResult Import(params object[] targets)
{
…
}
}
当然,实现这个类要麻烦一些,但最麻烦的应该还是定义和实现那些对象模型元素类型,一共有两个文档对象模型需要实现,这些对象间具有某种对应关系,并且同一个对象模型间的元素间的关系也非常微妙。无论如何,这些对象都只是解决单纯的问题,所以还是很容易的。有兴趣可以下载该项目的全部源码。[声明:syncfusion并非开源产品,提供其中的某些目标文件只是为了让测试代码可以跑起来,不要用于任何商业用途,否则后果自负。]
结论
回到头先的问题。在我实践中,我不认为面向对象有什么神秘,也不认为在项目实践中必须采用面向对象。面向对象方法是众多的开发方法中的一种,与其他方法相比,没什么特别了不起的、不可替代的地方,并且对于不同的人结果可能完全不同。不过,我真正从面向对象获得的好处是能够迅速从一堆可选的方法中选择一种有效的、擅长的方法来解决,并且兼顾到将来的扩展。例如在我的这个案例中,文档表格中的行数和列数都是固定的,如果将来需要支持行数列数不固定的情况,只需要扩展一下即可,不必推翻重来。
从结构化编程到面向对象编程,了解OO的一些特性和原则,然后再了解设计模式,到最后发现OO的一些缺陷,你会寻求通过AOP思维或者结构化思维来进行补充。这个漫长的过程中你一定会经历两次飞跃。有人归纳为第一次飞跃是“看山不是山,看水不是水”,第二次飞跃是“看山还是山,看水还是水”,我觉得有一定道理。了解如何抽象、如何多态、如何协作,属于术,此为学也;了解如何用OO去思考,去设计,此为道也。为学日益,为道日损,所以真正掌握OO是要放弃一些固有的、不全面甚至不正确的观点的,这才是OO真正的难点。
以下是我的几点总结:
结构化思维是面向对象思维的基础。幻想中的一步登入OO的天空并无可能。面向对象的很多原则与结构化思维并无矛盾。真正对OO从理论到实践都透彻的人并不一定排斥结构化思维。
不要比较面向对象与结构化思维的得失。谈到得失就必须有一杆秤。可惜这个世界上没有这样一杆公平的秤。每个人的经历不同、背景不同、思维方式不同,评估的结果就千差万别。其实没有对错,只有针对每个具体人的有效和无效。前面所述,通过面向对象可以将一个复杂问题迅速简化,当我遇到问题时候,我的方法通常是三步走:第一步解决最小的简单模型;第二步解决复合的常规模型;第三步解决扩展的推广模型。这样的确可能比结构化编程费一些事情,但基于我的智力,没有办法一次解决,所以我这样选择是基于我个人的情况。
不要讨论面向对象的性能损失。比方说,很多人都认为基于托管代码的系统比基于原生代码的系统慢,但据ACM(美国计算机协会)1999年的调查报告,采用Java或者C/C++编写的系统,性能差异很小,而不同程序员间的差异却高达30倍!既然如此,为什么不把更多精力花费在代码间,而是讨论面向对象会给系统带来多大的性能损失呢?
不要讨论什么充血模型或者贫血模型。解决问题才是最高目标,何必在乎形式呢?既然连结构化思维都可以接受,为什么要排斥所谓的贫血模型呢?(实话说我个人非常排斥老马的某些观点,为什么要把一个灵活开放的东西搞得那么死板教条呢?)譬如,面向对象有一个原则是尽量采用chatty接口,创建许多简单方法。而在分布式编程中这个原则就成了灾难(你不得不多次连接调用),所以分布式编程的原则是尽量采用chunky接口,把方法留给本地,这是典型的结构化思维。所以原则并不是一成不变的。
不要把责任推给你的上司、团队或者组织。不论你是一般程序员,还是团队负责人,每个人都是团队的重要成员,每个人都会对你正参与开发的系统、对团队产生同样的影响。如果你一直希望改变你目前的方式,那就需要产生额外的效果,令你的上司和团队成员支持你。如果他们不支持你,不是因为他本身的素质,而是因为你没有让他看到效果。除了你自己是可以控制的,团队中的任何人你都只能影响,而不能控制。要改变他们,不是靠说教,而是靠实实在在的绩效。检讨一下你自己,是不是自己在某一方面做得不够好?或者根本你的出发点或者目标就值得怀疑?我相信很多人在对项目质量的考评中,进度会放在比质量更优先的位置。事实上进度与质量并不矛盾。如果组织更注重进度指标或者甚至根本不注重质量指标,你可以在不影响进度的前提下,提升质量指标,一定会获得组织的肯定。然后你就会慢慢发现,其实进度和质量可以相互促进而不是相互冲突。