结构类模式对比

Published on 2016 - 09 - 06

结构类模式包括适配器模式、桥梁模式、组合模式、装饰模式、门面模式、享元模式和代理模式。为什么叫结构类模式呢?因为它们都是通过组合类或对象产生更大结构以适应更高层次的逻辑需求。我们来分析以下几个模式的相似点和不同点。

代理模式VS装饰模式

对于两个模式,首先要说的是,装饰模式就是代理模式的一个特殊应用,两者的共同点是都具有相同的接口,不同点则是代理模式着重对代理过程的控制,而装饰模式则是对类的功能进行加强或减弱,它着重类的功能变化,我们举例来说明它们的区别。

代理模式

一个著名的短跑运动员有自己的代理人。如果你很仰慕他,你找运动员说“你跑个我看看”,运动员肯定不搭理你,不过你找到他的代理人就不一样了,你可能和代理人比较熟,可以称兄道弟,这个忙代理人还是可以帮的,于是代理人同意让你欣赏运动员的练习赛,这对你来说已经是莫大的荣耀了。我们来看类图,如图1所示。

这是一个套用代理模式的简单应用,非常简单!一个对象,然后再是自己的代理。我们先来看一下代码,先看抽象主题类,如代码清单1所示。

public interface IRunner {
     //运动员的主要工作就是跑步
     public void run();
}

一个具体的短跑运动员跑步是很潇洒的,如代码清单2所示。

public class Runner implements IRunner {
     public void run() {
             System.out.println("运动员跑步:动作很潇洒");
     }
}

看看现在的明星运动员,一般都有自己的代理人,要么是专职的,要么就是自己的教练兼职,那我们来看看代理人的职责,如代码清单3所示。

public class RunnerAgent implements IRunner {
     private IRunner runner;
     public RunnerAgent(IRunner _runner){
             this.runner = _runner;
     }
     //代理人是不会跑的
     public void run() {
             Random rand = new Random();
             if(rand.nextBoolean()){
                     System.out.println("代理人同意安排运动员跑步");
                     runner.run();
             }else{
                     System.out.println("代理人心情不好,不安排运动员跑步");
          }
     }
}

我们只是定义了一个代理人,并没有明确定义是哪一个运动员的代理,需要在运行时指定被代理者,而且我们还在代理人的run方法中做了判断,想让被代理人跑步就跑步,不乐意就拒绝,对于主题类的行为是否可以发生,代理类有绝对的控制权。我们编写一个场景类来模拟这种情况,如代码清单4所示。

public class Client {
     public static void main(String[] args) {
             //定义一个短跑运动员
             IRunner liu = new Runner();
             //定义liu的代理人
             IRunner agent = new RunnerAgent(liu);
             //要求运动员跑步
             System.out.println("====客人找到运动员的代理要求其去跑步===");
             agent.run();
     }
}

由于我们使用了随机数产生模拟结果,因此运行结果有两种可能情况,第一种情况如下所示:

====客人找到运动员的代理要求其去跑步===
代理人同意安排运动员跑步
运动员跑步:动作很潇洒
运行结果的第二种情况如下所示:
====客人找到运动员的代理要求其去跑步===
代理人心情不好,不安排运动员跑步

不管是哪种情况,我们都证实了代理的一个功能:在不改变接口的前提下,对过程进行控制。在我们例子中,运动员要不要跑步是由代理人决定的,代理人说跑步就跑步,说不跑就不跑,它有绝对判断权。

装饰模式

如果使用装饰模式,我们该怎么实现这个过程呢?装饰模式是对类功能的加强,怎么加强呢?增强跑步速度!在屁股后面安装一个喷气动力装置,类似火箭的喷气装置,那速度变得很快,《蜘蛛侠》中的那个反面角色不就是这样的吗?好,我们来看类图,如图2所示。

很惊讶?这个代理模式完全一样的类图?是的,完全一样!不过其实现的意图却不同,我们先来看代码,IRunner和Runner与代理模式相同,详见代码清单1和代码清单2所示,在此不再赘述。我们来看装饰类RunnerWithJet,如代码清单5所示。

public class RunnerWithJet implements IRunner {
     private IRunner runner;
     public RunnerWithJet(IRunner _runner){
             this.runner = _runner;
     }
     public void run() {
             System.out.println("加快运动员的速度:为运动员增加喷气装置");
             runner.run();
     }
}

这和代理模式中的代理类也是非常相似的,只是装饰类对类的行为没有决定权,只有增强作用,也就是说它不决定被代理的方法是否执行,它只是再次增加被代理的功能。我们来看场景类,如代码清单6所示。

public class Client {
     public static void main(String[] args) {
             //定义运动员
             IRunner liu = new Runner();
             //对其功能加强
             liu = new RunnerWithJet(liu);
             //看看它的跑步情况如何
             System.out.println("===增强后的运动员的功能===");
             liu.run();
     }
}

运行结果如下所示:

===增强后的运动员的功能===
加快运动员的速度:为运动员增加喷气装置
运动员跑步:动作很潇洒

注意思考一下我们的程序,我们通过增加了一个装饰类,就完成了对原有类的功能增加,由一个普通的短跑运动员变成了带有喷气装置的超人运动员,其速度岂是普通人能相比的?!

最佳实践

通过例子,我们可以看出代理模式和装饰模式有非常相似的地方,甚至代码实现都非常相似,特别是装饰模式中省略抽象装饰角色后,两者代码基本上相同,但是还是有细微的差别。

代理模式是把当前的行为或功能委托给其他对象执行,代理类负责接口限定:是否可以调用真实角色,以及是否对发送到真实角色的消息进行变形处理,它不对被主题角色(也就是被代理类)的功能做任何处理,保证原汁原味的调用。代理模式使用到极致开发就是AOP,这是各位采用Spring架构开发必然要使用到的技术,它就是使用了代理和反射的技术。

装饰模式是在要保证接口不变的情况下加强类的功能,它保证的是被修饰的对象功能比原始对象丰富(当然,也可以减弱),但不做准入条件判断和准入参数过滤,如是否可以执行类的功能,过滤输入参数是否合规等,这不是装饰模式关心的。

代理模式在Java的开发中俯拾皆是,是大家非常熟悉的模式,应用非常广泛,而装饰模式是一个比较拘谨的模式,在实际应用中接触比较少,但是也有不少框架项目使用了装饰模式,例如在JDK的java.io.*包中就大量使用装饰模式,类似如下的代码:

OutputStream out = new DataOutputStream(new FileOutputStream("test.txt"))

这是装饰模式的一个典型应用,使用DataOutputStream封装了一个FileOutputStream,以方便进行输出流处理。

装饰模式VS适配器模式

装饰模式和适配器模式在通用类图上没有太多的相似点,差别比较大,但是它们的功能有相似的地方:都是包装作用,都是通过委托方式实现其功能。不同点是:装饰模式包装的是自己的兄弟类,隶属于同一个家族(相同接口或父类),适配器模式则修饰非血缘关系类,把一个非本家族的对象伪装成本家族的对象,注意是伪装,因此它的本质还是非相同接口的对象。

大家都应该听过丑小鸭的故事吧,我们今天就用这两种模式分别讲述丑小鸭的故事。话说鸭妈妈有5个孩子,其中4个孩子都是黄白相间的颜色,而最小的那只也就是叫做丑小鸭的那只,是纯白色的,与兄弟姐妹不相同,在遭受了诸多的嘲讽和讥笑后,最终丑小鸭变成了一只美丽的天鹅。那我们如何用两种不同模式来描述这一故事呢?

用装饰模式描述丑小鸭

用装饰模式来描述丑小鸭,首先就要肯定丑小鸭是一只天鹅,只是因为她小或者是鸭妈妈的无知才没有被认出是天鹅,经过一段时间后,它逐步变成一个漂亮、自信、优美的白天鹅。根据分析我们可以这样设计,先设计一个丑小鸭,然后根据时间先后来进行不同的美化处理,怎么美化呢?先长出漂亮的羽毛,然后逐步展现出异于鸭子的不同行为,如飞行,最终在具备了所有的行为后,它就成为一只纯粹的白天鹅了,我们来看类图,如图3所示。

类图比较简单,非常标准的装饰模式。我们按照故事的情节发展一步一步地实现程序。初期的时候,丑小鸭表现得很另类,叫声不同,外形不同,致使周围的亲戚、朋友都对她鄙视,那我们来建立这个过程,由于丑小鸭的本质就是一个天鹅,我们就先生成一个天鹅的接口,如代码清单7所示。

public interface Swan {
     //天鹅会飞
     public void fly();
     //天鹅会叫
     public void cry();
     //天鹅都有漂亮的外表
     public void desAppaearance();
}

我们定义了天鹅的行为,都会飞行、会叫,并且可以描述她们漂亮的外表。丑小鸭是一只白天鹅,是"is-a"的关系,也就是需要实现这个接口了,其实现如代码清单8所示。

public class UglyDuckling implements Swan {
     //丑小鸭的叫声
     public void cry() {
             System.out.println("叫声是克噜——克噜——克噜");
     }
     //丑小鸭的外形
     public void desAppaearance() {
             System.out.println("外形是脏兮兮的白色,毛茸茸的大脑袋");
     }
     //丑小鸭还比较小,不能飞
     public void fly() {
             System.out.println("不能飞行");
     }
}

丑小鸭具备了天鹅的所有行为和属性,因为她本来就是一只白天鹅,只是因为她太小了还不能飞行,也不能照顾自己,所以丑丑的,在经过长时间的流浪生活后,丑小鸭长大了。终于有一天,她发现自己竟然变成了一只美丽的白天鹅,有着漂亮、洁白的羽毛,而且还可以飞行,这完全是一种升华行为。我们来看看她的行为(飞行)和属性(外形)是如何加强的,先看抽象的装饰类,如代码清单9所示。

public class Decorator implements Swan {
     private Swan swan;
     //修饰的是谁
     public Decorator(Swan _swan){
             this.swan =_swan;
     }
     public void cry() {
             swan.cry();
     }
     public void desAppaearance() {
             swan.desAppaearance();
     }
     public void fly() {
             swan.fly();
     }
}

这是一个非常简单的代理模式。我们再来看丑小鸭是如何开始变得美丽的,变化是由外及里的,有了漂亮的外表才有内心的实质变化,如代码清单10所示。

public class BeautifyAppearance extends Decorator {
     //要美化谁
     public BeautifyAppearance(Swan _swan){
             super(_swan);
     }
     //外表美化处理
     @Override
     public void desAppaearance(){
             System.out.println("外表是纯白色的,非常惹人喜爱!");
     }
}

丑小鸭最后发现自己还能飞行,这是一个行为突破,是对原有行为“不会飞行”的一种强化,如代码清单11所示。

public class StrongBehavior extends Decorator {
     //强化谁
     public StrongBehavior(Swan _swan){
             super(_swan);
     }
     //会飞行了
     public void fly(){
             System.out.println("会飞行了!");
     }
}

所有的故事元素我们都具备了,就等有人来讲故事了,场景类如代码清单12所示。

public class Client {
     public static void main(String[] args) {
             //很久很久以前,这里有一个丑陋的小鸭子
             System.out.println("===很久很久以前,这里有一只丑陋的小鸭子===");
             Swan duckling = new UglyDuckling();
             //展示一下小鸭子
             duckling.desAppaearance();  //小鸭子的外形
             duckling.cry();  //小鸭子的叫声
             duckling.fly();  //小鸭子的行为     
             System.out.println("\n===小鸭子终于发现自己是一只天鹅====");
             //首先外形变化了
             duckling = new BeautifyAppearance(duckling);
             //其次行为也发生了改变
             duckling = new StrongBehavior(duckling); 
             //虽然还是叫丑小鸭,但是已经发生了很大变化
             duckling.desAppaearance();  //小鸭子的新外形
             duckling.cry();  //小鸭子的新叫声
            duckling.fly();  //小鸭子的新行为
     }
}

运行结果如下所示:

===很久很久以前,这里有一只丑陋的小鸭子===
外形是脏兮兮的白色,毛茸茸的大脑袋
叫声是克噜——克噜——克噜
不能飞行
===小鸭子终于发现自己是一只天鹅====
外表是纯白色的,非常惹人喜爱!
叫声是克噜——克噜——克噜
会飞行了!

使用装饰模式描述丑小鸭蜕变的过程是如此简单,它关注了对象功能的强化,是对原始对象的行为和属性的修正和加强,把原本被人歧视、冷落的丑小鸭通过两次强化处理最终转变为受人喜爱、羡慕的白天鹅。

用适配器模式实现丑小鸭

采用适配器模式实现丑小鸭变成白天鹅的过程要从鸭妈妈的角度来分析,鸭妈妈有5个孩子,它认为这5个孩子都是她的后代,都是鸭类,但是实际上是有一只(也就是丑小鸭)不是真正的鸭类,她是一只小白天鹅,就像《木兰辞》中说的“雄兔脚扑朔,雌兔眼迷离。双兔傍地走,安能辨我是雄雌?”同样,因为太小,差别太细微,很难分辨,导致鸭妈妈认为她是一只鸭子,从鸭子的审美观来看,丑小鸭是丑陋的。通过分析,我们要做的就是要设计两个对象:鸭和天鹅,然后鸭妈妈把一只天鹅看成了小鸭子,最终时间到来的时候丑小鸭变成了白天鹅。我们来看类图,如图4所示。

类图非常简单,我们定义了两个接口:鸭类接口和天鹅类接口,然后建立了一个适配器UglyDuckling,把一只白天鹅封装成了小鸭子。我们来看代码,先看鸭类接口,如代码清单13所示。

public interface Duck {
     //会叫
     public void cry();
     //鸭子的外形
     public void desAppearance();
     //描述鸭子的其他行为
     public void desBehavior();
}

鸭类有3个行为,一个是鸭会叫,一个是外形描述,还有一个是综合性的其他行为描述,例如会游泳等。我们来看鸭妈妈的4个正宗孩子,如代码清单14所示。

public class Duckling implements Duck {
     public void cry() {
             System.out.println("叫声是嘎——嘎——嘎");
     }
     public void desAppearance() {
             System.out.println("外形是黄白相间,嘴长");
     }
     //鸭子的其他行为,如游泳
     public void desBehavior(){
             System.out.println("会游泳");
     }
}

4只正宗的小鸭子形象已经清晰地定义出来了。鸭妈妈还有一个孩子,就是另类的丑小鸭,她实际是一只白天鹅。我们先定义出白天鹅,如代码清单15所示。

public class WhiteSwan implements Swan {
     //白天鹅的叫声
     public void cry() {
             System.out.println("叫声是克噜——克噜——克噜");
     }
     //白天鹅的外形
     public void desAppaearance() {
             System.out.println("外形是纯白色,惹人喜爱");
     }
     //天鹅是能够飞行的
     public void fly() {
             System.out.println("能够飞行");
     }
}

但是,鸭妈妈却不认为自己这个另类的孩子是白天鹅,它从自己的观点出发,认为她很丑陋,有碍自己的脸面,于是驱赶她——鸭妈妈把这只小天鹅误认为一只鸭。我们来看实现,如代码清单16所示。

public class UglyDuckling extends WhiteSwan implements Duck {
     //丑小鸭的叫声
     public void cry() {
             super.cry();
     }
     //丑小鸭的外形
     public void desAppearance() {
             super.desAppaearance();
     }
    //丑小鸭的其他行为
     public void desBehavior(){
             //丑小鸭不仅会游泳
             System.out.println("会游泳");
             //还会飞行
             super.fly();
     }
}

天鹅被看成了鸭子,有点暴殄天物的感觉。我们再来创建一个场景类来描述这一场景,如代码清单17所示。

public class Client {
     public static void main(String[] args) {
             //鸭妈妈有5个孩子,其中4个都是一个模样
             System.out.println("===妈妈有五个孩子,其中四个模样是这样的:===");
             Duck duck = new Duckling();
             duck.cry();  //小鸭子的叫声
             duck.desAppearance(); //小鸭子的外形
             duck.desBehavior(); //小鸭子的其他行为
             System.out.println("\n===一只独特的小鸭子,模样是这样的:===");
             Duck uglyDuckling = new UglyDuckling();
             uglyDuckling.cry(); //丑小鸭的叫声
             uglyDuckling.desAppearance(); //丑小鸭的外形
             uglyDuckling.desBehavior(); //丑小鸭的其他行为
     }
}

运行结果如下所示:

===妈妈有5个孩子,其中4个模样是这样的:===
叫声是嘎——嘎——嘎
外形是黄白相间,嘴长
会游泳
===一只独特的小鸭子,模样是这样的:===
叫声是克噜——克噜——克噜
外形是纯白色,惹人喜爱
会游泳
能够飞行

可怜的小天鹅被认为是一只丑陋的小鸭子,造物弄人呀!采用适配器模式讲述丑小鸭的故事,我们首先观察到的是鸭与天鹅的不同点,建立了不同的接口以实现不同的物种,然后在需要的时候(根据故事情节)把一个物种伪装成另外一个物种,实现不同物种的相同处理过程,这就是适配器模式的设计意图。

最佳实践

我们用两个模式实现了丑小鸭的美丽蜕变。我们发现:这两个模式有较多的不同点。

  • 意图不同

装饰模式的意图是加强对象的功能,例子中就是把一个怯弱的小天鹅强化成了一个美丽、自信的白天鹅,它不改变类的行为和属性,只是增加(当然了,减弱类的功能也是可能存在的)功能,使美丽更加美丽,强壮更加强壮,安全更加安全;而适配器模式关注的则是转化,它的主要意图是两个不同对象之间的转化,它可以把一个天鹅转化为一个小鸭子看待,也可以把一只小鸭子看成是一只天鹅(那估计要在小鸭子的背上装个螺旋桨了),它关注转换。

  • 施与对象不同

装饰模式装饰的对象必须是自己的同宗,也就是相同的接口或父类,只要在具有相同的属性和行为的情况下,才能比较行为是增加还是减弱;适配器模式则必须是两个不同的对象,因为它着重于转换,只有两个不同的对象才有转换的必要,如果是相同对象还转换什么?!

  • 场景不同

装饰模式在任何时候都可以使用,只要是想增强类的功能,而适配器模式则是一个补救模式,一般出现在系统成熟或已经构建完毕的项目中,作为一个紧急处理手段采用。

  • 扩展性不同

装饰模式很容易扩展!今天不用这个修饰,好,去掉;明天想再使用,好,加上。这都没有问题。而且装饰类可以继续扩展下去;但是适配器模式就不同了,它在两个不同对象之间架起了一座沟通的桥梁,建立容易,去掉就比较困难了,需要从系统整体考虑是否能够撤销。

参考文档