前言
上一篇,我们详细介绍了装饰模式,回顾一下装饰模式的主要意图:动态地给一个对象添加职责,就增加功能来说,装饰模式比生成子类更为灵活,方便。与此同时,因为装饰器与组件对象拥有相同的接口,逻辑上与组件对象归属为同一类型范畴,这样用户就可以通过装饰器的组合,透明地给组件对象添加各式各样的额外功能。每个装饰器所实现的功能粒度越小,其复用性相对也就越高,因为可以通过这些小粒度的装饰器来为实际的组件对象组合生成复杂的外在功能,需要注意的是,装饰器模式改变的是只是组件对象的“外壳”,并不改变其“内核”,我们可以通过策略模式来完成对组件内核功能的修改,详见装饰模式一文。接下来,我们将要面临一种全新的应用需求,需要我们运用另外一种结构型模式来应对。
动机
在实际的软件系统开发中,用户程序经常需要与复杂系统的各个子系统打交道,专业点地说叫耦合,当子系统发生改变时,用户程序也必须相应发生变化,正所谓“牵一发而动全身”。此外,由于子系统内部接口的不统一性,即便是用户直接和子系统联系,也会因为接口的不一致性,调用难度加大。我们能否提供一种封装机制,简化用户程序与子系统之间交互的接口?将复杂系统的各个子系统与用户程序解耦?外观模式给了我们一个比较好的思路。接下来,就让我们充分地来认识一下外观模式的庐山真面目吧。
意图
为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这个子系统更加容易使用。
将一个系统划分为若干个子系统有利于降低系统的复杂性,但是子系统过多,或者接口不够统一,也难免会造成用户程序与之交互的易用性。外观模式就是通过引入这么一个外观类,在这个类里面定义客户端想要的简单的方法,然后在这些方法的实现里面,由外观类再去分别调用系统内部中的各个子系统接口来实现功能,从而让客户端变得简单,只需要与外观类所定义好的简单接口打交道即可,也不用操心子系统间的互操作。这便是外观模式的巧妙之处呢。
结构图
- 外观(Facade)角色:定义了子系统的多个模块对外的高层接口,通过需要调用内部多个模式,从而能把客户的请求代理给适当的子系统对象。
- 模式(子系统)角色:接受外观对象的委派,真正实现功能,各个模块之间可能有交互,需要注意的是,外观对象知道各个模块,但是各个模式不应该知道外观对象。
代码示例
1: public class Facade{
2: public void Test(){
3: AModule aModule=new AModule();
4: aModule.TestA();
5:
6: BModule bModule=new BModule();
7: bModule.TestB();
8:
9: CModule cModule=new CModule();
10: cModule.TestC();
11:
12: }
13: }
14:
15: public class AModule{
16: public void TestA(){
17:
18: }
19: }
20:
21: public class BModule{
22: public void TestB(){
23:
24: }
25: }
26:
27: public class CModule{
28: public void TestC(){
29:
30: }
31: }
32:
33: public class Client{
34: public static void main(String[] args){
35: Facade facade=new Facade();
36: facade.Test();
37: }
38: }
示例代码很简单,我们完全可以将其写得复杂、规范些,比如为每一个模块抽象出一接口,让其实现相应接口,更加符合面向接口编程原则,再者就是在创建相应的模块对象时,我们也完全可以通过创建型模式来完成实例的创建工作。但是这里,我们主要是为了展示外观模式的基本原理和实现,就不再考虑应用场景的种种具体实现呢。从示例代码中,我们可以看到,Facade类提供了一个高层接口,供客户端程序调用,而接口里的具体实现则通过调用相关的模块(子系统)来协作完成,这样客户端只需要调用由Facade类提供的简单交互接口来实现复杂的业务逻辑功能,而不用纠结于各个模块间的交互性,极大地方便了客户端程序的使用,与此同时,Facade类也向客户端屏蔽了底层子模块复杂的交互过程。从这里,我们也能体会到外观模式的主要是目的不是给子系统添加新的功能接口,而是为了让外部减少与子系统的交互,松散耦合,从而让客户端能够更简单地使用子系统。示例代码比较简单,接下来,让我们来看看比较真实贴切的场景例子吧。
现实场景
在现实的生活场景中,我们经常碰到可以运用外观模式进行解释的场景,直观易懂。在我们日常进出购买日用吕的超市里,通常面积都比较大,各种电器开关也比较多,比如有日光灯开关、空调开关和各种用于宣传广告的电视开关等等,在没有采取智能改进的情况下,每天晚上九点半超市打烊的时候,超市工作人员将不得不依次逐个关闭所有电器开关,这样的工作繁杂,重复量大,更糟糕的是,在第二天的早晨,他们又不得不依次逐个打开超市里所有的电器开关,对于工作人员来说,这无疑是一个让人头痛的任务。如果每天都要进行两次这样重复、无技术含量的工作,这不仅是对工作人员体力的耗费,更是对超市人力资源的浪费。但是,在现实生活中,办法总是比问题多,现在的超市已经不再可能出现我们刚刚描述的场景状况呢,设计得都是比较智能、方便些呢。关键之处就是给超市设置一个电器总开关,由于来负责超市内所有电器开关的开闭操作,这样工作人员现在就只需负责这个总开关的开闭工作呢,彻底地从先前的繁杂工作中解脱出来。以前需要工作人员依次地将所有电器开关进行关闭操作,现在这个工作全交由总开关来完成呢,它会帮我们依次打开或者关闭超市内所有的电器开关。当然要实现这样一个智能的操作,必须在超市装饰之前,由专业的电路设计人员将相应的电路线路设计实施好,才能达到总控的目的。现在让我们用外观模式的观点来抽象上述场景:很明显,总开关就是外观模式中的核心对象——外观对象,而各个电器开关就是外观模式中的各个子系统,各自完成着自己负责的工作,也就是电器的开闭操作,而工作人员就是外观模式中的客户端,他们只需要与总开关(外观对象)打交道,来完成对所有电器开关的开闭操作。如此,通过总开关这个外观对象,工作人员将与复杂的各个电器开关(各个子系统)彻底解耦,达到一种完全松散耦合的关系。仔细想想,这是不是对外观模式较好的诠释生活场景呢?
接下来,我们将通过另一个具体的场景来更加详细地演绎外观模式。前几年,炒股很火,如果你也是其中一员的话,想必体会更加深刻。通常股民手中都是会持有多只股票,并时刻关注着这些股票的涨停情况,以便准备随时买入和抛出,但即使如此耗时耗神,股民通常也是如坐针毡,因为毕竟绝大多数人不是专业的行家,对证券知识了解的不够,更谈不上精通,因此出现亏损的情况比比皆是。如果有专门的专业人员来管理手中持有的股票,由其负责对股票的买入与抛出,显然盈利的机率会增加很多。基于这样的想法,很多普通股民现在也开始玩投资基金呢。基金将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于股票、债券、外汇等领域,而基金投资的收益归持有投资者所有的,管理机构收取一定的托管管理费用,这样由于基金会买几十只好的股票,不会因为某个股票的大跌而影响收益,尽管每一个的钱不多,但大家放在一直,反而容易达到好的投资效果呢。此外,由于专业的基金经理人相对专业,能够对较好地掌握股票的买入和卖出的时机。现在让我们考虑下上述的场景,一开始,一个普通的股民需要负责自己手中持有的所有股票的买入与卖出的操作,而现在股民需要做的只是将这些操作交由专业的基金经理人去完成就可以,换句话来说就是股民只需要购买基金,完全就可以不用不理会呢,因为负责股票的具体买卖都由基金公司来完成。下面让我们先来看看上述两种方法的抽象结构图:
上面表示的是股民自己管理自身持有的所有股票的买入与卖出的情况。可以看出,此时股民与所有的股票都有关联,因为他必须负责每一个股票的操作,用面向对象的专业术语来说就是耦合性过高。
上图表示的是股民通过基金来负责股票的管理工作。可以看出,现在股票的所有买入与卖出操作都交由基金统一管理,股民只需要与基金来打交道即可,将其与和股票间解耦,方便、灵活。
下面,我们通过代码来演绎下第二幅示意图的利好之处:
1: public abstract class Stock{
2: public abstract void Buy();
3: public abstract void Sold();
4: }
5:
6: public class Stock1 extends Stock{
7: public void Buy(){
8: System.out.println("Stock1 is buying!");
9: }
10:
11: public void Sold(){
12: System.out.println("Stock1 is solding!");
13: }
14: }
15:
16: public class Stock2 extends Stock{
17: public void Buy(){
18: System.out.println("Stock2 is buying!");
19: }
20:
21: public void Sold(){
22: System.out.println("Stock2 is solding!");
23: }
24: }
25:
26: public class Stock3 extends Stock{
27: public void Buy(){
28: System.out.println("Stock3 is buying!");
29: }
30:
31: public void Sold(){
32: System.out.println("Stock3 is solding!");
33: }
34: }
35:
36: public class NationalDebt1 extends Stock{
37: public void Buy(){
38: System.out.println("NationalDebt1 is buying!");
39: }
40:
41: public void Sold(){
42: System.out.println("NationalDebt1 is solding!");
43: }
44: }
45:
46: public class Realty1 extends Stock{
47: public void Buy(){
48: System.out.println("Realty1 is buying!");
49: }
50:
51: public void Sold(){
52: System.out.println("Realty1 is solding!");
53: }
54: }
55:
56: public class Fund{
57: //基金类必须知道所有的股票类型
58: Stock1 stock1;
59: Stock2 stock2;
60: Stock3 stock3;
61: NationalDebt1 nd1;
62: Realty1 rt1;
63:
64: public Fund(){
65: stock1=new Stock1();
66: stock2=new Stock2();
67: stock3=new Stock3();
68: nd1=new NationalDebt1();
69: rt1=new Realty1();
70: }
71:
72: //由基金统一控制所有股票的买入
73: public void buyFund(){
74: stock1.Buy();
75: stock2.Buy();
76: stock3.Buy();
77: nd1.Buy();
78: rt1.Buy();
79: }
80: //由基金统一控制所有股票的卖出
81: public void soldFund(){
82: stock1.Sold();
83: stock2.Sold();
84: stock3.Sold();
85: nd1.Sold();
86: rt1.Sold();
87: }
88: }
89:
90: public class Client{
91: public static void main(String[] args){
92: Fund fund=new Fund();
93: //购买基金,也就是买入所有持有股票
94: fund.buyFund();
95: //卖出基金,也就是卖出所有持有股票
96: fund.soldFund();
97: }
98: }
从上述代码片段,我们可以看到现在股民只需要与基金对象打交道呢,持有股票的买入与卖出操作都交于专业的基金经理人打理,股民无需过问,一觉醒来,钱翻番:)。好呢,对外观模式的举例就说这么多,两个例子都是比较简单,相信大家一看就能懂,其实外观模式个人认为也是比较简单易懂的一种设计模式,相较其他较复杂的模式而言。说白了就是定义一高层接口,通过该接口使得客户端更加方便地使用复杂系统中各个子系统,也就是将用户与子系统进行了解耦操作,降低它们之间的关联性。
实现要点
- 降低客户——子系统间的耦合度。用抽象类实现Facade而它的具体子类对应于不同的子系统实现,这可以进一步降低客户与子系统的耦合度,虽然我们的示例代码中没有为Facade抽象出一层抽象类。这样,客户就可以通过抽象的Facade类接口与子系统进行通讯,而无需知道它使用的是子系统的哪一个实现。换句话来说就是不同的Facade实现类通过组合不同的子系统来完成不同实现过程的相同功能(因为接口一致),而用户并不需要知道这些。
- 外观对象必须知道所有子系统对象,这样才能根据需要对它们进行组合调用,完成不同的功能需求。这点很关键。
运用效果
- 松散耦合。外观模式松散了客户端与子系统的耦合关系,让子系统内部的模块可以更容易扩展和维护。
- 简单易用。外观模式让子系统更易用,客户商无需要了解子系统的内部细节,更不需要与各子系统模块进行交互,只需要与外观对象进行交互就可以呢,由外观模式为客户端提供一站式服务。
- 如果应用需要,外观模式并不限制客户端直接使用子系统类,因此可以在系统易用买入与和通用性间加以选择。
适用性
- 需要为一个复杂子系统提供一个简单接口时。子系统往往会因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类,这使得子系统更具可重用性,也更容易对子系统进行定制,但这也给那些不需要定制的子系统的用户带来一些使用上的不便。而Facade提供了一个简单的缺省视图,由其代替客户端与各子系统的交互过程,极大地降低客户端与子系统的耦合性。
- 客户程序与抽象类的实现部分之间存在着很大的依赖性。引入Facade将这个子系统与客户以及其他的,可以提高子系统的独立性和可移植性。
- 需要构建一个层次结构的子系统时。使用外观模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,可以让它们仅通过Facade进行通讯,从而简化它们之间的依赖关系,也就简化了客户端与子系统的依赖关系。
相关模式
- 外观模式与中介者模式:两者很相似,但是本质不一样。中介者模式主要用于封装多个对象之间相互的交互,多用在系统内部的多个模块之间;而外观模式封装的是单向的交互,是从客户端访问系统的调用,没有从系统中来访问客户端的调用。在中介者模式的实现里面,是需要实现具体的交互功能的,而外观模式一般只是组合调用或者转调内部实现的功能,并不实现这些功能。另外,中介者模式的主要目的是松散多个模块之间的耦合,将各模块间的耦合关系全放在中介者中去实现,而外观模式的主要目的是简化客户端的调用,依次调用所有依赖的子系统。
- 外观模式与单例模式:两者可以组合使用。将Facade类实现为单例,这样就无需创建多个相同的Facade对象呢。
- 外观模式与抽象工厂模式:外观类需要与系统内部的多个模块进行交互,而每一个模块都有自己的接口,所以在外观类的具体实现里,获取这些接口的工作可以将由创建型模式来完成。通过抽象工厂或者其他方法到获取相应的模块接口,至于各模式内的实现细节,Facade也无需知道。
总结
外观模式的本质是:封装交互,简化调用。外观模式封装了子系统外部与子系统内部多个模块的交互过程,从而简化了外部的调用,通过外观类对象,子系统为外部提供了一些高层接口,方便客户端对子系统的使用。在这里,说明一点是,外观模式很好地体现了“最少知识原则”,为什么呢?因为外观模式促使客户端只需要与外观类交互,而不需去关心子系统内部模块的变动情况,以及它们之间的交互。否则,客户端将不得不与子系统内部的多个模块进行交互,与它们产生强依赖关系,任意一个模块的变动都可能引进客户端的变动。好呢,对外观模式的讲解就到这吧,提醒大家一点的是,重点区分其与中介者模式的异同点。下篇文章,我们将重点介绍享元模式,敬请期待!
参考资料:
- 程杰著《大话设计模式》一书
- 陈臣等著《研磨设计模式》一书
- GOF著《设计模式》一书
- Terrylee .Net设计模式系列文章
- 吕震宇老师 设计模式系列文章