前文说到一位用户拿着业界标准开关(一个标准的StandardSwitcher,它依赖IStandardSwitchable接口才能工作,然而目前我们的灯并不支持这个接口)出现在我面前,叫嚣着他的“标准开关”应该能打开我们的灯。好吧,这个需求是合理的,的确应该支持。但是该死的是,为什么没有早一点儿知道这个标准的存在呢?这样就不需要花费时间和人力定义这个接口,现在也不会这么纠结。和上次一样,先讲故事、演进方案,再分析背后的思想。这回主要讲解Adapter模式,GoF讲解了这个模式是什么,怎么用,用在什么地方。我想来解释一下Adapter模式的要点是什么,对Adapter模式的延展,以及对Adapter模式的误用。顺便得瑟一下我对面向对象设计的理解。两个方案现在有两个选择。1.让我们的灯直接支持标准开关。也就是让灯实现IStandardSwitchable接口。o好处:成本低,实现方式优雅。o坏处:相当于放弃了已经买了我们的灯,又想用标准开关的用户。2.不改变现在的灯,让标准开关能打开我们的灯。标准接口我们改不了,灯也不能改。好在计算机界有句话,叫“加一层可以解决一切问题”。这让我想到了买外国电器附赠的那个电源接口转换器。现在,我们的灯需要个类似的玩意儿。o好处:支持所有的灯。o坏处:这东西都是要附赠的,会降低我们的利润。第一个方案很简单,就是让Light多实现个接口就OK了。图就不给了。现在分析第二个方案,标准接口依赖IStandardSwitchable接口,那我们必须有一个类来实现它,并完成所需要的功能——操作灯。咱也是学过设计模式的人,这个问题很明显可以用Adapter模式来解释。相关类图很容易就可以画出来。图1让灯支持IStandardSwitchable接口的方案其对应的代码会是这个样子:1.publicinterfaceIStandardSwitchable2.{3.voidTurnOn();4.voidTurnOff();5.}6.7.8.publicclassSwitcherAdapter:IStandardSwitchable9.{10.publicLightSwitchee{get;set;}11.12.13.publicvoidTurnOn()14.{15.Switchee.TurnOn();16.}17.18.19.publicvoidTurnOff()20.{21.Switchee.TurnOff();22.}23.}代码1JobDone。Light通过SwitcherAdapter支持了新的接口,这简直就是应用适配器模式的典范啊。(嗯,这句的确是反话,不过你猜出来为什么这个Adapter不属于适配器模式吗?)“上回真是白跟你说了那么多,平时没觉得你这么不开窍啊。你自己好好想想吧!”背后看着我画UML图的设计Guru好像有点儿生气。上回?我冷静下来回想上回的内容和现在的问题。上回讲的DIP,讲不要依赖实现,要依赖抽象。再想想目前的需求,我们有灯,有收音机,如果用户说要用标准开关开收音机,难道还要实现一个RadioAdapter不成?这显然违反了OCP。需求是要“通过加一层让灯支持标准开关”,但是并不是说这一层就要使用灯,为了让这个Adapter更加通用,应该让Adapter依赖ISwitchable接口。像下面这个样子。图2Adapter模式与代码1的差别,仅仅是SwitcherAdapter里的Switchee属性的类型改成ISwitchable而已。代码就不再贴了。其所体现的原则就是上一篇讲的DIP。这个事儿其实任何人静静地想想都能想到。但我绕这个弯子,其实是想顺便表达这样一个意思:一个紧急需求来了的时候,人们更容易倾向于把完成工作放在第一位,从而一时忽视了设计的严谨度,事后又忘了重构,于是BadSmell就这样产生了。当然,这些大家也都知道。面向对象的设计并不是对现实的模拟(这一节算是一个插曲吧,因为这个论点太大,写出来都觉得不自量力,不写又觉得对不起自己爱得瑟的作风。一点拙见,大家多多批评。觉得偏题太远的话可以直接看一下节。)但是(重点来了),为什么紧张时做出的直观设计更可能是错误的呢?因为人一紧张就容易凭感觉,而使用直觉做设计时,大都会以现实世界为原本,但是良好的面向对象设计,是绝对不能仅仅依靠现实世界的。其实图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的问题。可以用下面这个图对比一下,来自《敏捷软件开发》)图3Proxy模式这不是在死抠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了。这事儿也的确真实地发生过。但是如果去看《ProObjective-CDesignPatternforiOS》第112页,对Adapter的描述真的是这样的。“TheDelegationpatternwasonceoneoftheinspirationsforcatalogingtheAdapterpatterninthe“GangofFour”book.”如果你怕我断章取义,可以自己去看。这个人是从类与类之间的关系出发,把具有相似结构、交互方式的类的组合都定义为Adapter。你说他的理解错了吗?我只能说:“狭义来讲,是错的,广义来讲,是对的。”但这是这个世界上最操蛋的答案之一。像上面链接的博客里描述的那个面试者,显然就成了广义与狭义之分的牺牲品——他说的是广义的Adapter,但是面试官想听到的是狭义的Adapter。(不过从后面的叙述来看,那个面试官也是半瓶子醋,问Delegate的时候居然会顺便问异步,让我不得不怀疑他是不是认为事件是异步触发的。)对Adapter有独特的理解很好,能把Adapter,Observer,Delegation,Proxy全统一起来理解更是NB。但是,其实在多数情况下,越是独到的见解,越可能会给面对面的沟通带来障碍。这些独到的见解在个人顿悟模式的过程中很有用,写到书里也很好,毕竟读者可以细细体味,帮助读者从不同的角度思考问题;但想在面试之类的当面沟通的场合上装逼,然后自己的口才又不咋地。怕只会画虎不成反类犬。对Adapter模式的误用学历史的时候,常常见到“左派”、“右派”这样的词,意思是他们走的路线不对。这个词用得很形象,都是走极端。模式的误用,常见的误用之一也是走极端。图2的Adapter模式,成功的把标准的开关接口适配到了我们的接口上。于是便有了一个顺理成章的思