从面向对象的设计模式看软件设计

jopen 12年前

        前些天发了一篇《如此理解面向对象编程》的文章,然后引起了大家的热议。然后我在微博上说了 一句——“那 23 个经典的设计模式和 OO 半毛钱关系没有,只不过人家用 OO 来实现罢了……OO 的设计模式思想和 Unix 的设计思想基本没什么差别”,结果引来了一点点争议。所以,我写下这篇文章把我的观点说明一下。我希望这样可以让大家更容易地理解什么是设计模式。我随便 帮 OO 和 Unix/Linux 搞搞基。

        什么是模式

        在正式说明 GoF 的那 23 个经典的设计模式其实和 OO 关系不大并和 Unix 的设计思想很相似的这个观点之前,让我先来说说什么是模式?设计模式的英文是 Design Pattern,模式是 Pattern 的汉译。所谓 Pattern 就是一种规则,或是一种模型,或是一种习惯。Pattern 这个东西到处都是,并不只有技术圏子里才有。比如:

  • 文章有文章的 Pattern。如新闻有新闻的 Pattern(第一段话简述了整个新闻),诗歌总是抒情的,论文总是死板的,讲稿总是高谈的,漫画总是幽默的,……
  • 小说有小说的 Pattern。比如,
  • 武侠小说必然要整个武林大会,整几个 NB 的武功和大师,分个正派和反派,还有一个或数个惊天阴谋,坏人总是要在一开始占尽优势,好人总是要力挽狂澜……
  • 言情小说总是要有第三者,总是要有负心人,里面的女子总是要哭得死去活来,但又痴心不改,……
  •  新闻联播的模式是:头 10 分钟领导很忙,中间 10 分钟人民很幸福,后 10 分钟国外很乱。中国政府官方宣传稿也模式也很明显,各种赞美,口号,胜利,总是要坚持个什么,团结个什么,迈向个什么,某某精神,某某思想,群众情绪稳定,不明真相,等等……
  • 春节的模式是,回家,吃饺子,放个鞭炮,给压岁钱,同学聚会…… 同学聚会的模式基本上都是在饭桌上回忆一下校园时光,比较一下各自的当前处境,调戏一下女同学……
  • …… ……

        这就是 Pattern,只要你细心观察,你会发现这世间有很多很多的 Pattern。

        GoF 的 23 个设计模式

        《设计模式》这本书中,GoF 这四个人总结了 23 个经典的面向对象的设计模式,某中有 5 个创建模式,7 个结构模式,11 个行为模式。很多人都会觉得这是面向对象的设计模式,很多人也觉得非面向对象不能用这些模式。我觉得这是一种教条主义。就像《那些流行的编程方法》中的“设计模式驱动型编程”一样,就像《如此理解面向对象》一样的那么的滑稽。

        好了,回到我的论点——“GoF 的这 23 个设计模式和 OO 关系不大,并且和 Unix 的设计思想基本一致,只不过 GoF 用 OO 实现了它们”,就像我上面说过的那些生活中的 Pattern 一样,只要你仔细思考,你会发现这 23 个设计模式在我们的生活和社会中也能有他们的身影。而且也一样可以用 OO 的方式实现之。

        让我们来看看这 23 个经典的设计模式中的几个常用的模式:

        Factory 模式,这个模式可能是是个人都知道的模式。这个模式在现实社会中就像各种工厂一样,工厂跨 界的不多,基本上都是在生产同一类的产品,有的生产汽车,有的生产电视,有的生产衣服,有的生产卫生纸……基本上来说,一个生产线上只有做同一类的东西。 这和 Factory 模式很相似。编程中,像内存池,线程池,连接池等池化技术都是这个模式,当然,Factory 给你的一个对象,而不单单只是资源,factory 创建出来的对象都有同样的接口可以被多态调用。这其实和 Unix 把所有的硬件都 factory 成文件一样,并提供了 read/write 等文件操作来让你操作任意设备的I/O。

        Abstract Factory:抽象工厂这个模式是创建一组有同一主题的不同的类。这个模式在现实社会当中也有很多例子,比如:

  • 移动公司的合约机计划,88 套餐(通话 100 分钟,短信 100 条,彩信,20 条,上网 200M),128 套餐(通话 200 分钟,短信 150 条,彩信 50 条,上网 500M)……
  • 家里的装修,总是要有厨卫,有门,有灯,有沙发,有茶几,有床,有衣柜,有电视,有冰箱,有洗衣机……,这些都是必需的,只是每个家庭里的具体装修不一样。
  • Diablo 游戏中的 Normal,Hard,Nightmare,Hell 模式,这些模式的怪和场景和故事情况都差不多,就是每个场景的怪物和装备的属性不一样。或是 WarCraft 中的地图就是一个 Abstract Factory 模式(注:Warcraft 的地图什么都能干)。这和学校中的小学,初中,高中,大学差不多,都是一样的学习环境,一样的教学方式,一样的教室,都要期中考和期末考,都有班长和科代 表,就是学的东西的难度不一样,但基本上都是语文,英语,数,理,化,还有永远都有的政治课。学校就是一个抽象工厂。

        这就是抽象工厂的业务模型(或是:Business Pattern),你觉得是不是不一定非要用 OO 来实现这样的模式?(我们思考一下,我们会不会被先入为主了,觉得不会 OO 都不知道怎么实现了),不用 OO,用相同格式但内容不同的配置文件是不是也能实现?在 Unix 下,抽象工厂这个模式在 Unix 下就像是/etc/rcX.d 下的那些东西,1 代表命令行单用户,2,代表命令行多用户,3 代表命令行多用户完整模式启动,5 代表图形界面启动,0 代表关机,6 代表重启,你要切换的话,init 就行了。

        Proxy 模式,原型模式,复制一个类的实现。这个模式在现实中的例子也有很多:传真,复印,都是这个模式。Unix 进程和 Github 项目的 Fork 就是一种。进程 fork 明显不是 OO 的模型(参看:关于 Fork 的一道面试题)。用非 OO 的方法同样可以实现这个模式。

        Singleton 模式,单例模式。生活中,公司只有一个 CEO,法律限制你只能有一个老婆,你只能有一个身份证号,一个 TCP 端口只能被一个进程使用,等等。软件开发方面,并不一定只有 OO 才能做到,你可以用一个全局变量,一个中心服务器,甚至可以使用行政手段来约束开发中不会出现多个实例。Unix 下实现单例进程的一个最常用的实践是在进程启动的时候用“(S_IRUSR S_IWUSR S_IRGRP S_IROTH)”模式打开一个“锁文件”。

        Adapter 模式,适配器模式。可以兼容欧洲美国中国的插头或插座,万能读卡器,可以播放各种格式多媒 体文件的插放器,可以解析 FTP/HTTP/HTTPS/等网络协议的浏览器,可以兼容各大银行的银联接口、支付宝、Paypal、VISA 等银行接口,可以适配各种后端的解释器的 Nginx 或 Apache,等等。用非 OO 的编程方式就是重新包装成一个标准接口。这个模式很像 Unix 下的/dev 下的那些文件,操作系统把系统设备适配成文件,于是你就可以使用 read/write 来进行读写了。

        Bridge 模式,桥接模式。这个模式用的更多,比如一个灯具可以接各种灯泡或灯管,一个电钻可以换上不 同的钻头来适应不同的材料,一辆汽车可以随时更换不同的轮胎来适应不同的路面,你的桌面可以随时更换一个图片来适应你的心情,你的单反相机可以更换不同的 镜头来拍不同的照片…… 桥接模式说白了就是组件化,模块化,可以自由拼装。在 OO 中,其主要是通过让业务类组合一个标准接口来完成,这在非 OO 的程序设计中用得实在是太多了,主要是通过回调函数或是标准接口来实现。这个也是 Unix 设计哲学中的主要思想。在 Unix 中,文件的权限使用的就是 Bridge 模式,标准接口是用户,用户组和其它,rwx 三个模式,然后用 chmod/chown 改一改,这文件就有不同的属主和属性了。

        Decorator 模式,装饰模式。这个模式在生活中太多了,你给你的手机或电脑贴个什么,挂个什么,吃 东西的时候加点什么佐料,多点肉还是多个蛋,一个 Unix/Linux 命令的各种参数是对这个命令的修饰,等等。我觉得这个模式在 Unix 中最经常的体现就是通过管道把命令连接起来来完成一个功能,比如:ps -elf  是列进程的,用管道 grep hchen 就可以达到过滤的目的,grep 的逻辑没有侵入 ps 中,grep 修饰了 ps,但是其组合起来完成了一个特定的功能。可见,这和 OO 没有什么关系。

        Facade 模式,这个模式我们每个人从会编程的时候就在无意识地用这个模式了。这个模式就是把一大堆类 拼装起来,并统一往外提供提口。在现实生活中这样的例子太多了,比如:旅行社把机票,酒店,景点,导游,司机,进店打了一个包叫旅行;IBM 把主机,存储,OS,J2EE,DB,网络,流程打了个包叫企业级解决方案。Unix 中最典型的一个例子就是用 Shell 脚本组合各种命令来创造一个新的功能,这是的 Shell 中的各种命令通过标准I/O这个接口进行组合交互。

        Proxy 模式,代理模式。我们租个房,买个机票,打个官司,都少不了代理,人大代表代理了老百姓去行使 政治权力。我们去饭馆里吃饭也是一种代理模式,因为我们只管吃就好了,洗菜做饭洗碗的工作都被 Proxy 帮你干了,于是你就省事多了。操作系统就是硬件的代理,CDN 就是网站的代理,……使用代理你可以让事情变理更简单,也可以在代理层加入一些权限检查,这样可以让业务模块更关注业务,而把一些非业务的事情剥离出来交 给代理以完成解耦。可见这个模式和 OO 没啥关系。Unix 下这个模式最佳体现就是 Shell,它代理了系统调用并提供 UI。还有很多命令会帮你把/proc 目录下的那些文件内容整理和显示出来。

        Chain of Responsibility 模式,劫匪来抢银行,保安搞不定,就交给 110,110 搞不定就交给武警。有什么事件发生时的响应的 Escalation Path,办公中的逐级审批。这个模式用一个函数指针数组或是栈结构就可以实现了。这个思想很像编程中的异常处理机制,一层一层地往上传递异常直到异常被 捕捉。在 Unix 下,一个最简单的例子就是用 && 来把命令拼起来,如:cmd1 && cmd2 &&, 如果 cmd1 失改了,cmd2 就会执行,如果 cmd1 和 cmd2 都失败了,cmd3 才会执行。如: cd lib && rm -rf .o 或 ping -c1 coolshell.cn && ssh haoel@coolshell.cn

        Command 模式,这恐怕是软件里最多的模式了,比如:编译器里的 Undo/Redo,宏录制。还有数据库的事务处理,线程池,设置向导,包括程序并行执行的指令集等等。这个模式主要是把一个对象的行为封装成一个一个的 有相同接口的 command,然后交给一个统一的命令执行器执行或管理这些命令。这个模式和我们的 Unix/Linux 机器启动时在/etc/init.d 下的那些S和K开头的脚本很像,把各种 daemon 的启动和退出行为封装成一个脚本其支持 reload/start/stop/status 这样的命令,然后把他们按一定的规范做符号链接到/etc/init.d 目录下,这样操作系统就会接管这些 daemon 的启动和退出。

        Observer 模式,观察者模式,这个模式也叫 pub-sub 模式,很像我们用手机订阅手机报,微博的 follow 的信息流也是这样的一个模式。MVC 中的C会 sub V 中的事件,用非 OO 的方式其实也是一个回调函数的事。在很多异步系统中,你需要知道最终的调用有没有成功,比如说调用支付宝的支付接口,你需要向支付宝注册一个回调的接口, 以便支付宝回调你。Linux 下的一些系统调用如 epoll/aio/inotify/signal 都是这种思路。

        Strategy 模式,策略模式,这个模式和 Bridge 模式很像,只不过 Bridge 是结构模式,其主要是用于对象的构造;而 Strategy 是行为模式,主要是用于对象的行为。策略模式很像浏览器里的各种插件,只要你装了某个插件,你就有某个功能。你可以安装多个插件来让你的浏览器有更多的功 能(书本上的这个模式是你只能选用一个算法,当然,我们不用那么教条)。就像《你可能不知道的 Shell》中的那个设置设置$EDITOR 变量后可以按 ctrl+x e 启动编译器,或是用 set -o vi 或 set -o emacs 来让自己的 shell 像 vi 或 emacs 一样,或是像 find -exec 或 xargs 一样的拼装命令。

        Bridge 和 Strategy 是 OO 设计模式里的“Favor Composition Over Inheritance” 的典范,其实现了接口与实现分离的。Unix 中的 Shell 就是一种,你可随意地更换不同的 Shell。还有 Emacs 中的 LISP 驱动C,C实现了引擎,交给 LISP 实现逻辑。把程序分为前端和后端,通过 socket 专用应用协议进行通讯,前端实现策略,后端实现机制。再看看 makefile 把编译器和源代码的解耦,命令行输出这个接口可以把一个复杂的功能解耦并抽像成各种各样小而美的小功能命令,等等这样的例子,你会发现,还有大量的编程框 架都会多少采用这样的思想,可以让你的软件像更换汽车零件一样方便。我在用 Unix 的设计思想来应对变更的需求中说过灯具厂,灯泡厂,和开关厂的例子。

        后记

        因为写作仓促,上面的那些东西,可能会你让你觉得有些牵强,那么抱歉了,你可以帮我看看在生活中和 Unix 里有没有更帅的例子。

        不过,我们会发现上面 OO 搞出来的那么多模式在 Unix 下看来好像没有那么复杂,而且 Unix 下看起来并没有那么多模式,而且 Unix 中的设计模式无非就是这么几个关键词:单一,简洁,模块,拼装。我们再来看看 OO 设计的两大准则:1)钟情于组合而不是继承,2)依赖于接口而不是实现。还有S.O.L.I.D 原则也一样(如果你仔细观察,你会发现 SOLID 原则在 Unix 下也是完美地体美)。你看,Unix 和 OO 设计模式是不是完美的统一吗?

        我有种强烈的感觉——Unix 对这些所谓的 OO 的设计模式实现得更好。因为 Unix 就一条设计模式!再次推荐《The Art of Unix Programming

从面向对象的设计模式看软件设计

        餐后甜点

        我上面提到了《The Art of Unix Programming》,所以我有必要再谈谈这本书中我中毒最深的一章《模块性:保持清晰和简洁》中所谈到的胶合层。

        胶合层这一节中说了,我们开发软件一般要么 Top-Down,要么 Bottom-Up,这两种方法者有好有不好。顶层一般是应用逻辑层,底层一般是原语层(我理解为技术沉淀层,或是技术基础层)。自顶向下的开发,你可能 会因为开发到底层后发现底层可沉淀的东西越来越不爽(因为被可能被很多业务逻辑所侵入),如果自底向上的开发,你可能越到上层你越发现很多你下面干的基础 上工作有很多用不上。所以,最好的方式是同时进行,一会顶层,一会底层,来来回回的开发——说白了就是在开发中不断的重构,边开发边理解边沉淀。

        无论怎么样,你会发现其中需要一层胶合层来胶合业务逻辑层和底层原语层(软件开发中的业务层和技术层的胶合),Unix 的设计哲学认为,这层胶合层应该尽量地薄,胶合层越多,我们就只有可能在其中苦苦挣扎。

        其实,胶合层原则就是分离原则上更为上层地体现,策略(业务逻辑)和机制(基础技术或原语)的清楚的分离。你可以看到,OO 和 Unix 都是在做这样的分离。但是需要注意到的时,OO 用抽象接口来做这个分离——很多 OO 的模式中,抽象层太多了,导致胶合层太过于复杂了,所以,OO 鼓励了——“厚重地胶合和复杂层次”,反而增加了程序的复杂度(这种情况在恶化中)。而 Unix 采用的是薄的胶合层,薄地相当的优雅。(通过这段话的描述,我相信你会明白了《如此理解面向对象编程》中的个例子——为什么用 OO 来实现会比用非 OO 来实现更为地恶心——那就是因为 OO 胶合层太复杂了)

        OO 的最大的问题就——接口复杂度太高!(注:Unix 编程艺术这本书里说了软件有三个复杂度:代码量、接口、实现,这三个东西构成了我们的软件复杂度)

        再送一个果盘

        大家一定记得《SteveY 对 Amazon 和 Google 平台的长篇大论》 中 Amazon 中那个令人非常向往的 SOA 式的架构。因为以前在 Amazon,有些话不好说。现在可以说了,我在 Amazon 里,我个人对这个服务化的架构相当的不待见,太复杂,复杂以乱七八糟,方向是好的,想法也是好的,但是这东西和 OO 一样,造成大量的接口复杂度,今天的 Amazon,完全没人知道各个服务是怎么个调用的,一团乱麻(其内部并不像你看到的 AWS 那么的美妙。注:AWS 是非常不错的,是相当好的设计)。

        那么我们怎么来解决 SOA 的接口复杂度问题?其实,Unix 早就给出了答案——数据驱动编程(详见: 《Unix 编程艺术》的第 9.1 章),在我离开 Amazon 的时候,美国总部的 Principle SDE 们在吐槽今天 Amazon 的 SOA 架构,更好的架构应该是数据驱动式的。(今天还在 Amazon 的同学可以上内网 boardcast 上看看相关的 Principle Talk 视频)

        (注:这本来是我想在 2012 年杭州 QCon 上的分享的一个主题,无奈当时被大会组织者给拒了,所以只好讲了一个《建一支小团队》,今天有多人还是不能明白甚至反感我的那个《小团队》的演讲,但是我 相信那是必然的趋势,就像十年前大家在说“程序员只能干到 30 岁”时,当时的我我却毫不犹豫地相信十年后,30 岁以上的有经验的老程序员一定会成为各个公司角逐和竟争的红人)

来自: coolshell.cn