前文说到一位用户拿着业界标准开关(一个标准的StandardSwitcher,它依赖IStandardSwitchable接口才能工作,然而目前我们的灯并不支持这个接口)出现在我面前,叫嚣着他的“标准开关”应该能打开我们的灯。好吧,这个需求是合理的,的确应该支持。但是该死的是,为什么没有早一点儿知道这个标准的存在呢?这样就不需要花费时间和人力定义这个接口,现在也不会这么纠结。和上次一样,先讲故事、演进方案,再分析背后的思想。 这回主要讲解Adapter模式,GoF讲解了这个模式是什么,怎么用,用在什么地方。我想来解释一下Adapter模式的要点是什么,对Adapter模式的延展,以及对Adapter模式的误用。顺便得瑟一下我对面向对象设计的理解。 两个方案 现在有两个选择。
第一个方案很简单,就是让Light多实现个接口就OK了。图就不给了。 现在分析第二个方案,标准接口依赖IStandardSwitchable接口,那我们必须有一个类来实现它,并完成所需要的功能——操作灯。咱也是学过设计模式的人,这个问题很明显可以用Adapter模式来解释。 相关类图很容易就可以画出来。 图1 让灯支持IStandardSwitchable接口的方案 其对应的代码会是这个样子:
代码1 Job Done。Light通过SwitcherAdapter支持了新的接口,这简直就是应用适配器模式的典范啊。(嗯,这句的确是反话,不过你猜出来为什么这个Adapter不属于适配器模式吗?) “上回真是白跟你说了那么多,平时没觉得你这么不开窍啊。你自己好好想想吧!”背后看着我画UML图的设计Guru好像有点儿生气。 上回?我冷静下来回想上回的内容和现在的问题。上回讲的DIP,讲不要依赖实现,要依赖抽象。再想想目前的需求,我们有灯,有收音机,如果用户说要用标准开关开收音机,难道还要实现一个RadioAdapter不成?这显然违反了OCP。 需求是要“通过加一层让灯支持标准开关”,但是并不是说这一层就要使用灯,为了让这个Adapter更加通用,应该让Adapter依赖ISwitchable接口。像下面这个样子。 图2 Adapter模式 与代码1的差别,仅仅是SwitcherAdapter里的Switchee属性的类型改成ISwitchable而已。代码就不再贴了。其所体现的原则就是上一篇讲的DIP。 这个事儿其实任何人静静地想想都能想到。但我绕这个弯子,其实是想顺便表达这样一个意思:一个紧急需求来了的时候,人们更容易倾向于把完成工作放在第一位,从而一时忽视了设计的严谨度,事后又忘了重构,于是Bad Smell就这样产生了。当然,这些大家也都知道。 面向对象的设计并不是对现实的模拟 (这一节算是一个插曲吧,因为这个论点太大,写出来都觉得不自量力,不写又觉得对不起自己爱得瑟的作风。一点拙见,大家多多批评。觉得偏题太远的话可以直接看一下节。) 但是(重点来了),为什么紧张时做出的直观设计更可能是错误的呢?因为人一紧张就容易凭感觉,而使用直觉做设计时,大都会以现实世界为原本,但是良好的面向对象设计,是绝对不能仅仅依靠现实世界的。其实图1 的设计从直觉上来讲是符合需求,也很符合人们对这个世界的认知的。但是它并不是一个良好的面向对象设计。图2是相对良好的设计,但是图2显然又没有图1 那么直观,那么好理解,那么符合这个世界的真实状态。 图2和图1 的差别仅仅在于Adapter要依赖谁上,Adapter要依赖于ISwitchable接口这个事儿,并不是为了更真实地模拟这个世界,而纯粹地是为了解耦合而出现(或者说,为了依赖抽象)。但是在现实世界中,是不存在解不解耦合的概念的。解耦合是为了保证设计上的灵活性引入的概念。 现实中事物间的依赖都是具体的,是为了复用、灵活性等才引入的抽象,客观现实是不存在抽象的。抽象是要取决于你是如何看待客观事物的。举个例子,在动物学家看来,人与动物间有IS-A关系;但是如果你是要开发一款MMORPG游戏,人(NPC和Avatar)和动物(一般会是怪物)应该是不会有IS-A关系的。观察的角度不同,就会得出不同的设计;这些设计没有对错之分,只有是否满足需求之别。 所以,有些地方,把面向对象的设计过程解释为对现实世界的模拟是很片面的。如果仅仅以现实世界的样子对系统进行设计,得出的设计很可能是僵化的,就像图1那样。(有人可能想说我曲解了人家的意思,但是我想说,你写成那个样子明明就是故意给人误解的,至少是很容易引导人误解。容易被误解,就是有问题。没什么好狡辩的。) 但是,这并不意味着做设计就要全面地抽象,模拟现实世界的好处是代码容易理解,但是如果全部抽象成图2那样,所有都抽象出个接口,所有都依赖抽象,那代码的可读性显然会下降。所以,好的面向对象设计,会是真实地模拟现实与抽象现实间的取舍的过程。如果你看过一些功能相似、但实现不同的开源框架,会发现有些好理解,有些不好理解,其根本原因就是其抽象的层次或者说抽象的程度不一样。抽象度过高,灵活性也许上去了,但是并不见得就是好事儿。过度设计,就是因为对现实的抽象度太高,造成可读性差,不好维护,还没解决问题,就先被问题解决掉了。 上面的例子可能依然没有什么说服力。我再举一个。上篇文章有人回复说, “开关里面还包含一个开关接口 ,很奇怪的方式。 在我看来应该是灯光有开关”。 我想感谢一下这位朋友,因为他提出的这个思路,我一开始就潜意识地无视掉了。经他一提,我才意识这也是设计过程中一类常见的问题。这个设计是一个很真实地反应现实的设计。但是并不是一个可行的类设计。如果你按这个方案写代码,就会发现很多问题。原因我已经回复了。 总结一下,做面向对象设计的时候,请记得自己要做的是什么?不要让现实世界的“真实”的样子混淆了视听。面向对象设计,是以可复用地、灵活地实现需求为目标的,对现实的抽象,而不是对现实的模拟;抽象的结果很可能在现实中并不存在。 Adapter模式的关键 Adapter模式最关键的要求是:Adapter是对两个功能相近的接口间的适配。如果被适配的对象是个具体类,那么多数情况下,Adapter非但不会带来好处,反而是仅仅增加了维护成本,就像前面说的,有一个新的具体类出现,就要同时添加一个Adapter。 (如果你非说你见过很多 “适配”具体类的,你是对的,但是那叫Proxy,不叫Adapter,解决的也不是同一种问题,而且多数情况下,Proxy是可以自动生成的,所以不需要担心加一个类,就要自己实现一个对应的Proxy的问题。可以用下面这个图对比一下,来自《敏捷软件开发》) 图3 Proxy模式 这不是在死抠Adapter模式的含义。因为只有理解Adapter的目标、适用范围之后,才不会误用这个模式。见过不少人理解力很好或是英文很好,看到 Adapter这个词是个模式就想当然地觉得自己“知道”了这个模式的用法(毕竟这个模式也的确不复杂),并“用”了起来。比如图1的那个例子,就是最常见的误用之一。 这也不是在死抠名词。给模式命名的好处之一就是让两个都懂模式的人沟通起来更顺畅。模式名所表达的,不是一个简单的类关系图,而是对要解决问题的类型的定位和解决问题的策略。 Adapter,表示遇到的问题是接口不匹配。 Proxy,表示遇到的问题是主体逻辑与附加逻辑(持久化、网络传输等)纠缠。 名词用错了,就可能会带来不必要的误会。 如果你就是觉得没必要死抠概念,下面的“广义Adapter模式”可能会比较适合你。 广义Adapter模式 这年头好像什么东西都非要搞出个狭义和广义之分。我个人比较反感这一点,因为狭广之分的存在,本身就是一种对概念的模糊。这导致人们在沟通时,如果遇到问题,常常要想一下对方说的是广义的还是狭义的,而不是把焦点放在问题本身。这像是给自己和对方找借口或是后路。或许是因为大家都想给自己留个后路,这东西才会这么流行。附经典对白一则: “嗯?不对吧,不应该是XXXX吗?” “呃,我说的是一种广义上的XXXX。” “哦。(Shit!)” 每个人们学习模式,总会有自己的理解,自己的抽象。当理解的角度不同的时候,就会把Adapter模式的内涵延展到不同的地方。这就导致了不同人对广义Adapter的定义是不同的。 比如《敏捷软件开发》,从逻辑关系出发,把Adapter的概念延展为:使用一个特定的类,实现对方法调用的定向派发(我自己总结的,原文没这话)。从这个概念上讲,Adapter模式可以用于对具体类的适配。因为这个延展的概念实际上已经超出了原有的GoF的定义。这显然不能说是错误的,你甚至会觉得这个人水平真高,能对设计模式进行再抽象,再扩展。 但是问题是,不同人对同一概念的延展方向是不同的。你觉得Adapter和Delegate/Event有什么相似之处吗?我相信更多人会觉得Observer模式与Delegate/Event的相似之处更多些。因为无数的人和书都说过C#的Delegate /Event机制就是Observer模式的一种具体实现。如果你面试的时候说,Delegation就是一种Adapter,你的面试可能就直接 Pass了。这事儿也的确真实地发生过。 但是如果去看《Pro Objective-C Design Pattern for iOS》第112页,对Adapter的描述真的是这样的。 “The Delegation pattern was once one of the inspirations for cataloging the Adapter pattern in the “Gang of Four” book.” 如果你怕我断章取义,可以自己去看。 这个人是从类与类之间的关系出发,把具有相似结构、交互方式的类的组合都定义为Adapter。你说他的理解错了吗?我只能说:“狭义来讲,是错的,广义来讲,是对的。”但这是这个世界上最操蛋的答案之一。 像上面链接的博客里描述的那个面试者,显然就成了广义与狭义之分的牺牲品——他说的是广义的Adapter,但是面试官想听到的是狭义的Adapter。(不过从后面的叙述来看,那个面试官也是半瓶子醋,问Delegate的时候居然会顺便问异步,让我不得不怀疑他是不是认为事件是异步触发的。) 对Adapter有独特的理解很好,能把Adapter, Observer, Delegation, Proxy全统一起来理解更是NB。但是,其实在多数情况下,越是独到的见解,越可能会给面对面的沟通带来障碍。这些独到的见解在个人顿悟模式的过程中很有用,写到书里也很好,毕竟读者可以细细体味,帮助读者从不同的角度思考问题;但想在面试之类的当面沟通的场合上装逼,然后自己的口才又不咋地。怕只会画虎不成反类犬。 对Adapter模式的误用 学历史的时候,常常见到“左派”、“右派”这样的词,意思是他们走的路线不对。这个词用得很形象,都是走极端。 模式的误用,常见的误用之一也是走极端。 图2 的Adapter模式,成功的把标准的开关接口适配到了我们的接口上。于是便有了一个顺理成章的思路,ISwitchable和 IStandardSwitchable接口都是对开关的定义,我们通过Adapter模式,让支持IStandardSwitchable的开关能够开我们的灯。 那么我们之前的这个设计: 图4. 第一回中提出的开关开灯方案(Abstract Server)
是不是应该改成这样? 图5. 试图把Adapter模式用于实现DIP 这个设计相比原来的设计方案,抽象度更高、耦合性更低,Light甚至不需要依赖ISwitchable接口就可以工作,这样我们可以很有信心地说,我们可以让一切类都支持ISwitchable接口! 这个想法很丰满,但是现实很骨感。如果你认真看过了前面的内容,应该已经知道这个方案其实很烂的原因了。 这个世界很微妙,《敏捷软件开发》(P370)的确就把图5称为Adapter模式,不过你应该懂的,他说的是广义的Adapter模式。并不是说对具体类的Adapter就一定是误用,如果没有违反OCP就不是误用,如果那个Light是个Utility类,就不算是误用。 (如果你想喷Adapter模式本来就有两种,一种是基于类的,一种是基于对象的,你最好先去把Adapter概念回个炉,我们说的根本不是一码事儿。) 误用的原因 我自己总结了一下出现这种误用的原因有三(这些原因会让人出现各种形式的误用,而不针对Adapter模式):
下回预告 我们的灯卖得好,用户就多了起来,需求也多了起来。这样一下子来了两个用户,一个要求,我要用两个开关控制同一个灯(床头一个,走廊一个,看来这用户晚上常起夜);另一个要求,我想用一个开关控制屋子里所有的灯(看来这用户不差钱)。 那么,我们又需要做出怎样的设计来应对这些需求呢 |
|