写下这个标题的时候我就在想,正确的代码?能够按要求正确地运行,不就是正确的代码吗,如果这些代码还具有较高的性能,那么甚至还能被称作非常正确。其实这是不准确的,如果代码是方便面,吃完就可以丢到垃圾桶里,我想这么理解就很不错了。但事实上,一份代码可能在写成之后,需要不断修改、升级发布新的版本,而在这个过程中会有很多人加入其中,所以在多次易手之后,代码能够让别人快速理解,那么清晰的逻辑结构以及良好的可读性才是衡量正确的标准。
但这个问题的解决方案我们并不陌生——Architecture。但是这个词压下来总让很多coder觉得面临了一个难题:啊,我要怎样在看清海市蜃楼前画出海市蜃楼?踌躇中不由得手心出汗。至少我是这么觉得的。我相信,即便是有这样能力的那些架构师们,也不是来自于氪星球,生来就有超能力的。如果你喜欢抬杠,非要说Knuth很小的时候就能够把计算机当作打印机,只是把脑子里的宏图复写下来而已,那我只好服输:赛亚人也不算数。那么为了不讲鬼故事吓跑小朋友,其实可以将架构换成一个亲民一点的词,设计模式。这个东西大家一定都耳熟能详,它们其实就是匠人手里小工具,能够帮助我们更好地搭一座像模像样的小屋。刚接触设计模式的时候,发现大大小小几十个模式虽然多,但其实都不难理解,然而到自己实际开始书写代码时,发现这些模式对我一点帮助也没有,代码自成一体,好像没有必要或者不能使用一些模式,总不能为了使用设计模式而使用设计模式吧。嗯,最后这个观点是正确的。我觉得这个时候其实是离程序太近了,两个月之后回来再看看,想法可能就有所改观了。
Graham认为,程序员和画家在本质上是一样的,画家或者说画手在绘制作品的时候仅仅有一个主题和大概的布局,在一笔一笔添加到画布上时也同时在填充画手心里的构思。编程的过程就是和程序对话的过程,书写的过程就是思考的过程,在不断编写的过程中代码才逐渐丰满起来,不要企望在写下第一个int的时候,整个程序的轮廓都清晰可见,程序的结构是一步步修改完善的,架构师也是一点点养成的。好像有点扯远了,那么我们回到主题上,设计模式能帮助我们写出正确的代码。可是怎么用呢?这就是上面大段口水想要表达的内容,在编写的过程中时刻警觉所遇到的困境,并且思考使用优雅的方法来解决,自然地融入设计模式,揣摩理解架构的核心理念。
我就来举个小例子吧。在完成一个程序的编写后,没有进行整体的调试,也许你会说,天呐,竟然没有调试。遗憾的是,这就是事实。因为这个程序需要和真实设备进行大量的服务交互,而模拟环境下无法提供这些服务,糟糕的是,我要等一个月才能有这样满足条件的设备,所以唯一能做的就是在模拟环境下测试一下UI的流程。而测试UI的手段相当原始,相信任何心理健康的人都会像在帝国大厦顶上看到金刚一样惊讶,是的,我将所有需要调用服务的代码都临时注释掉。我承认这是我在java环境下没有找到类似条件编译这样的方法而采取无耻手段,但好在我很快就停止这种令人羞愧的行径。因为,我不但需要在代码中加入一些仅仅为了调试的测试语句,更可怕的是,出去喝了一杯咖啡之后,我自己也忘记了哪里被注释掉了最后应该还原回去。
在意识到这种暴力是无益于和谐社会之后,我开始思考如何在上设备前进行比较全面的测试,幸运的是在等待设备前我有足够发呆的时间。我首先想到的是,可以构建一个模拟的服务,在测试的时候使用这个模拟的测试服务(TestService),在真机测试时再切换成真正的设备服务(RealService)。那么,现在我需要做的,仅仅是将使用设备服务的代码修改成是使用测试服务,即
RealService.doService() ---->TestService.doService()
其他的代码都不需要修改,我仅需要作一个Ctrl+H的操作就可以了。可是做这种危险的替换,我想我晚上会担心得睡不着觉的。所以,为何不将实现抽象出来呢?就像无数教科书上说的:面向接口编程而不要面向实现。
可能在第一步时候你就想到这么做了,想要我承认你比我聪明,别做梦了,这不是自命不凡的程序员干的事儿。话说回来,好像现在要好多了,代码中出现使用服务的地方都是用了顶级服务,类似于IService.doService(),是的,仅需要在获取服务的地方切换真实服务和测试服务,事情终于变得合理了一点。而且,似乎可以利用依赖注入模式(DependencyInjection)来将设计变得更漂亮一点。为什么不呢?我修改了代码,从统一的入口处获取服务,而服务此时使用了IService接口进行了抽象,整个程序根本不知道使用的是真实服务还是测试服务,我自己配置决定具体注入的是RealService还是TestService。
如果我就此打住,相信很多人会说,嗯,你说得很对,但等等,怎么做啊?是的,我自己的代码,只能我自己来设计。难道要丢给测试团队的小妹妹,那么任何人都会责怪我的。好吧,我希望在配置文件里写上程序运行模式,比如run_mode=test,那么程序就会建立测试服务并注入到请求服务的地方。那么现在我需要一个创建者,请求服务的代码只认识这个服务创建者,它向这个创建者说,喂,我要服务!然后这个创建者说好,他看了一看配置文件里的run_mode=test,然后拿起一个测试服务给请求者,说:给你服务!是的,事情就这么简单,我当时脑子里就是这么想的,我希望这样说你已经明白了,这就是一个抽象工厂模式(Abstract Factory)。
服务请求者保持了一个ICreator(我使用了Creator而不是Factory是有原因的)引用,当请求者需要使用服务时,ICreator根据配置文件决定使用的是测试服务创建器(TestServiceCreator)还是真实服务创建器(RealServiceCreator),然后它们各自生产自己的服务交付给请求者,完成了简单的依赖注入。
现在先暂停一下,回顾一下现在的设计,从最初的直接获取并使用服务,到现在是用创建器获取服务,中间涉及的对象增加了,似乎增加了程序的复杂度了。不过事实上,程序现在更美观正确了,服务使用者和服务的获取分离开了,我终于可以开始进行主程序的调试而不必担心被你嘲笑了。如果以后需要修改服务,或者添加新的服务,那么仅需要添加新的服务及其创建器就好了,服务的调用代码是不需要更改的。降低系统的耦合,提高系统的扩展能力,这就是无数现行开发框架的核心目的,理解这个中心思想(这让我想起了小学语文老师),再去掌握这些开发框架,它们才能被融汇成自己的内功而不是手中的冷兵剑。
事情到了这里似乎是完美了,但等等,还有点教科书之外的东西。有一个服务它很特别,我要说“特别”真是一个terrific的词,如果你说一个女孩很特别,我知道你是爱上她了,她肯定在某方面很吸引你,但如果在编程的时候遇到特别的东西,就是值得头疼的事了,它意味着你得使用专门的代码进行特殊处理。这个令人头痛的东西就是服务A,这个服务需要请求者逐渐填充一些配置参数,最后再使用根据这些配置参数获得的服务,如:
serviceA.config(param);
serviceA.doService();
最终的结果和原工厂模式很相似,但不同的是现在工厂可以看成是动态创建的,橙色部分可以看成是一个生成器模式。现在程序的服务创建是这样的:
requester.creator.configService(args).createService().doService();
创建器在逐步配置完成后最后一步才返回服务对象,在设计接口时将配置方法定义为:
ICreator configService(args)
所以在使用中可以不断配置:
creator.configService(…).configService(…)………
这种方式被称为方法链,方法链在编程中是一个好用的手段,不过有的人并不赞同,因为它增加了调试的难度,使得难以追踪异常的来源,不过它的可读性比方块赋值代码高得多,总的来说利大于弊。
OK,现在特别的服务也解决了,可以放心地测试了。
编写正确的代码,我也忘记了再哪本书上看到这句话的了,仅用以警醒自己在编码的时候勤于思考,尽量写出优雅的代码,点滴涓流,终汇成海。
I wish and I believe.