本系列《面向接口编程详解》将分为三部分:
- 面向接口编程详解(一)——思想基础
在这一篇中,将对接口及面向接口编程有个大致的介绍,着重在于思想上的讲解。 - 面向接口编程详解(二)——编程实例
这一篇将结合一个实例“移动存储设备模拟”来让大家对面向接口编程有个直观印象。 - 面向接口编程详解(三)——模式研究
讲解几个设计模式中的面向接口思想和基于.NET平台的分层架构中的面向接口思想,加深理解。 面向接口编程详解(一)——思想基础 我想,对于各位使用面向对象编程语言的程序员来说,“接口”这个名词一定不陌生,但是不知各位有 没有这样的疑惑:接口有什么用途?它和抽象类有什么区别?能不能用抽象类代替接口呢?而且,作为程序员,一定经常听到“面向接口编程”这个短语,那么它是 什么意思?有什么思想内涵?和面向对象编程是什么关系?本文将一一解答这些疑问。 1.面向接口编程和面向对象编程是什么关系 首先,面向接口编程和面向对象编程并不是平级的,它并不是比面向对象编程更先进的一种独立的编程思想,而是附属于面向对象思想体系,属于其一部分。或者说,它是面向对象编程体系中的思想精髓之一。 2.接口的本质 接口,在表面上是由几个没有主体代码的方法定义组成的集合体,有唯一的名称,可以被类或其他接口所实现(或者也可以说继承)。它在形式上可能是如下的样子:
interface
InterfaceName
{ void Method1(); void Method2( int para1); void Method3( string para2, string para3); }
那么,接口的本质是什么呢?或者说接口存在的意义是什么。我认为可以从以下两个视角考虑: 1)接口是一组规则的集合,它规定了实现本接口的类或接口必须拥有的一组规则。体现了自然界“如果你是……则必须能……”的理念。 例如,在自然界中,人都能吃饭,即“如果你是人,则必须能吃饭”。那么模拟到计算机程序 中,就应该有一个IPerson(习惯上,接口名由“I”开头)接口,并有一个方法叫Eat(),然后我们规定,每一个表示“人”的类,必须实现 IPerson接口,这就模拟了自然界“如果你是人,则必须能吃饭”这条规则。 从这里,我想各位也能看到些许面向对象思想的东西。面向对象思想的核心之一,就是模拟真实世界,把真实世界中的事物抽象成类,整个程序靠各个类的实例互相通信、互相协作完成系统功能,这非常符合真实世界的运行状况,也是面向对象思想的精髓。 2)接口是在一定粒度视图上同类事物的抽象表示。注意这里我强调了在一定粒度视图上,因为“同类事物”这个概念是相对的,它因为粒度视图不同而不同。 例如,在我的眼里,我是一个人,和一头猪有本质区别,我可以接受我和我同学是同类这个说 法,但绝不能接受我和一头猪是同类。但是,如果在一个动物学家眼里,我和猪应该是同类,因为我们都是动物,他可以认为“人”和“猪”都实现了 IAnimal这个接口,而他在研究动物行为时,不会把我和猪分开对待,而会从“动物”这个较大的粒度上研究,但他会认为我和一棵树有本质区别。 现在换了一个遗传学家,情况又不同了,因为生物都能遗传,所以在他眼里,我不仅和猪没区别,和一只蚊子、一个细菌、一颗树、一个蘑菇乃至一个SARS病毒都没什么区别,因为他会认为我们都实现了IDescendable这个接口(注:descend vi. 遗传),即我们都是可遗传的东西,他不会分别研究我们,而会将所有生物作为同类进行研究,在他眼里没有人和病毒之分,只有可遗传的物质和不可遗传的物质。但至少,我和一块石头还是有区别的。 可不幸的事情发生了,某日,地球上出现了一位伟大的人,他叫列宁,他在熟读马克思、恩格斯的辩证唯物主义思想巨著后,颇有心得,于是他下了一个著名的定 义:所谓物质,就是能被意识所反映的客观实在。至此,我和一块石头、一丝空气、一条成语和传输手机信号的电磁场已经没什么区别了,因为在列宁的眼里,我们 都是可以被意识所反映的客观实在。如果列宁是一名程序员,他会这么说:所谓物质,就是所有同时实现了“IReflectabe”和“IEsse”两个接口 的类所生成的实例。(注:reflect v. 反映 esse n. 客观实在) 也许你会觉得我上面的例子像在瞎掰,但是,这正是接口得以存在的意义。面向对象思想和核心之一叫做多态性,什么叫多态性?说白了就是在某个粒度视图层面上 对同类事物不加区别的对待而统一处理。而之所以敢这样做,就是因为有接口的存在。像那个遗传学家,他明白所有生物都实现了IDescendable接口, 那只要是生物,一定有Descend()这个方法,于是他就可以统一研究,而不至于分别研究每一种生物而最终累死。 可能这里还不能给你一个关于接口本质和作用的直观印象。那么在后文的例子和对几个设计模式的解析中,你将会更直观体验到接口的内涵。 3.面向接口编程综述 通过上文,我想大家对接口和接口的思想内涵有了一个了解,那么什么是面向接口编程呢?我个人的定义是:在系统分析和架构中,分清层次和依赖关系,每个层次不是直接向其上层提供服务(即不是直接实例化在上层中),而是通过定义一组接口,仅向上层暴露其接口功能,上层对于下层仅仅是接口依赖,而不依赖具体类 。 这样做的好处是显而易见的,首先对系统灵活性大有好处。当下层需要改变时,只要接口及接口功能不变,则上层不用做任何修改。甚至可以在不改动上层代码时将 下层整个替换掉,就像我们将一个WD的60G硬盘换成一个希捷的160G的硬盘,计算机其他地方不用做任何改动,而是把原硬盘拔下来、新硬盘插上就行了, 因为计算机其他部分不依赖具体硬盘,而只依赖一个IDE接口,只要硬盘实现了这个接口,就可以替换上去。从这里看,程序中的接口和现实中的接口极为相似, 所以我一直认为,接口(interface)这个词用的真是神似! 使用接口的另一个好处就是不同部件或层次的开发人员可以并行开工,就像造硬盘的不用等造CPU的,也不用等造显示器的,只要接口一致,设计合理,完全可以并行进行开发,从而提高效率。 本篇文章先到这里。最后我想再啰嗦一句:面向对象的精髓是模拟现实,这也可以说是我这篇文章的灵魂。所以,多从现实中思考面向对象的东西,对提高系统分析设计能力大有脾益。 下篇文章,我将用一个实例来展示接口编程的基本方法。 而第三篇,我将解析经典设计模式中的一些面向接口编程思想,并解析一下.NET分层架构中的面向接口思想。 对本文的补充: 仔细看了各位的回复,非常高兴能和大家一起讨论技术问题。感谢给出肯定的朋友,也要感谢提出意见和质疑的朋友,这促使我更深入思考一些东西,希望能借此进步。在这里我想补充一些东西,以讨论一些回复中比较集中的问题。 1.关于“面向接口编程”中的“接口”与具体面向对象语言中“接口”两个词 看到有朋友提出“面向接口编程”中的“接口”二 字应该比单纯编程语言中的interface范围更大。我经过思考,觉得很有道理。这里我写的确实不太合理。我想,面向对象语言中的“接口”是指具体的一 种代码结构,例如C#中用interface关键字定义的接口。而“面向接口编程”中的“接口”可以说是一种从软件架构的角度、从一个更抽象的层面上指那 种用于隐藏具体底层类和实现多态性的结构部件。从这个意义上说,如果定义一个抽象类,并且目的是为了实现多态,那么我认为把这个抽象类也称为“接口”是合 理的。但是用抽象类实现多态合理不合理?在下面第二条讨论。 概括来说,我觉得两个“接口”的概念既相互区别又相互联系。“面向接口编程”中的接口是一种思想层面的用于实现多态性、提高软件灵活性和可维护性的架构部件,而具体语言中的“接口”是将这种思想中的部件具体实施到代码里的手段。 2.关于抽象类与接口 看到回复中这是讨论的比较激烈的一个问题。很抱歉我考虑不周没有在文章中讨论这个问题。我个人对这个问题的理解如下: 如果单从具体代码来看,对这两个概念很容易模糊,甚至觉得接口就是多余的,因为单从具体功能来看,除多重继承外(C#,Java中),抽象类似乎完全能取代接口。但是,难道接口的存在是为了实现多重继承?当然不是。我认为,抽象类和接口的区别在于使用动机。使用抽象类是为了代码的复用,而使用接口的动机是为了实现多态性。 所以,如果你在为某个地方该使用接口还是抽象类而犹豫不决时,那么可以想想你的动机是什么。 看到有朋友对IPerson这个接口的质疑,我个人的理解是,IPerson这个接口该不该定义,关键看具体应用中是怎么个情况。如果我们的项目中有 Women和Man,都继承Person,而且Women和Man绝大多数方法都相同,只有一个方法DoSomethingInWC()不同(例子比较粗 俗,各位见谅),那么当然定义一个AbstractPerson抽象类比较合理,因为它可以把其他所有方法都包含进去,子类只定义 DoSomethingInWC(),大大减少了重复代码量。 但是,如果我们程序中的Women和Man两个类基本没有共同代码,而且有一个PersonHandle类需要实例化他们,并且不希望知道他们是男是女,而只需把他们当作人看待,并实现多态,那么定义成接口就有必要了。 总而言之,接口与抽象类的区别主要在于使用的动机,而不在于其本身。而一个东西该定义成抽象类还是接口,要根据具体环境的上下文决定。 再者,我认为接口和抽象类的另一个区别在于,抽象类和它的子类之间应该是一般和特殊的关系,而接口仅仅是它的子类应该实现的一组规则。(当然,有时也可能 存在一般与特殊的关系,但我们使用接口的目的不在这里)如,交通工具定义成抽象类,汽车、飞机、轮船定义成子类,是可以接受的,因为汽车、飞机、轮船都是 一种特殊的交通工具。再譬如Icomparable接口,它只是说,实现这个接口的类必须要可以进行比较,这是一条规则。如果Car这个类实现了 Icomparable,只是说,我们的Car中有一个方法可以对两个Car的实例进行比较,可能是比哪辆车更贵,也可能比哪辆车更大,这都无所谓,但我 们不能说“汽车是一种特殊的可以比较”,这在文法上都不通。 面向接口编程详解(二)——编程实例 通过上一篇文章的讨论,我想各位朋友对“面接接口编程”有了一个大致的了解。那么在这一篇里,我们用一个例子,让各位对这个重要的编程思想有个直观的印象。为充分考虑到初学者,所以这个例子非常简单,望各位高手见谅。 问题的提出
定义: 现在我们要开发一个应用,模拟移动存储设备的读写,即计算机与U盘、MP3、移动硬盘等设备进行数据交换。 上下文(环境): 已知要实现U盘、MP3播放器、移动硬盘三种移动存储设备,要求计算机能同这三种设备进行数据交换,并且以后可能会有新的第三方的移动存储设备,所以计算机必须有扩展性,能与目前未知而以后可能会出现的存储设备进行数据交换。 各个存储设备间读、写的实现方法不同,U盘和移动硬盘只有这两个方法,MP3Player还有一个PlayMusic方法。 名词定义: 数据交换={读,写} 看到上面的问题,我想各位脑子中一定有了不少想法,这是个很好解决的问题,很多方案都能达到效果。下面,我列举几个典型的方案。 解决方案列举
方案一: 分 别定义FlashDisk、MP3Player、MobileHardDisk三个类,实现各自的Read和Write方法。然后在Computer类中 实例化上述三个类,为每个类分别写读、写方法。例如,为FlashDisk写ReadFromFlashDisk、WriteToFlashDisk两个 方法。总共六个方法。 方案二: 定 义抽象类MobileStorage,在里面写虚方法Read和Write,三个存储设备继承此抽象类,并重写Read和Write方法。 Computer类中包含一个类型为MobileStorage的成员变量,并为其编写get/set器,这样Computer中只需要两个方 法:ReadData和WriteData,并通过多态性实现不同移动设备的读写。 方案三: 与方案二基本相同,只是不定义抽象类,而是定义接口IMobileStorage,移动存储器类实现此接口。Computer中通过依赖接口IMobileStorage实现多态性。 方案四: 定义接口IReadable和IWritable,两个接口分别只包含Read和Write,然后定义接口IMobileStorage接口继承自IReadable和IWritable,剩下的实现与方案三相同。 下面,我们来分析一下以上四种方案: 首先,方案一最直白,实现起来最简单,但是它有一个致命的弱点:可扩展性差。或者说,不符合“开放-关闭原则”(注:意为对扩展开放,对修改关闭)。当将 来有了第三方扩展移动存储设备时,必须对Computer进行修改。这就如在一个真实的计算机上,为每一种移动存储设备实现一个不同的插口、并分别有各自 的驱动程序。当有了一种新的移动存储设备后,我们就要将计算机大卸八块,然后增加一个新的插口,在编写一套针对此新设备的驱动程序。这种设计显然不可取。 此方案的另一个缺点在于,冗余代码多。如果有100种移动存储,那我们的Computer中岂不是要至少写200个方法,这是不能接受的! 我们再来看方案二和方案三,之所以将这两个方案放在一起讨论,是因为他们基本是一个方案(从思想层面上来说),只不过实现手段不同,一个是使用了抽象类,一个是使用了接口,而且最终达到的目的应该是一样的。 我们先来评价这种方案:首先它解决了代码冗余的问题,因为可以动态替换移动设备,并且都实现了共同的接口,所以不管有多少种移动设备,只要一个Read方 法和一个Write方法,多态性就帮我们解决问题了。而对第一个问题,由于可以运行时动态替换,而不必将移动存储类硬编码在Computer中,所以有了 新的第三方设备,完全可以替换进去运行。这就是所谓的“依赖接口,而不是依赖与具体类”,不信你看看,Computer类只有一个 MobileStorage类型或IMobileStorage类型的成员变量,至于这个变量具体是什么类型,它并不知道,这取决于我们在运行时给这个变 量的赋值。如此一来,Computer和移动存储器类的耦合度大大下降。 那么这里该选抽象类还是接口呢?还记得第一篇文章我对抽象类和接口选择的建议吗?看动机。这里,我们的动机显然是实现多态性而不是为了代码复用,所以当然要用接口。 最后我们再来看一看方案四,它和方案三很类似,只是将“可读”和“可写”两个规则分别抽象成了接口,然后让IMobileStorage再继承它们。这样 做,显然进一步提高了灵活性,但是,这有没有设计过度的嫌疑呢?我的观点是:这要看具体情况。如果我们的应用中可能会出现一些类,这些类只实现读方法或只 实现写方法,如只读光盘,那么这样做也是可以的。如果我们知道以后出现的东西都是能读又能写的,那这两个接口就没有必要了。其实如果将只读设备的 Write方法留空或抛出异常,也可以不要这两个接口。总之一句话:理论是死的,人是活的,一切从现实需要来,防止设计不足,也要防止设计过度。 在这里,我们姑且认为以后的移动存储都是能读又能写的,所以我们选方案三。 实现
下面,我们要将解决方案加以实现。我选择的语言是C#,但是在代码中不会用到C#特有的性质,所以使用其他语言的朋友一样可以参考。 首先编写IMobileStorage接口:
1
namespace
InterfaceExample
2
{ 3 public interface IMobileStorage 4 { 5 void Read(); //从自身读数据 6 void Write(); //将数据写入自身 7 } 8 }
比较简单,只有两个方法,没什么好说的,接下来是三个移动存储设备的具体实现代码: U盘
1
namespace
InterfaceExample
2
{ 3 public class FlashDisk : IMobileStorage 4 { 5 public void Read() 6 { 7 Console.WriteLine( "Reading from FlashDisk……" ); 8 Console.WriteLine( "Read finished!" ); 9 } 10 11 public void Write() 12 { 13 Console.WriteLine( "Writing to FlashDisk……" ); 14 Console.WriteLine( "Write finished!" ); 15 } 16 } 17 }
MP3
1
namespace
InterfaceExample
2
{ 3 public class MP3Player : IMobileStorage 4 { 5 public void Read() 6 { 7 Console.WriteLine( "Reading from MP3Player……" ); 8 Console.WriteLine( "Read finished!" ); 9 } 10 11 public void Write() 12 { 13 Console.WriteLine( "Writing to MP3Player……" ); 14 Console.WriteLine( "Write finished!" ); 15 } 16 17 public void PlayMusic() 18 { 19 Console.WriteLine( "Music is playing……" ); 20 } 21 } 22 }
移动硬盘
1
namespace
InterfaceExample
2
{ 3 public class MobileHardDisk : IMobileStorage 4 { 5 public void Read() 6 { 7 Console.WriteLine( "Reading from MobileHardDisk……" ); 8 Console.WriteLine( "Read finished!" ); 9 } 10 11 public void Write() 12 { 13 Console.WriteLine( "Writing to MobileHardDisk……" ); 14 Console.WriteLine( "Write finished!" ); 15 } 16 } 17 }
可以看到,它们都实现了IMobileStorage接口,并重写了各自不同的Read和Write方法。下面,我们来写Computer:
1
namespace
InterfaceExample
2
{ 3 public class Computer 4 { 5 private IMobileStorage _usbDrive; 6 7 public IMobileStorage UsbDrive 8 { 9 get 10 { 11 return this ._usbDrive; 12 } 13 set 14 { 15 this ._usbDrive = value; 16 } 17 } 18 19 public Computer() 20 { 21 } 22 23 public Computer(IMobileStorage usbDrive) 24 { 25 this .UsbDrive = usbDrive; 26 } 27 28 public void ReadData() 29 { 30 this ._usbDrive.Read(); 31 } 32 33 public void WriteData() 34 { 35 this ._usbDrive.Write(); 36 } 37 } 38 }
其中的UsbDrive就是可替换的移动存储设备,之所以用这个名字,是为了让大家觉得直观,就像我们平常使用电脑上的USB插口插拔设备一样。 OK!下面我们来测试我们的“电脑”和“移动存储设备”是否工作正常。我是用的C#控制台程序,具体代码如下:
1
namespace
InterfaceExample
2
{ 3 class Program 4 { 5 static void Main( string [] args) 6 { 7 Computer computer = new Computer(); 8 IMobileStorage mp3Player = new MP3Player(); 9 IMobileStorage flashDisk = new FlashDisk(); 10 IMobileStorage mobileHardDisk = new MobileHardDisk(); 11 12 Console.WriteLine( "I inserted my MP3 Player into my computer and copy some music to it:" ); 13 computer.UsbDrive = mp3Player; 14 computer.WriteData(); 15 Console.WriteLine(); 16 17 Console.WriteLine( "Well,I also want to copy a great movie to my computer from a mobile hard disk:" ); 18 computer.UsbDrive = mobileHardDisk; 19 computer.ReadData(); 20 Console.WriteLine(); 21 22 Console.WriteLine( "OK!I have to read some files from my flash disk and copy another file to it:" ); 23 computer.UsbDrive = flashDisk; 24 computer.ReadData(); 25 computer.WriteData(); 26 Console.ReadLine(); 27 } 28 } 29 }
现在编译、运行程序,如果没有问题,将看到如下运行结果: 好的,看来我们的系统工作良好。 后来……
刚过了一个星期,就有人送来了新的移动存储设备NewMobileStorage,让我测试能不能用,我微微一笑,心想这不是小菜一碟,让我们看看面向接口编程的威力吧!将测试程序修改成如下:
1
namespace
InterfaceExample
2
{ 3 class Program 4 { 5 static void Main( string [] args) 6 { 7 Computer computer = new Computer(); 8 IMobileStorage newMobileStorage = new NewMobileStorage(); 9 10 Console.WriteLine( "Now,I am testing the new mobile storage:" ); 11 computer.UsbDrive = newMobileStorage; 12 computer.ReadData(); 13 computer.WriteData(); 14 Console.ReadLine(); 15 } 16 } 17 }
编译、运行、看结果: 哈哈,神奇吧,Computer一点都不用改动,就可以使新的设备正常运行。这就是所谓“对扩展开放,对修改关闭”。 又过了几天,有人通知我说又有一个叫SuperStorage的移动设备要接到我们的Computer上,我心想来吧,管你是“超级存储”还是“特级存储”,我的“面向接口编程大法”把你们统统搞定。 但是,当设备真的送来,我傻眼了,开发这个新设备的团队没有拿到我们的IMobileStorage接口,自然也没有遵照这个约定。这个设备的读、写方法 不叫Read和Write,而是叫rd和wt,这下完了……不符合接口啊,插不上。但是,不要着急,我们回到现实来找找解决的办法。我们一起想想:如果你 的Computer上只有USB接口,而有人拿来一个PS/2的鼠标要插上用,你该怎么办?想起来了吧,是不是有一种叫“PS/2-USB”转换器的东 西?也叫适配器,可以进行不同接口的转换。对了!程序中也有转换器。 这里,我要引入一个设计模式,叫“Adapter”。它的作用就如现实中的适配器一样,把接口不一致的两个插件接合起来。由于本篇不是讲设计模式的,而且Adapter设计模式很好理解,所以我就不细讲了,先来看我设计的类图吧: 如图所示,虽然SuperStorage没有实现IMobileStorage,但我们定义了一个实现IMobileStorage的 SuperStorageAdapter,它聚合了一个SuperStorage,并将rd和wt适配为Read和 Write,SuperStorageAdapter的具体代码如下:
1
namespace
InterfaceExample
2
{ 3 public class SuperStorageAdapter : IMobileStorage 4 { 5 private SuperStorage _superStorage; 6 7 public SuperStorage SuperStorage 8 { 9 get 10 { 11 return this ._superStorage; 12 } 13 set 14 { 15 this ._superStorage = value; 16 } 17 } 18 19 public void Read() 20 { 21 this ._superStorage.rd(); 22 } 23 24 public void Write() 25 { 26 this ._superStorage.wt(); 27 } 28 } 29 }
好,现在我们来测试适配过的新设备,测试代码如下:
1
namespace
InterfaceExample
2
{ 3 class Program 4 { 5 static void Main( string [] args) 6 { 7 Computer computer = new Computer(); 8 SuperStorageAdapter superStorageAdapter = new SuperStorageAdapter(); 9 SuperStorage superStorage = new SuperStorage(); 10 superStorageAdapter.SuperStorage = superStorage; 11 12 Console.WriteLine( "Now,I am testing the new super storage with adapter:" ); 13 computer.UsbDrive = superStorageAdapter; 14 computer.ReadData(); 15 computer.WriteData(); 16 Console.ReadLine(); 17 } 18 } 19 }
运行后会得到如下结果: OK!虽然遇到了一些困难,不过在设计模式的帮助下,我们还是在没有修改Computer任何代码的情况下实现了新设备的运行。 好了,理论在第一篇讲得足够多了,所以这里我就不多讲了。希望各位朋友结合第一篇的理论和这个例子,仔细思考面向接口的问题。当然,不要忘了结合现实。 下一篇,我将解析经典设计模式中的面向接口编程思想和.NET平台分层架构中接口的运用。 面向接口编程详解(三)——模式研究 通过前面两篇,我想各位朋友对“面向接口编程”的思想有了一定认识,并通过第二篇的例子,获得了 一定的直观印象。但是,第二篇中的例子旨在展示面向接口编程的实现方法,比较简单,不能体现出面向接口编程的优势和这种思想的内涵。那么,这一篇作为本系 列的终结篇,将通过分析几个比较有深度的模式或架构,解析隐藏其背后的面向接口思想。这篇我将要分析的分别是MVC模式和.NET平台的分层架构。 这篇的内容可能会比较抽象,望谅解。 1.从MVC开始 MVC简介: 本文不打算详细解释MVC架构,而是把重点放在其中的面向接口思想上。所以在这里,只对MVC做一个简略的介绍。 MVC是一种用于表示层设计的复合设计模式。M、V、C分别表示模型(Model)、View(视图)、Controller(控制器)。它们的职责如下: 模型:用于存储应用中的数据及运行逻辑,是应用的实体。 视图:负责可视部分,用于与用户交互及呈现数据。视图只负责显示,不负责将用户的操作行为解释给模型。 控制器:负责将用户的行为解释给模型。根据指定的策略和用户的操作,调用模型的逻辑。 关于三者的关系,我画了一张图,大家请看: 它们之间的交互有以下几种: 1.当用户在视图上做任何需要调用模型的操作时,它的请求将被控制器截获。 2.控制器按照自身指定的策略,将用户行为翻译成模型操作,调用模型相应逻辑实现。 3.控制器可能会在接到视图操作时,指定视图做某些改变。 4.当模型的状态发生改变时,将通过某种方式通知视图。 5.视图可以从模型获取状态,从而改变自己的显示。 MVC介绍完了,那么可能会有人问,我们的主题呢?面向接口思想呢?其实,MVC中处处都存在面向接口的影子。下面,我对其中几个侧面进行解释。 1.首先我们可以看到,视图和模型是有直接交互的,也就是上面的4、5两点。但是有一点可能会让你吃惊:它们两个谁也不“认识”谁,即它们相互并不知道对方是做什么的、有什么属性、有什么方法,但是它们能交互。这是怎么做到的呢?因为它们个各知道对方实现了某一个接口。 此乃面向接口思想一大作用:使相互不认识的类进行交互。这样做是很有好处的,首先它们之间的耦合度大大降低,其次双方都可以进行替换,只要实现了相同的接口,就没有问题。 打个不太恰当的比喻。我们都知道120这个电话号码,是急救电话。其实120就是个接口,因为当你拨打这个电话时,你不知道那边是哪所医院,甚至不知道那 边是不是医院,你只知道电话那头的地方可以救人,也可以说实现了IHelp接口。这样,你通过一个号码可以说同全部的救人机构联系起来了,当有紧急事件, 接线控制那边会将你的请求接到最近可用的机构,你就可以最快的得到帮助。 现在我们假设没有使用面向接口思想,来看看会发生什么恐怖的事情:首先,我家的120号码是绑定在本市第一人民医院的,即当我拨打120时,只能拨通第一 人民医院。如果有一天我食物中毒了,急忙拨通了120,但是电话那边告诉我他们医院的救护车都派出去了,我问那怎么接通别家医院的电话,那边的MM很温柔 的告诉我,让我打电话给网通公司,然后重新为我布线。于是我吐血而亡…… 言归正传。这里,我要引入一个设计模式,叫观察着(Observer)模式。这个模式大约是这样的:整个模式中有两种实体:观察者和被观察者,它们分别实 现一个接口,这里我们姑且叫做IObserver与IObserverSubject。IObserver只有一个方法,例如叫Update,当被观察者 状态改变时,调用这个方法,用来通知观察者。IObserverSubject接口有两个方法,都是供观察者调用。一个用来将观察者注册为此被观察者的观 察对象,另一个用于将观察者移除。 一般情况下,一个被观察者对应多个观察者。 在MVC中,视图是观察者,模型是被观察者,当模型状态改变时,调用所有观察者的Update方法,通知视图模型有变,视图在Update方法里写下响应 代码,完成操作。通过这个方法,视图和模型就可以在仅依赖接口的情形下进行交互,而不必强耦合,而且在模型不变的情况下,视图可以随意替换。(只要实现了 IObserver) 2. 在MVC中另一个使用接口的地方就是控制器,这里我要首先引入一个设计模式:策略模式(Strategy)。在MVC中,控制器就使用了这个模式。 刚才我说过,视图负责与用户交互,但是,它只负责界面显示部分,至于当用户做了某个操作(如单击某个按钮)后系统应该怎么反应,视图并不负责,它只是将这 个动作交给控制器,控制器根据内置的策略,将用户操作翻译成模型的逻辑。这就是说,同一个视图、同一种操作,模型可以做出不同的反应,这取决与控制器的内 置策略。所以,我们的系统中可以有很多控制器,它们有不同的策略,当视图希望改变策略时,它可以更换控制器。怎么实现呢?这就需要视图不能和具体控制器耦 合,而是要仅依赖一个控制器接口(如IController),并聚合一个IController的实例。当希望更改策略时,可以在系统运行时动态更换 Controller,这就是策略模式的实现。 关于MVC的接口思想就先介绍到这里。其实MVC中还有很多地方用到面向接口,由于本文不是专门 介绍MVC或设计模式的,所以对用到的模式没有做详解,而是把重点放在其中的面向接口思想上。如果没有设计模式的基础,读上文可能会有些困难,希望各位见 谅!我打算在以后专门写文章来解析MVC。 2..NET平台下分层架构的面向接口思想 我们知道,在做大一点的系统应用时(特别是B/S架构),比较好的方法是分层架构。所谓分层架构,是指将系统从职责上分成若干层,每层各司其职,上层依赖下层完成操作。 在.NET平台上,比较经典的分层架构是三层架构,从下到上依次是:数据访问层、业务逻辑层、表示层。各层职责如下: 数据访问层:负责与数据源交互,完成数据访问等一系列操作。 业务逻辑层:完成与系统业务有关的逻辑操作。 表示层:负责与用户交互、呈现数据等一切与系统表示有关的操作。 刚才我们说过,分层架构下是向下依赖的(不考虑依赖倒置),也就是业务逻辑层要调用数据访问层完成与数据源有关的操作,而表示层调用业务逻辑层完成业务逻辑工作。但是,表示层对数据访问层是没有依赖的。 在这个架构中,每一层都不是一个类,而是一个类族,例如,在一个CMS系统中,数据访问层可能会有一系列的类,分别负责用户、文章、评论等业务实体的数据 访问操作,而业务逻辑层也一样。如果我们直接依赖,即业务逻辑层实例化数据访问层的类,表示层再实例化业务逻辑层的类,会造成强耦合。如果我想把数据库从 SQLServer换成MySQL,则要改变整个业务逻辑层代码,这是个不好的设计。(还记得“开放-关闭”原则吗)所以,一般的做法是,为数据访问层和 业务逻辑层分别定义一族接口,业务逻辑层不依赖具体的数据访问层,而是仅依赖数据访问层的接口族,表示层也一样,依赖业务逻辑层的接口族。如此一来,当要 更换数据库时,我们就不必改写整个业务逻辑层,因为业务逻辑层里根本没有任何数据访问层中的具体类,而全是通过接口实现的。在.NET中,只要配合配置文 件和反射机制,再运用Abstract Factory设计模式,就可以实现“依赖注入”,即在不改动代码的情况下根据配置选择相应的层次组件。这样,我们就可以为不通数据库分别实现数据访问 层,也可以编写ORM的数据访问层,甚至是基于XML的,只要实现了数据访问层接口族,就可以和业务逻辑层无缝连接,从而极大提高了软件的灵活性和可维护 性。当然要更改业务逻辑层也是一样。 如果说,前面的例子都是从微观视角讨论接口,那么,这个例子则从宏观视角展现了面向接口编程的内涵和优势。很抱歉在这里不能对这个架构深入讲解,有兴趣的 朋友可以参考微软的官方示例.NET PetShop4。(但是请注意,这个示例中业务逻辑层没有定义接口族,而是强耦合于表示层中,这可能是因为考虑到在这个系统中业务逻辑没有更改的可能。 另外由于是个示例,不是真正的B2C系统,所以业务逻辑层很简单。) 好了,本系列文章就到这里。希望各位朋友通过这三篇文章,能对“面向接口编程”有一定的了解。当 然,我只是起到一个抛砖引玉的作用,其真正的内涵和精髓,还需要各位从实践中慢慢认识。还有,就是面向接口思想不是孤立的,它和设计模式等内容都是面向对 象大系中的精华,而且是相互渗透、相互联系的。其实,很多设计模式就是面向接口思想的体现。我们应该把这些放在一起学习,从而真正提供自己的面向对象思考 能力和实战能力。 |