turnonthetv团结另外一种说法形式

&&&& 现在有两个选择。
让我们的灯直接支持标准开关。也就是让灯实现IStandardSwitchable接口。
好处:成本低,实现方式优雅。
坏处:相当于放弃了已经买了我们的灯,又想用标准开关的用户。
不改变现在的灯,让标准开关能打开我们的灯。标准接口我们改不了,灯也不能改。好在计算机界有句话,叫&加一层可以解决一切问题&。这让我想到了买外国电器附赠的那个电源接口转换器。现在,我们的灯需要个类似的玩意儿。
好处:支持所有的灯。
坏处:这东西都是要附赠的,会降低我们的利润。
&&&& 第一个方案很简单,就是让Light多实现个接口就OK了。图就不给了。
&&&& 现在分析第二个方案,标准接口依赖IStandardSwitchable接口,那我们必须有一个类来实现它,并完成所需要的功能&&操作灯。咱也是学过设计模式的人,这个问题很明显可以用Adapter模式来解释。
&&&& 相关类图很容易就可以画出来。
图1 让灯支持IStandardSwitchable接口的方案
&&&& 其对应的代码会是这个样子:
public interface IStandardSwitchable
void TurnOn();
void TurnOff();
public class SwitcherAdapter : IStandardSwitchable
public Light Switchee { }
public void TurnOn()
Switchee.TurnOn();
public void TurnOff()
Switchee.TurnOff();
&&&& 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模式):
想当然地类推。像上面那样,从适配IStandardSwitchable可行,直接推出适配ISwitchable也可以,毕竟这是同样功能的接口啊。但是,不能这样类推。
妄图用同一个方式解决所有问题的想法或创造出一个work for everything的东西的想法。我直觉上就想用热力学第二定律来反驳这想法(和work forever差不多意思),不过&no silver bullet&可能更合适些。但是有些人,尤其是Level越高的,就越容易陷入这个泥潭。可能他们觉得不创造些NB的东西出来,就太对不起大家了。当然,这个想法是很好的,但是也要讲求方法,拿着锤子就看什么都是钉子的做法是要不得的。 参考。第一条就是,独立思考,妄图通过学习各种模式就可以应对一切设计问题的想法就是要不得的。还有一条让我印象很深的就是关于Google的使用,推荐大家也去看看。
对设计原则和设计模式的理解不透彻。如果真正理解了Adapter模式的意图、适用范围。是不会犯这样的错误的。但是很可惜,这个世界上的诱惑太多了,哪怕Wikipedia这样看似很权威的地方都在误导着别人(所以,自己思考,自己判断)。是这样的:&Applying the dependency inversion principle can also be seen as applying the , i.e.&直译过来就是&遵循依赖倒置原则可被视同于应用适配器模式&。Oops&用了适配器模式,那的确是DIP了,但是适配器并不用来达到DIP这个目标的,适配器模式虽然DIP,但是如果用来现实DIP,效果却很糟糕,带来了更多 的问题。我猜作者的本意只是想表达:适配器模式本身是符合DIP原则的。这没错。但是我相信有一票人看到这里就去研究适配器模式并计划用它来实现DIP了。(有人嫌我啰嗦,我只是想把问题说清楚,让更多的人无可误解。)
&&& 这里说的缺乏经验可能并不是工作年限不足的问题,更可能的是态度的问题,要么是对Adapter模式想当然、觉得自己在字面上的理解就差不多,要么是想对Adapter模式进行所谓的&活用&,结果犯了激进冒险主义错误。
喜欢Adapter模式详解的人还喜欢:
【上一篇】:
&& 【下一篇】:
List的各种排序方法设计模式设计模式(Design Pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。这个术语是由埃里希&伽玛等人(Erich Gamma,Richard Helm,Ralph Johnson和John Vlissides这四人提出的。也被称为:Gang of Four,GOF,)在1990年代从建筑设计领域引入到计算机科学的。 设计模式并不能直接用于完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。以上描述摘自维基百科。设计模式出现于1990年代,那时主要的是和C++这样的编程语言。随着计算机语言的发展,越来越多的现代语言的出现和流行,已经改变了设计模式出现时的计算机语言背景。虽然,现在Java和C++依然流行,但、Scala、、等现代语言使用者越来越多。语言(Lisp、Scheme、等)也越来越来受到重视。的视野大大拓展了,手中可使用的工具也更丰富了。所以现在有一部分人质疑设计模式已经与时代发展脱节,认为设计模式是过时的产物。设计模式解决问题的方式,增加了不必要的复杂度。在一些现代语言中,设计模式解决的问题可以更好地被其他方式处理,有些问题甚至可能根本不存在。这种质疑一定程度上是正确的。因为,我们现在拥有了更高级的语言工具。语言的选择是很重要的,因为语言的选择会影响我们看问题的方式。在使用过程式语言时,你可能为了方便读取数据,而“制造”某种类似于类(class)的数据结构,你可能会称这种设计模式为“封装“。但在面向对象编程语言中,你不会认为这是一个需要设计模式来解决的问题,因为语言本身已经为你解决了。在使用面向对象语言时,为了使操作可以在对象间传递,我们使用一个被称为命令模式()的设计模式。但是,在拥有高阶函数的编程语言中,你可能没有意识到这是个问题。因为,你很自然地就将函数(即操作)作为参数传入到另一个函数中,而这就是函数式编程语言中高阶函数的概念。从这点来看,随着我们所使用的编程语言的演化,我们遇到的问题也确实一直在改变。GOF提出的23种设计模式也许部分过时了。但我们遇到的问题并不会消失,设计模式的概念将一直存在:提炼普遍存在的问题,提出解决方案。这其实是一个抽象过程。现在已有的编程语言都存在表达的局限,即对某类问题抽象层次过低。所以,我们在使用任何编程语言时,都还是会遇到一些普遍存在的却没有被语言本身很好解决的问题。这时,我们就会使用到“设计模式”,即人们总结出来的解决方案:遇到问题A,用方案A;遇到问题B,用方案B。只不过问题会一直变化。现在我们不可以再使用那23个模式来解决问题了,但是我们仍然需要总结出其它模式来解决新的问题。这种情况一直会持续到我们拥有“完美语言”的那一天。但现在看起来,这一天还没有到来的迹象。Swift中的设计模式Swift是一门多范式编程语言。除了支持面向对象范式之外,还支持函数式编程范式,和。这使得Swift可以使用函数式编程的某些优秀工具解决我们在面向对象编程中遇到的困难。作为一门崭新的语言(2014年发布),Swift也同时吸收了很多其他语言的优秀特性。这使得与GOF首次提出23个设计模式时相比,我们现在看待Swift中设计模式相关问题的看法肯定已大大不同了。毕竟GOF提出设计模式主要是针对Java和C++这种相对而言更传统的编程语言。在Swift中,一些模式已经被语言特性所吸收,你在使用Swift甚至察觉不出这类问题的存在;一些问题仍然存在,我们仍然需要某种设计模式,但实现起来会更为简便;当然,仍然会存在一些问题,Swift也没有解决。另外一个有趣的相反的问题是:Swift是否引起了一些新问题,需要一些新的设计模式来解决呢?相对于一些实验性编程语言(如Lisp,Haskell)而言,作为一门工业语言,Swift的语言设计其实是保守的,吸收的大都是其他语言成熟的优秀特性。本质上,Swift仍然主要还是一门面向对象编程语言。函数式编程,等新的语言特性的引入,都只是一种改进,还谈不上是革命性的变化。这使得Swift虽然显得保守,但也风险可控,伴随剧烈革命而引入新的问题的可能性被降低了。当然,最终结论仍然需要大规模的业界应用实践之后才能得出。这篇文章主要会涉及Swift改善的那一部分。具体而言,是指Swift消除了哪些设计模式,使哪些设计模式的实现简化了。函数式编程Swift支持函数式编程范式。程序员可以使用Swift写出函数式风格的代码。函数式编程是一种以数学函数为程序语言建模的核心的编程范式。在函数式编程中,函数是核心概念,是“头等公民”,函数被赋予了更多职责,拥有更多灵活的使用方式。这一章可以看到使用函数式编程范式,可以消除一些面向对象编程中使用到的设计模式。高阶函数高阶函数,指可以将其他函数作为参数或者返回结果的函数。由于高阶函数,我们发现GOF设计模式中命令模式(Command)在Swift中消失了。命令模式使用对象封装一系列操作(命令),使得操作可以重复使用,也易于在对象间传递。由于Swift仍然主要是一门面向对象编程语言,我们仍然可以使用Swift实现一个经典的命令模式。实现命令模式的目的只是和之后使用高阶函数的方案对比:// Receiver class Light { func turnOn { println(&The light is on&) } func turnOff { println(&The light is off&) } } // Abstract Command protocol Command { func execute } // Concrete Command class FlipUpCommand: Command { private let light: Light init(light: Light) { self.light = light } func execute { light.turnOn } } class FlipDownCommand: Command { private let light: Light init(light: Light) { self.light = light } func execute { light.turnOff } }以上例程中,灯(Light)是命令(Command)的操作对象(Receiver)。我们定义了命令的协议,同时我们实现两个具体的命令操作:FlipUpCommand和FlipDownCommand。它们分别使灯亮,和使灯灭。// Invoker class LightSwitch { var queue: [Command] = func addCommand(command: Command) { queue.append(command) } func execute { for command in queue { command.execute } } } // Client class Client { static func pressSwitch { let
= Light let flipUpCommand = FlipUpCommand(light: lamp) let flipDownCommand = FlipDownCommand(light: lamp) let switcher = LightSwitch switcher.addCommand(flipUpCommand) switcher.addCommand(flipDownCommand) switcher.addCommand(flipUpCommand) switcher.addCommand(flipDownCommand) switcher.execute } } // Use Client.pressSwitch这段则代码显示了如何使用命令模式。在中,由于存在高阶函数。我们可以直接将一个函数作为参数传给另外一个函数。所以,使用类包裹函数在对象间传递这件事情就显得多余了。以下代码显示如何使用高阶函数达到命令模式相同的效果:// Invoker class LightSwitchFP { var queue: Array&(Light) -& & = func addCommand(command: (Light) -& ) { queue.append(command) } func execute(light: Light) { for command in queue { command(light) } } } // Client class ClientFP { static func pressSwitch { let lamp = Light let flipUp = { (light: Light) -& in light.turnOn } let flipDown = { (light: Light) -& in light.turnOff } let lightSwitchFP = LightSwitchFP lightSwitchFP.addCommand(flipUp) lightSwitchFP.addCommand(flipDown) lightSwitchFP.addCommand(flipUp) lightSwitchFP.addCommand(flipDown) lightSwitchFP.execute() } } // Use ClientFP.pressSwitch使用高阶函数的版本中,负责集中调度命令的LightSwitchFP类有一个接受命令的函数addCommand。由于Swift支持高阶函数,这个函数无需接受一个携带命令函数的对象,而是直接接受表示命令的函数。这样更为直接自然。所以,命令模式在Swift这样拥有高阶函数的编程语言中,就显得多余了。一等函数一等函数,进一步扩展了函数的使用范围,使得函数成为语言中的“头等公民”。这意味函数可在任何其他语言构件(比如变量)出现的地方出现。可以说,一等函数是更严格的高阶函数。策略模式(Strategy)定义了一系列算法,将每个算法封装起来,并且使它们之间可以互相替换。此模式让算法的变化独立于使用算法的客户。下面,我们使用Swift讨论一下一等函数对策略模式的影响。我们先用Swift实现传统的策略模式:protocol Strategy { func compute(first: Int, second: Int) -& Int } class Add: Strategy { func compute(first: Int, second: Int) -& Int { return first + second } } class Multiply: Strategy { func compute(first: Int, second: Int) -& Int { return first * second } } class Context { let strategy: Strategy init(strategy: Strategy) { self.strategy = strategy } func use(first: Int, second: Int) { pute(first, second: second) } } let context = Context(strategy: Add) context.use(1, second: 2)类似于命令模式,策略模式中的策略对象主要用于封装操作(函数),不同的是策略模式中的策略对象封装的是不同的算法。这些算法实现了相同的接口,在这个例子中,接口是用Strategy协议表示的。我们使用两个实现了Strategy协议的具体类:Add和Multiply分别封装两个简单的算法。Context对象,用于对算法进行配置选择,它有一个Strategy类型的实例变量:strategy。通过配置Context的strategy具体类型,可以使用不同的算法。然后我们再看看如果存在一等函数,策略模式是否可以得到化简:let add = { (first: Int, second: Int) -& Int in return first + second } let multiply = { (first: Int, second: Int) -& Int in return first * second } class ContextFP { let strategy: (Int, Int) -& Int init(strategy: (Int, Int) -& Int) { self.strategy = strategy } func use(first: Int, second: Int) { strategy(first, second) } } let fpContext = FPContext(strategy: multiply) fpContext.use(1, second:2)由于Swift的函数都是一等函数,使得我们可以把函数作为参数传给另外一个函数。这无需在使用对象来封装算法。函数可以成为封装算法的载体,这样更为直接自然。例子中,ContextFP的构造器的传参就是函数类型。给予构造器代表不同算法的函数,就配置了不同的算法。函数也可以作为类的实例变量。这样在类中,直接维护代表算法的函数也成为可能。从类型声明可以看出,ContextFP中的实例变量strategy就是一个函数。一等函数的概念使得函数获得了更高的地位,使得函数的灵活性大大增加。在很多场景下直接使用函数会是更直接自然的选择。范式,赋予了对象更高的地位。但是,如果给予函数“正常”一些的地位,可以简化不少问题。设计模式中的不少模式存在都是由于函数的使用限制,需要使用在使用类包裹函数。类似的例子还有模版方法模式(Template method)。柯里化函数柯里化函数(Curried Function),是指接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,该函数返回一个接受余下参数的新函数。这个名词来源于逻辑学家:Haskell Curring。编程语言也取自这位逻辑学家的名字。(Abstract Factory)提供了一种方式,可以将一组具有同一主题的单独的工厂封装起来。在正常使用中,客户端程序需要创建的具体实现,然后使用抽象工厂作为接口来创建这一主题的具体对象。客户端程序不需要知道(或关心)它从这些内部的工厂方法中获得对象的具体类型,因为客户端程序仅使用这些对象的通用接口。抽象工厂模式将一组对象的实现细节与他们的一般使用分离开来。函数柯里化可以用于制造一系列相关的函数。这和抽象工厂模式的目的类似。仍然如前例,先使用Swift实现经典的抽象工厂模式。首先,我们给出一个产品族,包括两个牌子,两种设备类型的四个组合:// Abstract Product protocol Product { var brand: String { get } var name: String { get } } protocol Phone: Product { } protocol Pad: Product { } // Concrete Product class ApplePhone: Phone { var brand: String { return &Apple& } var name: String init(name: String) { self.name = name } } class SamsungPhone: Phone { var brand: String { return &Samsung& } var name: String init(name: String) { self.name = name } } class ApplePad: Pad { var brand: String { return &Apple& } var name: String init(name: String) { self.name = name } } class SamsungPad: Pad { var brand: String { return &Samsung& } var name: String init(name: String) { self.name = name } }然后,我们需要抽象工厂模式,来为创建这些同一主题的产品提供易于使用,方便扩展的方法:// Abstract Factory protocol AbstractFactory { func createPhone -& Phone func createPad -& Pad } // Concrete Factory class AppleFactory: AbstractFactory { func createPhone -& Phone { return ApplePhone(name: &iPhone 6S&) } func createPad -& Pad { return ApplePad(name: &iPad Air 2&) } } class SamsungFactory: AbstractFactory { func createPhone -& Phone { return SamsungPhone(name: &Note 5&) } func createPad -& Pad { return SamsungPad(name: &Note&) } }再看看如何使用抽象工厂:let appleFactory: AbstractFactory = AppleFactory appleFactory.createPad let samsungFactory: AbstractFactory = SamsungFactory samsungFactory.createPhone对于这个抽象工厂实现来说,虽然加入一个新的设备类型会有很大问题,需要改变的接口,但如果只增加品牌就很好办了。只需在实现一个具体品牌的工厂即可。然后,我们使用柯里化函数来实现一个工厂方法。这个函数接受创建产品所需要各种参数,返回具体的产品。但我们实际上不会使用这个工厂方法直接创建产品。而是使用柯里化,让这个工厂方法充当一个函数工厂,创建一批可以作为产品工厂的方法。然后,我们可以使用这批产品工厂方法创建具体的产品。func createProductWithType(type: String)(brand: String)(name: String) -& Product? { switch (type, brand) { case (&Phone&, &Apple&): return ApplePhone(name: name) case (&Pad&, &Apple&): return ApplePad(name: name) case (&Phone&, &Samsung&): return SamsungPhone(name: name) case (&Pad&, &Samsung&): return SamsungPad(name: name) default: return nil } } let createApplePhone = createProductWithType(&Phone&)(brand: &Apple&) let createSamsungPhone = createProductWithType(&Phone&)(brand: &Samsung&) let createApplePad = createProductWithType(&Pad&)(brand: &Apple&) let createSamsungPad = createProductWithType(&Pad&)(brand: &Samsung&) // Use let applePhone = createApplePhone(name: &iPhone 6S&) println(applePhone)与抽象工厂模式相比,使用柯里化函数充当函数工厂,我们可以更轻松地基于一些条件创建一系列工厂方法。Swift的语言特性扩展扩展(Extension)扩展是一种向已有的类,枚举或者结构体添加新功能的方法。扩展和Objective-C中的类别(Category)类似,但是与Objective-C中的分类不同的是,Swift中的扩展没有名字。适配器模式(Adapter)将一个类的接口转接成用户所期待的。适配器使得因接口不兼容而不能在一起工作的类工作在一起。通常适配器有两种实现方法:一种使用继承,创建新类,并继承原有类,同时实现需要兼容的接口;另一种使用组合,创建新类,维护原有类的实例对象,并实现需要兼容的接口。无论哪种方法我们都需要创建一个新的被称为适配器的类。扩展使得我们多了一种为类增添方法的手段。适配器模式的实质是使得一个已经存在的类,适配另外一个接口。实现接口其实就是实现接口所定义的方法。如果拥有扩展这种工具,就无需适配器模式这种笨重的做法。下面先试着完成适配器的经典实现。首先,我们写一套配合完美的圆木桩和圆洞。如果圆木桩的直径小于圆洞的直径,毫无疑问,我们可以把圆木桩打入圆洞。protocol
{ var : Double { get } } class RoundPeg: Circularity { let radius: Double init(radius: Double) { self.radius = radius } } class RoundHole { let radius: Double init(radius: Double) { self.radius = radius } func pegFits(peg: Circularity) -& Bool { return peg.radius &= radius } }现在我们的工地出现了一根方木桩。我们仍然需要测试这个方木桩是否可以打入圆洞。由于木桩是否能被打入圆洞的测试要求木桩实现了Circularity,这时我们就需要一个适配器,将SquarePeg适配为符合Circularity接口的类。适配的结果就是下面的SquarePegAdaptor,它实现了Circularity接口。这样,我们可以判断它是否能打入圆洞。class SquarePeg { let width: Double init(width: Double) { self.width = width } } class SquarePegAdaptor: Circularity { private let peg: SquarePeg var radius: Double { get { return sqrt(pow(peg.width/2, 2) * 2) } } init(peg: SquarePeg) { self.peg = peg } }检测结果:// Test if the square peg is fit with the hole let hole = RoundHole(radius: 5.0) for i in 5...10 { let squarePeg = SquarePeg(width: Double(i)) let peg: Circularity = SquarePegAdaptor(peg: squarePeg) let fit = hole.pegFits(peg) println(&width:\(i), fit:\(fit)&) }然后,我们看看使用扩展是否能简化适配器模式:// Use extension extension SquarePeg: Circularity { var : Double { get { return sqrt(pow(width/2, 2) * 2) } } }使用扩展,我们无需像适配器模式那样,创建新类充当适配器。而只需要扩展原有类,为原有类添加一个方法,同时就实现了接口。在使用时,也更简单了,我们只需要直接使用原有类即可:for i in 5...10 { let peg = SquarePeg(width: Double(i)) let fit = hole.pegFits(peg) println(&width:\(i), fit:\(fit)&) }由于Swift提供了扩展(Extension)这一新的语言特性,使得处理接口转换这类原来需要适配器模式解决的问题时,更为简单了。模式(Interpreter)定义语言的文法,并建立一个解释器来解释语言中的句子。本质上说,解释器模式是一种语言扩展工具。如果你使用的语言不适合解决某类问题,可以使用解释器模式构建一些新的语法,甚至新的语言用于更为方便地解决特定领域的问题。这是在语言不提供语言级别的对自身扩展时的一种替代。一些语言会禁止对语言本身进行扩展,例如Java。这可能是因为Java的设计者认为,扩展语言需要专业的技能和良好的语法设计,这通常不是普通可以胜任的。扩展语言这种特性常常会带来一些容易令人混淆的问题,很多描述这类技术的文档,也通常会告诫读者,“如果你不清楚自己在做什么事情,就最好不要使用这个功能”。但另一方面,语言禁止对本身的扩展也就限制了其使用范围,所以,语言中才会出现解释器模式这种解决扩展语言自身功能的方案。当然不可避免的,这种解决方案会比有语言级别的支持复杂晦涩。下面我们会讨论一种扩展语言语法的语言特性:运算符重载。看看有了运算符重载,解释器模式是否能得到简化。运算符重载是多态的一种。运算符(比如+,=或==)被当作多态函数,他们的行为随着其参数类型的不同而不同。这意味着我们可以赋予一个运算符新的含义,使用在新的数据类型上,这就是一种扩展语言语法的特性。我们仍然分别用解释器模式和运算符重载来解决同一个问题。两个例程的比较可以清晰地说明差别。问题的要求是实现复数的加法和乘法。先用解释器模式实现:import Foundation protocol
{ func interprete(:Dictionary&String, Expression&) -&
ComplexNumber: Expression { let real: Double let imaginary: Double init(real: Double, imaginary: Double) { self.real = real self.imaginary = imaginary } func interprete(variables:Dictionary&String, Expression&) -& ComplexNumber { return self } } struct Plus: Expression { let leftOperand: Expression let rightOperand: Expression init(left: Expression, right: Expression) { self.leftOperand = left self.rightOperand = right } func interprete(variables:Dictionary&String, Expression&) -& ComplexNumber { let left = leftOperand.interprete(variables) let right = rightOperand.interprete(variables) return ComplexNumber(real: left.real + right.real, imaginary: left.imaginary + right.imaginary) } } struct Multiply: Expression { let leftOperand: Expression let rightOperand: Expression init(left: Expression, right: Expression) { self.leftOperand = left self.rightOperand = right } func interprete(variables:Dictionary&String, Expression&) -& ComplexNumber { let left = leftOperand.interprete(variables) let right = rightOperand.interprete(variables) let resultReal = left.real * right.real - left.imaginary * right.imaginary let resultImaginary = left.real * right.imaginary + left.imaginary * right.real return ComplexNumber(real: resultReal, imaginary: resultImaginary) } } struct Variable: Expression { let name: String init(name: String) { self.name = name } func interprete(variables: Dictionary&String, Expression&) -& ComplexNumber { if let variable = variables[name] { return variable.interprete(variables) } return ComplexNumber(real: 0, imaginary: 0) } }然后,我们实现解释器。为了使实现更简单,问题更聚焦,我们只实现形式的复数加法和乘法。这是因为对于后缀表达式,我们只需借助栈(Stack)对表达式进行一次遍历,就可以计算出结果,这大大降低了算法复杂度。而对中缀表达式的解释,我们通常也会先将它转化为,再做解释,计算出结果。鉴于这个例子里主要是展示解释器模式,而不是讲解解释器实现,所以,我们仅实现最简单的后缀加法和乘法。struct Stack&M& { var items = Array&M& mutating func push(item: M) { items.append(item) } mutating func pop -& M { return items.removeLast } }
Evaluator: Expression { let syntaxTree: Expression init(expression: String) { var expressionStack = Stack&Expression& let tokens = ponentsSeparatedByString(& &) for token in tokens { if token == &+& { let subExpression = Plus(left: expressionStack.pop, right: expressionStack.pop) expressionStack.push(subExpression) } else if token == &*& { let subExpression = Multiply(left: expressionStack.pop, right: expressionStack.pop) expressionStack.push(subExpression) } else { expressionStack.push(Variable(name: token)) } } syntaxTree = expressionStack.pop } func interprete(variables: Dictionary&String, Expression&) -& ComplexNumber { return syntaxTree.interprete(variables) } }最后,看看如何使用解释器模式:let expression = &w x *& let sentence = Evaluator(expression: expression) var variables = Dictionary&String, & variables[&w&] = ComplexNumber(real: 1, imaginary: 2) variables[&x&] = ComplexNumber(real: 2, imaginary: 4) sentence.interprete()由于Swift支持运算符重载,我们可以更简单地使用语言已经提供的特性完成上面例程中相同功能。Swift支持以运算符作为函数名。所以,对于Swift中已经实现的运算符,我们只需要重载两个函数,使运算符可以应用于新的类型:复数类型,即可。func + (left: ComplexNumber, right: ComplexNumber) -& ComplexNumber { let resultReal = left.real + right.real let resultImaginary = left.imaginary + right.imaginary return ComplexNumber(real: resultReal, imaginary: resultImaginary) } func * (left: ComplexNumber, right: ComplexNumber) -& ComplexNumber { let resultReal = left.real * right.real - left.imaginary * right.imaginary let resultImaginary = left.real * right.imaginary + left.imaginary * right.real return ComplexNumber(real: resultReal, imaginary: resultImaginary) }完成运算符重载之后,我们就可以像将两个整数相加一样,使用加法运算符将两个复数相加:let first = ComplexNumber(real: 1, imaginary: 2) let second = (real: 2, imaginary: 4) first + second first * second可以看到运算符重载使得扩展运算符的使用范围变得更容易了。这也很容易理解,解释器模式其实就是对语言扩展自身语法这一功能的缺失而设计的设计模式。再次设计很有时候需要绕过很多障碍,很容易比语言的原生支持更为复杂繁琐。总结这篇短文中,我们回顾了设计模式的概念,并讨论了在现代编程语言背景下,设计模式相关问题的一些变化。然后使用语言演示了5个设计模式和新的语言特性之间的关系。这5个设计模式和语言特性分别是:高阶函数和命令模式,一等函数和策略模式,柯里化函数和,扩展和适配器模式,和模式。这5个例子展示了设计模式在现代中的变化:要么被语言特性更好的代替了;要么变得更为简单了。参考文档给InfoQ中文站投稿或者参与内容工作,请邮件至q.com。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群)。全球架构师峰会,12月18-19日,北京&国际会议中心,9折报名截止11月27日!您好,朋友!您需要注册一个InfoQ账号或者登录才能进行评论。在您完成注册后还需要进行一些设置。告诉我们您的想法允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p当有人回复此评论时请E-mail通知我不错by孙 庚泽不错,写的挺好的,严谨且易懂,给小编32个赞!!将为您减少类似内容我要收藏335个赞不感兴趣分享到分享到:相关文章还可以输入140字热门频道14.8万人订阅23.1万人订阅38.5万人订阅20.3万人订阅24.3万人订阅你还可用第三方账号来登录请输入你注册的电子邮件地址绑定密保手机*您可用使用此密保手机找回密码及登录*请勿随意泄露手机号,以防被不法分子利用,骗取帐号信息手机号码发送验证码确定电子邮件请输入您的意见和建议请您输入正确的邮箱地址,以便我们和您联系,帮您解决问题。扫描下载手机客户端热门搜词}

我要回帖

更多关于 tv的完全形式 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信