起因
应该是第三遍读这本《Head First Design Pattern》了,也是第一次由电子书改成了实体书,每次阅读的感觉都不同。刚开始的时候懵懵懂懂,后来若有所思,最令我感觉深刻的是曾经认为不对的现在觉得未必不对,曾经被抛弃的做法现在可能又重新捡起,也许这就是很多高人所说的螺旋上升的过程,也是公司和部门去年一直宣讲的“实践-总结-再实践-再总结”的哲学原理。
在博客园上写设计模式的相关话题,自然会招来板砖一片,好在我已经做好了准备:)
好脑子不如烂笔头,这既是自己对之前的总结,也是将来可以看到的曾经的历程,总归是一件好事。
到目前为止,回想自己用得比较频繁也相对理解比较深的,应该就是策略模式和观察者模式,这本令人爱不释手的《Head
First Design Pattern》的前两篇也正好是从这两个模式开始,我将结合我曾经的工作经历对这两个模式先做一些总结。
困境
刚参加工作的时候,我在一家主要以研发和生产仪器设备的公司工作。在产品中,上位机软件通过串口和下位机软件通讯,最终达到客户使用电脑操控设备的要求。
我们写了一个控制串口操作的类,主要有一些参数的配置和串口的读写方法,顺便提一句,我们对于串口的操作使用了阻塞模式(我对观察者模式的反思中会提到这点)。
类可以简单用下面的类图来表示,为了简单,我省略了相关的一些方法和属性,只保留了两个最关键的方法:读和写。
(需要注意的是,下面所有的COM指的是串口。 )
所有的应用都直接使用这个类,并将这个类所在的单元放到自己的工程中作为工程的一部分(使用而不是引用)。大家根据各自需求的不同,可能会对这个类做一些修改。很明显,这存在着很多的问题,比较突出的两个是:
1.版本不一致,大家每人维护自己的版本,时间长了造成版本之间的差异性越来越大,耗费更多的精力来维护;
2.耦合性强。每个程序都直接使用这个类的定义,造成了维护的困难。
大家并非没有发现这些问题,只是人不够用,项目太多,大家的能力也是有高有低,公司的规范和流程还不健全,于是这些充满着各种臭味的代码越来越大,随着人员的流动,很多代码都被抛弃了,大家充满激情重新开发之前的一些类,试图解决曾经的问题,却常常做一些重复的事情,犯别人犯过的错误。
直到有一天,大约是2004年底,总工要求我们的新产品使用RJ45网口代替串口进行通讯。
我们很快掌握了Socket的运用,也在测试的小例子里很容易实现了上位机和下位机的通讯(实验性的开发叫做穿刺,有XP经验的人应该对穿刺这个词比较熟悉),然后将Socket的主要方法封装成一个类。负责穿刺的同事很有成就感,觉得他解决了95%的问题,接下来的工作就是用这个类来替换串口。他的这个类可以用下面的类图简单表示:
这个同事有理由骄傲,因为从类图上来看这个类和之前的类是如此的相像。
但是在实际的替换中,还是出现了很多的问题:
1.程序的耦合性太强,很多人为了省事开放了TCom类很多的后门方法,直接读写这个类的内部属性;
2.各个版本不一致,要完成一个项目就要在之前的项目中寻找类似的程序,然后在上面改代码,也就是说多个不同的程序如果要替换串口为Socket方式,则要分别进行替换,而替换后又多了一个版本出来,有限的人要维护急剧膨胀的项目,大家都觉得越来越吃力,越来越烦躁;
3.还是耦合性的原因,完成了TCP对串口的替换,原来没有出现的Bug暴露了,原来改过的Bug又复现了,大家忙着四处救火。
经过一段时间的改造和测试,终于出了一个打满补丁但是相对稳定的版本,这时候也到了年底,总工在明年的规划中提出了要实现更多通讯接口的支持,比如当时正红得发紫的USB接口。
看到这里,估计很多刚毕业就有幸使用纯面向对象语言,周围有水平很高的师傅的朋友会觉得不可思议,但是相信更多有过作坊式开发经验的同行会会心一笑。
第一次改造-封装变化
当时我们的软件开发人员大多都集中在一个叫做软件组的公共部门里,我和另外一个同事在一个以研发海外仪器和新产品为主的部门里。大家聚在了一起,商量今后的方法,提出了封装,解除耦合,模块化等目标。那时候有同事在学习UML,有同事已经在学习设计模式,当然更多的同事在不断深化自己对面向对象的理解。
首先第一步,是分析程序中哪些是变化的,哪些是稳定的。经过我们分析,对于通讯来说,调用通讯的接口是稳定的,而实现这些接口的方式是可变的,因此我们首先提炼出一个接口。这个接口比较复杂,最核心的部分大家应该可以想到:
需要说明的是当时提出了使用的接口并不是Java或者C#中的interface语言要素而是使用了抽象类。
然后我们找到一个代码质量相对较好的程序,一起重构了TCom类,让它能够实现ICommnication接口,同时将TSocket类也做了改造,让它也实现了ICommnication接口。
第三步我们将接口定义和两个类的实现放到公共资源中,规定只能由一个人进行维护,所有对它进行的修改需要先提交请求和理由,经过大家讨论之后才能将修改的结果放到公共资源中。所有的程序只能以只读方式使用这些公共资源。
最后一步是个力气活,要将之前绑定TCom类的程序改为针对接口ICommunication进行操作,创建对象的时候也相应地使用具体的类来创建。想到之后的美丽人生,大家都改造的很起劲,终于,在春节后一个月内完成了所有的改造工作,当然,不可避免会出现新的Bug。
小试牛刀
新项目要使用USB接口,我们很高兴接受了任务。一个同事用了2周进行USB通讯的穿刺,完成了一个实现了ICommunication接口的类TUSB,经过测试之后放入了我们的公共资源库中。同时为了表明这些类之间的血缘关系,我们一致同意对类进行统一的命名——在给相关的类加上Comm后缀,类图如下所示:
接着我们用了一分钟进行类的替换——只需要在创建的时候将TCOMComm变成TUSBComm,编译通过,一次调试成功,连总工(主要是公司电气设计、下位机软件和管理的权威)都感到神奇,赞叹方法的力量。我们和总工开玩笑:“还需要什么接口?PCI?ISA?要不再加个IEEE火线接口吧~”
看看我们都做了什么?
第一当然是封装变化,我们找出了变化的部分和不变的部分,然后将变化和不变的部分独立开来;
第二是解除了应用程序和通讯之间的耦合,使得应用程序发生变化不影响通讯,通讯接口发生变化也不影响应用程序。
在这里和大家一起复习一下《Head First Design Pattern》第一章中提到的OO原则:
1.面向对象的三个特性:封装、继承、多态
2.封装变化
3.针对接口编程而不是针对实现编程
4.为解除软件之间的紧耦合而努力
我们上面的做法实际上就运用了策略模式的思想,先复习一下策略模式的定义:
定义一族算法,将算法和使用算法的程序分离,这些算法之间可以互相替换。
在我们上面的应用中,每个通讯方式可以看作是一种算法,他们具有相似的接口和不同的实现方式,同时使用他们的方法是相同的,我们可以将这些算法和应用他们的程序独立,这些算法之间可以互相替换。
第二次改造-物理分离
好日子过了一段时间,发现了新的问题——每次更换通讯方式都需要修改代码,虽然只是在创建对象的时候简单替换一下类,但是还是会出现软件开发的一种现象:
对代码的局部修改可能会影响到全局。
我们必须想一种办法来解决。
当然这个时候我可以引出工厂模式的话题了,但我还是想按照《Head First Design Pattern》的顺序进行,这样使得看这本书的朋友可以和我一起思考。同时,既然是反思,反思的内容还是要基于当时的实际情况。
我们当时想到的办法是使用动态连接库的方式解决问题。
将每个通讯类都都做成一个DLL(USBComm.dll、COMComm.dll、SocketComm.dll……),每个DLL都输出同样的接口,放在指定的路径下,比如Bin\Communication\下,程序自动加载这个路径下的动态连接库,然后调用它的通讯方法。
应用程序和通讯之间经过这样的改造之后,不但逻辑上分离了,在物理上也做到了分离。
看一下我们的成果,当通讯方式变化的时候,直接使用其他的DLL替换当前的DLL就可以了,不用改程序,甚至不需要过多的培训,实施人员就能自己做这个工作了。
到目前为止,我也认为这种傻瓜型的方案是非常好的一种方案,降低了实施和学习的成本(我一看到很多配置文件就觉得头大:)),在Ruby中实现MVC据说也是定义了一些文件夹,相关内容直接放到里面,避免了大量配置文件的使用。
第三次改造-可配置性
有的时候,客户需要在两种或者更多种通讯方式之间切换,这种傻瓜型的方式就需要做些改变。
这是个很简单的工作,在有了之前的成果的基础上,我们只要将所有可能的通讯方式都放到之前提到的文件夹下,同时在创建的时候自动读取这个文件夹下的每个DLL,然后在程序中列举出来让用户选择。
用户怎么知道每个DLL有什么作用呢?很简单:在DLL中输出两个方法,分别是通讯的名字和主要功能。
甚至你可以保存它最后一次的配置,让它不需要每次配置,这些都是很容易实现而且对客户很友好的事情。
到目前为止,一切都很爽了。我们要开发一种新的通讯方式,只要实现接口,做成DLL并放到指定路径下,应用程序不需要做任何改变就能自动读取,真正做到了可扩展性。
策略模式的总结到目前为止就差不多结束了,对于最后可配置性的介绍,大家有没有觉得它已经类似插件了?下次我会介绍一下我之前做过的一个大粒度的插件的一些情况以及在做插件的过程中使用到的观察者模式。
|