23种设计模式分析(3):结构型模式

1.1.6 Adapter适配器模式


  GOF《设计模式》一书对Adapter模式是这样描述的:将一个类的接口转换成客户希望的另外一个接口。Adapter模式使原本由于接口不兼容而不能一起工作的类可以一起工作。这段话大致是说:我们需要一种方式,为一个功能正确但接口不合的对象创建一个新接口。
  我们经常碰到要将两个没有关系的类组合在一起使用。第一解决方案是:修改各自类,但是如果我们没有源代码,或者我们不愿意为了一个应用而修改各自的类。怎么办?使用Adapter,在这两种类之间创建一个混合接口(混血儿)。因此,Adapter模式经常被描述成第三方函数库或旧的程序库与现有系统接口/需求不一致时的救星,也正是由于Adapter模式的这种特性,有人也将Adapter模式比喻成变压器,变压器把一种电压变换成另一种电压。美国的生活用电电压是110V,而中国的电压是220V。如果要在中国使用美国电器,就必须有一个能把220V电压转换成110V电压的变压器,这个变压器就是一个Adapter。
  应用于类的Adapter模式:

图6-1 Adapter Pattern(类模式)结构图

  其中涉及的角色包括:
  目标(Target)角色:这是客户所期待的接口。
  源(Adaptee)角色:需要适配的类。
  适配器(Adapter)角色:负责包装源接口,以把源接口转换成目标接口。这一角色必须是类。
  应用于对象的Adapter模式:

图6-2 Adapter Pattern(对象模式)结构图

  其中涉及的角色包括:
  目标(Target)角色:这是客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
  源(Adaptee)角色:需要适配的类。
  适配器(Adapter)角色:通过在内部包装(Wrap)一个Adaptee对象,把源接口转换成目标接口。
  在Adapter模式的结构图中可以看到,类模式的Adapter采用继承的方式复用Adaptee的接口,而在对象模式的Adapter中我们则采用组合的方式实现Adaptee的复用。
  Class Adapter中adapter类通过继承adaptee类获得adaptee类的功能,而Object Adapter中adpter通过包容adaptee获得其功能。基于“当涉及到依存性时,应当始终优先选择组合/成员关系而不是继承”的设计原则(<Exceptional C++>),并且由于多继承在使用上的复杂性,及在部分情况下不可行等原因,Object Adapter的运用显得更加广泛。此外,当需要改变多个已有子类的接口时,如果使用Class Adapter模式,就要针对每一个子类做一个适配器,而这不太实际,此时比较适合使用Object Adapter。
  Object Adapter模式的基本思想在于:Delegate委托,即将需要完成的工作交给adaptee去完成,当然,为了兼容Client需求与adaptee接口之间的不一致,在委派的过程中,我们往往也需要作一些适当的调整。
  Object Adapter模式依赖于一个对象(适配器Adatper)包含另一个对象(被适配的对象Adaptee)。我们要让类A和类B一起工作,使之同时具有A和B的功能,可以写一个类A的子类C,在C中增加一个类B的成员,以达到适配的目的,这其实相当于组合的方式。现实中也会出现这样的情况:我们有类A,希望给A增加一些比较难实现的功能,首先想到继承A并自己实现此功能,其实更好的办法是看有没有现成的类恰好实现了此功能,可以通过委托方式把它适配进来,以达到代码复用。
  假设客户给我们如下需求:为都有“显示”(display)行为的点、线、正方形分别创建类。客户对象不必知道自己到底拥有点、线、还是正方形。它只需知道拥有这些形状中的一个。也就是说,我们要用一个更高层次的概念将这些具体形状都涵盖进去,这个高层概念可以称为:“可显示的形状”。因此,我们需要创建一个接口Shape:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. interface Shape {  
  2.   
  3.     public void display();  
  4. }  
  现在客户忽然间有让我们给这个系统增加一个画圆的功能。这个看起来很简单,只需定义一个Circle类来实现Shape接口,但是我们要给它编写display方法,这可不是件简单的事,假如此时我们正好发现一个XXCircle类,它有一个方法刚好可以完成这个功能,其代码如下:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class XXCircle {  
  2.   
  3.     public void displayCircle() {  
  4.         System.out.println("通过XXCircle.displayCircle()画圆");  
  5.     }  
  6. }  
  我们可以用Object Adapter模式把它适配进来:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. //组合方式:包含一个adaptee,即把需要完成的工作委托给它  
  2. class CircleObject implements Shape {  
  3.   
  4.     public XXCircle circle;  
  5.   
  6.     public CircleObject(XXCircle xxcircle) {  
  7.         circle = xxcircle;  
  8.     }  
  9.   
  10.     @Override  
  11.     public void display() {  
  12.         circle.displayCircle();  //由Delegate来完成工作  
  13.     }  
  14. }  
  15.   
  16. public class Client {  
  17.   
  18.     public static void main(String[] args) {  
  19.         XXCircle circle = new XXCircle();  
  20.         CircleObject co = new CircleObject(circle); //把这个adaptee对象包含进去  
  21.         co.display();  
  22.     }  
  23. }  
  在上面代码中,XXCircle属于被适配者Adaptee,CircleObject是适配器Adapter。适配器将源XXCircle和目标Shape进行适配,这样客户用适配器CircleObject就可完成画圆的工作。
   Class Adapter模式是通过多重继承来实现的(java中没有多继承,是通过接口来实现的)。 对于上面画笔的例子,其Class Adapter模式的代码实现如下:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class CircleClass extends XXCircle implements Shape {  
  2.   
  3.     @Override  
  4.     public void display() {  
  5.         super.displayCircle();  
  6.     }  
  7. }  
  8.   
  9. public class Client {  
  10.   
  11.     public static void main(String[] args) {  
  12.         CircleClass cc = new CircleClass();  
  13.         cc.display();  
  14.     }  
  15. }  
  这里继承了adaptee类同时实现了目标接口(这里的目标是一个接口,一般情况下也可以是具体的或抽象的类),相当多重继承。
   Adapter的优势:
  (1) 委托实现,提供不同接口。
上面的Adapter即是这种情况。
   (2) 提供接口默认实现,简化客户代码。 对于Java程序员而言,Adapter这个词可能再熟悉不过了,JDK中有很多Adapter类,如WindowAdapter、MouseAdapter等,分别实现了WindowListener、MouseListener等接口,这些都是Class Adapter的实例。由于这些接口非常常用,而我们往往只关心这些接口的某几个方法,但implements接口要求必须实现接口的所有方法,以至于我们经常不得已将接口方法实现为空。这实在是一件非常繁琐的事情,并且,代码中充斥着一些空方法,也会影响阅读,为此,JDK通过随接口一同发布的Adapter类为这些接口方法提供了缺省的空方法,我们只需要从Adapter类派生(实际上,由于Adapter类往往被用于编写匿名内部类,因此,连派生这一步都省了,编译器会自动为我们完成这一步),并为需要处理的接口方法提供实现即可。
  这与上面讨论的Adapter的定义稍有不同,但本质上还是比较相似的:解决需求与实现间不一致的问题。
   (3) 接口转换。 有时候,现有的实现在设计之初没有或者不便于实现一些基本功能以外的接口,当遇到这种问题的时候,一种解决方案是在现有的实现中添加这些接口的实现,但往往更好的办法是提供一个Adapter类,在维持原有接口的同时,达到支持新接口的目的。
  对于普通应用而言,这种应用Adapter模式的情况比较常见。例如一个用javax.swing.JTable显示AddressBook的应用,根据设计,我们不可能直接让AddressBook类实现JTable所需的TableModel接口(这会让其它使用AddressBook类的人莫名其妙),要在JTable中显示AddressBook,一种可行的作法是引入Adapter类,实现TableModel接口,在保持JTable和AddressBook独立性的基础上,通过引入Adapter类达到适配的目的。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. import java.util.*;  
  2. import javax.swing.*;  
  3. import javax.swing.table.*;  
  4.   
  5. //地址条目  
  6. class AddressItem {  
  7.   
  8.     public String name;  
  9.     public String address;  
  10.     public String tel;  
  11.     //...more  attribute  
  12.   
  13.     public AddressItem(String name, String address, String tel) {  
  14.         this.name = name;  
  15.         this.address = address;  
  16.         this.tel = tel;  
  17.     }  
  18.   
  19.     public String getName() {  
  20.         return name;  
  21.     }  
  22.   
  23.     public String getAddress() {  
  24.         return address;  
  25.     }  
  26.   
  27.     public String getTel() {  
  28.         return tel;  
  29.     }  
  30. }  
  31.   
  32. //地址薄  
  33. class AddressBook {  
  34.   
  35.     ArrayList<AddressItem> personList = new ArrayList<>();  
  36.   
  37.     public int getSize() {  
  38.         return personList.size();  
  39.     }  
  40.   
  41.     public boolean addItem(AddressItem item) {  
  42.         return personList.add(item);  
  43.     }  
  44.   
  45.     public boolean addItem(AddressItem[] aItem) {  
  46.         return personList.addAll(Arrays.asList(aItem));  
  47.     }  
  48.   
  49.     public AddressItem getItem(int index) {  
  50.         return personList.get(index);  
  51.     }  
  52. }  
  53.   
  54. //通过在TableModel中包含一个AddressBook对象达到适配,这样AddressBook不用做任何更改  
  55. class AddressBookTableAdapter extends AbstractTableModel {  
  56.   
  57.     AddressBook ab;  
  58.     String[] columnName = {"Name""Address""Tel."};  
  59.   
  60.     public AddressBookTableAdapter(AddressBook ab) {  
  61.         this.ab = ab;  
  62.     }  
  63.   
  64.     // TableModel  impl  
  65.     @Override  
  66.     public int getRowCount() {  
  67.         return ab.getSize();  
  68.     }  
  69.   
  70.     @Override  
  71.     public int getColumnCount() {  
  72.         return columnName.length;  
  73.     }  
  74.   
  75.     @Override  
  76.     public Object getValueAt(int row, int col) {  
  77.         AddressItem item = ab.getItem(row);  
  78.         if (item == null) {  
  79.             return null;  
  80.         }  
  81.         switch (col) {  
  82.             case 0:  
  83.                 return item.getName();  
  84.             case 1:  
  85.                 return item.getAddress();  
  86.             case 2:  
  87.                 return item.getTel();  
  88.             default:  
  89.                 return null;  
  90.         }  
  91.     }  
  92.   
  93.     @Override  
  94.     public String getColumnName(int c) {  
  95.         return columnName[c];  
  96.     }  
  97. }  
  98.   
  99. class AddressBookTest extends JFrame {  
  100.   
  101.     AddressItem[] aItem = {  
  102.         new AddressItem("Bill""Sky  Road.  No.1""123"),  
  103.         new AddressItem("David""Sky  Road.  No.2""456"),  
  104.         new AddressItem("God""Sky  Road.  No.3""789")  
  105.     };  
  106.     AddressBook ab = new AddressBook();  
  107.   
  108.     public AddressBookTest() {  
  109.         ab.addItem(aItem);  
  110.         TableModel model = new AddressBookTableAdapter(ab);  
  111.         getContentPane().add(new JScrollPane(new JTable(model)));  
  112.     }  
  113.   
  114.     public static void main(String[] args) {  
  115.         JFrame frame = new AddressBookTest();  
  116.         frame.setTitle("Tables  and  Models");  
  117.         frame.setBounds(300300450300);  
  118.         frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);  
  119.         frame.setVisible(true);  
  120.     }  
  121. }  
   (4) 简化接口,分离实现
  有些情况下,随着系统的升级和设计的修改,新的实现方式可能会对外提供新的接口,为了保持向下兼容,一种普遍的作法是在客户(使用我们接口的人,并非最终Customer)接口类中通过重载(或Factory Pattern等方式)提供针对不同接口的实现。这种实现存在两个严重问题:代码难以维护,随着系统的升级,接口类将变得越来越臃肿;底层对客户过于透明,或者说,客户程序员要很好地使用接口类,必须对底层的实现策略有所了解。
  一种改进的作法是:针对不同接口提供不同的Adapter类,最终将接口统一起来。这种实现方式不仅使得底层对客户程序员透明,也使得今后进行系统扩展所受到的束缚和需要进行的修改更小。
   总结出Adapter模式可应用于如下的情况:
  1、系统需要使用现有的类,而此类的接口不符合系统的需要。
  2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。这种情况从不同的角度考虑,可能被划入Facade模式的范畴,但从与现有设计适配的角度考虑该问题,则将其划归Adapter模式也是可以理解的。
  3、通过接口转换,将一个类插入另一个类系中。有人举过这样一个例子:虎与飞禽是没有直接关联的两类动物,但是现在出来了个“飞虎”,它同时具有虎肉食动物跟飞禽会飞的特质,要在飞禽这个类系中添加一个成员类“飞虎”,除了直接实现“飞虎”类,还有一种简单的办法是实现一个Adapter类,在其中包容一个虎的对象,同时实现飞禽的接口即可。当然,对于这个问题,多继承或者实现多接口可能是一个更直观的作法,在实际应用中,可视具体需要确定采用何种作法。


1.1.7 Bridge桥接模式


  将事物的抽象部分与其实现部分解耦,使它们都可以独立地变化。大致意思是说,对事件本身的抽象为抽象部分,对事物功能的实现为实现部分,要使这两部分分离。注意这里的实现部分并不是指从抽象类派生的具体类。
  Bridge模式的结构如下:

图7-1 桥接模式

  其中类和对象的关系为:
  1. Abstraction(抽象类):定义抽象部分的接口,维护一个Implementor(实现抽象类)的对象。
  2. RefinedAbstraction(扩充抽象类):扩充由Abstraction定义的接口。
  3. Implementor(实现类接口):定义实现部分的接口,这个接口不一定要与Abstraction的接口完全一致,事实上这两个接口可以完全不同,一般地将,Implementor接口仅提供基本操作,而Abstraction定义的接口可能会做更多复杂的操作。
  4. ConcreteImplementor(具体实现类):实现Implementor的接口并且具体实现它。

  由于Bridge实现抽象-实现的特性,它与Builder模式存在一定的相似性,但二者的区别也是十分显著的,后者更专注于复杂对象的创建,可以认为是Bridge模式的在对象创建方面的一个应用。
  Bridge模式是结构型模式,与Object Adapter模式及Facade模式等Structural Patterns也存在一定的相似性。Bridge模式与Facade模式的区别比较明显,二者的意图完全不同,前者通过Delegate实现抽象-实现的分离,而后者通过封装已有多个子系统来简化接口;而Bridge模式与Object Adapter模式的区别也主要表现在意图上,Adapter模式的目的在于接口转换,虽然对于Object Adapter而言,具体的实现被委托给了具体的adaptee类。此外,Adapter与Adaptee往往不具有相同的接口(要不然何来转换的必要),而Bridge模式下,Implementor与ConcreteImplementor属于统一类系,并且,前者标识了后者对外的接口;同样,对于Facade类而言,它与被封装的子系统之间往往也没有继承关系。

  实例1:几何图形渲染
  下面是《Design Patterns Explained》书中的例子。其结构图如下:

图7-2 桥接模式例子-几何图形渲染

  下面是它的实现:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. package com.demo.designpattern;  
  2.   
  3. //事物的实现部分:要完成形状的渲染功能  
  4. abstract class Drawing {  
  5.   
  6.     abstract public void drawLine();  
  7.   
  8.     abstract public void drawCircle();  
  9. }  
  10.   
  11. //具体的渲染器V1Drawing  
  12. class V1Drawing extends Drawing {  
  13.   
  14.     @Override  
  15.     public void drawLine() {  
  16.         DP1.draw_a_line();  //调用关DP1中的静态方法  
  17.     }  
  18.   
  19.     @Override  
  20.     public void drawCircle() {  
  21.         DP1.draw_a_circle();  
  22.     }  
  23. }  
  24.   
  25. //具体的渲染器V2Drawing  
  26. class V2Drawing extends Drawing {  
  27.   
  28.     @Override  
  29.     public void drawLine() {  
  30.         DP2.drawLine();//调用关DP2中的静态方法  
  31.     }  
  32.   
  33.     @Override  
  34.     public void drawCircle() {  
  35.         DP2.drawCircle();  
  36.     }  
  37. }  
  38.   
  39. class DP1 {  
  40.   
  41.     public static void draw_a_line() {  
  42.         System.out.println("使用DP1的draw_a_line()画线");  
  43.     }  
  44.   
  45.     public static void draw_a_circle() {  
  46.         System.out.println("使用DP1的draw_a_circle()画圆");  
  47.     }  
  48. }  
  49.   
  50. class DP2 {  
  51.   
  52.     public static void drawLine() {  
  53.         System.out.println("使用DP2的drawLine()画线");  
  54.     }  
  55.   
  56.     public static void drawCircle() {  
  57.         System.out.println("使用DP2的drawCircle()画圆");  
  58.     }  
  59. }  
  60.   
  61. //事物本身的抽象部分:抽象的形状实体  
  62. abstract class Shape {  
  63.   
  64.     protected Drawing myDrawing; //它只知道这是一个渲染器,并不知道是哪个具体的渲染器  
  65.   
  66.     abstract public void draw();  
  67.   
  68.     Shape(Drawing drawing) {  //构造函数:用渲染器创建一个形状实体  
  69.         myDrawing = drawing;  //运行时myDrawing会绑定到某个具体的渲染器上  
  70.     }  
  71.   
  72.     protected void drawLine() {  //渲染直线  
  73.         myDrawing.drawLine();  
  74.     }  
  75.   
  76.     protected void drawCircle() {  //渲染圆形  
  77.         myDrawing.drawCircle();  
  78.     }  
  79. }  
  80.   
  81. //具体的矩形形状  
  82. class Rectangle extends Shape {  
  83.   
  84.     public Rectangle(Drawing darw) { //同样只知道是一个渲染器,并不知道是哪个具体的渲染器  
  85.         super(darw);  
  86.     }  
  87.   
  88.     @Override  
  89.     public void draw() {  
  90.         drawLine();  
  91.         drawLine();  
  92.         drawLine();  
  93.         drawLine();  
  94.     }  
  95. }  
  96.   
  97. //具体的圆形形状  
  98. class Circle extends Shape {  
  99.   
  100.     public Circle(Drawing draw) {  
  101.         super(draw);  
  102.     }  
  103.   
  104.     @Override  
  105.     public void draw() {  
  106.         myDrawing.drawCircle();  
  107.     }  
  108. }  
  109.   
  110. //客户程序  
  111. public class BridgeTest {  
  112.   
  113.     public static void main(String[] args) {  
  114.         Drawing draw1 = new V1Drawing();  
  115.         Drawing draw2 = new V2Drawing();  
  116.         Shape shape1 = new Rectangle(draw1);  
  117.         shape1.draw();  
  118.         Shape shape2 = new Circle(draw2);  
  119.         shape2.draw();  
  120.     }  
  121. }  

  输出结果如下:

[plain]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. 使用DP1的draw_a_line()画线  
  2. 使用DP1的draw_a_line()画线  
  3. 使用DP1的draw_a_line()画线  
  4. 使用DP1的draw_a_line()画线  
  5. 使用DP2的drawCircle()画圆  
  在这个例子中Shape对象实际上是一个Rectangle或Circle对象,但Client并不知道到底是那个,因为它们看起来都一样(都是一个Shape对象)。Drawing实际上是一个V1Drawing或V2Drawing,但Shape对象并知道到底是哪个,因为它们看起来都一样。DP1或DP2在使用时则知道是哪一个。Shape是事物的抽象,Drawing是实现或者操作事物方法的抽象。他们两个都可以独立地变化。正如例子中所说那样,我们输出一个矩形可以使用V1Drawing也可以使用V2Drawing来完成,输出一个圆形也是一样都有两种方法。 Bridge模式遵循了设计模式中两条基本策略:找出变化并封装它;优先使用对象聚合,而不是类继承。
  实例2:Java AWT库
  AWT的架构就使用了Bridge模式。AWT现在基本上被Swing取代了,但是我一直都觉得AWT也有其优势,至少它使用的本地代码就要比Swing快上许多,而且,可以为用户提供熟悉的本地操作系统界面(注意Swing也可以提供本地化的外观,但不是用本地Peer的方式)。简单来讲,AWT提供对程序员的是对窗体界面系统的抽象,而在内部实现中,针对每一种操作系统,分别有不同实现,这就是同位体(Peer)的概念。当程序员调用AWT对象时,调用被转发到对象所对应的一个Peer上,在由Peer调用本地对象方法,完成对象的显示。例如,如果你使用AWT创建了一个Menu类的实例,那么在程序运行时会创建一个菜单同位体的实例,而由创建的同位体的来实际执行菜单的现实和管理。不同的系统,有不同的同位体实现,Solaris JDK将产生一个Motif菜单的同位体,Windows下的JDK将产生一个Windows的菜单的同位体,等等。同位体的使用,使得交叉平台窗口工具的开发变得极为迅速,因为同位体的使用可以避免重新实现本地窗口控件中已经包含的方法。

图7-3 AWT中的组件和其对等体

  实际上,从设计的角度来看,这是一个抽象和实现分离的过程。AWT是抽象,同位体是实现,抽象和实现各自成为一个对象体系,它们由一个桥连接起来,可以各自发展各自的对象层次,而不必顾虑另一方面。这就是Bridge模式所提供的思想。Bridge模式更可以提供在各个不同的实现中动态的进行切换,而不必从新编译程序。
  Bridge模式的优点只有在存在多个RefinedAbstraction、ConcreteImplementor的情况下才比较突出,但很多程序库,为了简化接口类,同时出于扩展的需要,往往也将类与其实现分离成独立的两个类,这在STL、boost中可以说是屡见不鲜。对于我们的普通应用,在设计时也应考虑为以后的扩展保留一定余地,特别是在设计那些基础类时尤其需要注意。
  在MFC中,典型的Bridge模式的应用如CArchive与CFile(分别代表类图中的Abstraction与Implementor),CArchive对外提供读写对象的接口,而CFile及其子类负责提供不同的数据保存机制,如读写内存,读写磁盘文件,读写Socket等。在构造CArchive对象时向其传递一个CFile(或其子类)的对象,使得前者可以获取必要的序列化相关信息,客户代码在进行实际的序列化操作时,可以完全不用去考虑CFile类是如何进行读写的,这种分离实现的结果不但简化了各自对外的接口,也使得对Abstraction和Implementor进行扩展变得十分方便。
  通常,Bridge模式和AbstractFactory模式一起工作,由AbstractFactory来创建一个具体实现的对象体系。特殊的,当只有一个实现的时候,可以将Implementor抽象类去掉。这样,在抽象和实现之间建立起了一一对应的关系,但这并不损害Bridge模式的内涵。这被称为退化了的Bridge模式。
  很多时候,Abstraction层次和Implementor层次之间的方法都不是一一对应的,也就是说,在Abstraction和Implementor之不是简单的的消息转发。通常,我们会将Abstraction作为一个抽象类(而不是接口)来实现。在Implementor层次中定义底层的,或者称之为原子方法,而在Abstraction层次中定义一些中高层的基于原子方法的抽象方法。这样,就能更为清晰的划分Abstraction和Implementor,类的结构也更为清晰。
  在以下几种情况下可以考虑使用Bridge模式:
  Case 1、
你不希望在抽象和它的实现部分之间有一个固定的绑定关系。例如这种情况可能是因为,在程序运行时刻实现部分应可以被选择或者切换。
  Case 2、类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这时Bridge模式使你可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充。
  Case 3、对一个抽象的实现部分的修改应对客户不产生影响,即客户的代码不必重新编译。
  Case 4、(C++)你想对客户完全隐藏抽象的实现部分。在C++中,类的表示在类接口中是可见的。
  Case 5、有许多类要生成。这样一种类层次结构说明你必须将一个对象分解成两个部分。Rumbaugh称这种类层次结构为“嵌套的泛化”(nested generalizations)。
  Case 6、你想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点。一个简单的例子便是Coplien的String类,在这个类中多个对象可以共享同一个字符串表示(StringRep)。
  与Builder模式一样,Bridge模式也是3D原则(见设计模式的原则)在设计中的应用,因此他们具有一定的相似性,但Builder专注与复杂对象的创建,而Bridge模式则主要强调Delegate,即职责的委派。
  这里Case 1在很多界面方案切换程序中被大量使用。如使用单独的界面绘制类,在其中实现同一界面方案下不同控件的色彩、阴影等绘制方法,每一种界面元素都保存或者可以间接获取到该绘制类的指针,从而可以在实际绘制自身时加以运用。感兴趣的朋友可以参考著名界面库BCGPro的CBCGPVisualManager及其派生类的实现。同样,Case 1在许多需要考虑跨平台(可能是OS,也可能是所使用的第三方应用系统,如数据库)问题的应用中也被大量用到,只不过,在这些情况下,实际被编译到最终应用中的可能只是多个Implementor中的一个。
  以上几种Case中,前5种主要表现的是由Delegate所带来的分别修改而不相互影响等优点,而Case 6所提出的共享实现是说,我们可以通过将大家公用的部分提取成单独的Implementor,只在各SpecificAbstraction中进行更深入的对Abstraction的细化,同时通过组合/继承将部分操作交给Implementor完成。
  Bridge模式有以下一些优点:
  (1)分离接口及其实现部分。一个实现未必不变地绑定在一个接口上。抽象类的实现可以在运行时刻进行配置,一个对象甚至可以在运行时刻改变它的实现。
  将Abstraction和Implementor分离有助于降低对实现部分编译时刻的依赖性,当改变一个实现类时,并不需要重新编译Abstraction类和它的客户程序。为了保证一个类库的不同版本之间的二进制兼容性,一定要有这个性质。另外,接口与实现分离有助于分层,从而产生更好的结构化系统,系统的高层部分仅需知道Abstraction和Implementor即可。
  (2)提高可扩充性。你可以独立地对Abstraction和Implementor层次结构进行扩充。
  (3)实现细节对客户透明。你可以对客户隐藏实现细节,例如共享Implementor对象以及相应的引用计数机制(如果有的话)。


1.1.8 Facade外观模式


  为子系统中的一组接口提供一个一致的界面, Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。大致意思是说,为子系统中的各类(或结构与方法)提供一个简明一致的界面,隐藏子系统的复杂性,使子系统更加容易使用。Façade模式是结构型模式。
  实际应用中,我们在对付一些老旧的code(尤其是将C的代码转成C++代码)或者即便不是老旧code,但涉及多个子系统时,除了重写全部代码(对于老旧code而言),我们还可能采用这样一种策略:重新进行类的设计,将原来分散在源码中的类/结构及方法重新组合,形成新的、统一的接口,供上层应用使用。
  Facade模式的示意图如下:

图8-1 外观模式

  Facade:为客户调用方定义简单的统一接口(外观),将客户的请求传递给相应的子系统对象处理。
  Clients:调用者。通过Facade接口调用提供某功能的内部类群。
  Packages:各子系统,功能提供者。指提供功能的类群(模块或子系统)。
  例如,我们把一个很重要的文件,放在了第二抽屉里,而第二个抽屉的钥匙放在了第一个抽屉里,我们要想取出这个文件,第一步肯定要拿到第一个抽屉的钥匙,然后打开它再拿出第二个抽屉的钥匙,最后打开第二个抽屉取出文件。首先我们要实现二个子系统,把抽屉比喻成系统,有点夸张了(DrawerOne、DrawerTwo):

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class DrawerOne {  
  2.   
  3.     public void open() {  
  4.         System.out.println("第一个抽屉被打开了");  
  5.         getKey();  
  6.     }  
  7.   
  8.     public void getKey() {  
  9.         System.out.println("得到第二个抽屉的钥匙");  
  10.     }  
  11. }  
  12.   
  13. class DrawerTwo {  
  14.   
  15.     public void open() {  
  16.         System.out.println("第二个抽屉被打开了");  
  17.         getFile();  
  18.     }  
  19.   
  20.     public void getFile() {  
  21.         System.out.println("得到这个重要文件");  
  22.     }  
  23. }  
  24.   
  25. public class Client {  
  26.   
  27.     public static void main(String[] args) {  
  28.         DrawerOne darwerOne = new DrawerOne();  
  29.         DrawerTwo darwerTwo = new DrawerTwo();  
  30.         darwerOne.open();  
  31.         darwerTwo.open();  
  32.     }  
  33. }  
  由于没有使用Façade模式,可以看到要想得到这个文件要首先打开第一个抽屉,然后再打开第二个抽屉,在我们实际所开发的系统中,有时候客户要实现某一操作,并不需要知道实现这一操作的详细步骤,而是简单地点击某一个按钮就可以得到自己想要的结果。下面对上面的代码使用Façade模式进行改进,建立一个FacadeDrawer类:

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class DrawerFacade {  
  2.   
  3.     DrawerOne darwerOne = new DrawerOne();  
  4.     DrawerTwo darwerTwo = new DrawerTwo();  
  5.   
  6.     public void open() {  
  7.         darwerOne.open();  
  8.         darwerTwo.open();  
  9.     }  
  10. }  
  11.   
  12. public class DrawerClient {  
  13.   
  14.     public static void main(String[] args) {  
  15.         DrawerFacade drawer = new DrawerFacade();  
  16.         drawer.open();  
  17.     }  
  18. }  
  正如上面所说,客户端client,它并不需要关心子系统,而是关心DrawerFacade所留下来的和外部交互的接口,而子系统在DrawerFacade的聚合。
  又如系统有A1, A2, A3等类。客户端需要调用各系统的A1.doSomething1(); A2.doSomething2(); A3.doSomething3()来完成某功能。Facade模式的实现模型就是:
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class A1 {  
  2.   
  3.     public void doSomething1() {  
  4.         //...  
  5.     }  
  6. }  
  7.   
  8. class A2 {  
  9.   
  10.     public void doSomething2() {  
  11.         //...  
  12.     }  
  13. }  
  14.   
  15. class A3 {  
  16.   
  17.     public void doSomething3() {  
  18.         //...  
  19.     }  
  20. }  
  21.   
  22. public class Facade {  
  23.   
  24.     public void doSomething() {  
  25.         A1 a1 = new A1();  
  26.         A2 a2 = new A2();  
  27.         A3 a3 = new A3();  
  28.         a1.doSomething1();  
  29.         a2.doSomething2();  
  30.         a3.doSomething3();  
  31.     }  
  32. }  
  33.   
  34. public class Client {  
  35.   
  36.     public static void main(String[] args) {  
  37.         Facade facade = new Facade();  
  38.         facade.doSomething();  
  39.     }  
  40. }  
  Facade某种意义上与Adapter及Proxy有类似之处,但是,Proxy(代理)注重在为Client-Subject提供一个访问的中间层,如CORBA可为应用程序提供透明访问支持,使应用程序无需去考虑平台及网络造成的差异及其它诸多技术细节;Adapter(适配器)注重对接口的转换与调整;而Facade所面对的往往是多个类或其它程序单元,通过重新组合各类及程序单元,对外提供统一的接口/界面。
  举例说明这种区别:
  Adapter模式:有银行A系统,银行B系统,它们具有大致一样的功能,但它们的对外接口各不一样。如果需要在C系统中使用A系统或B系统,为了接口的一致,我们需要在C系统中实现一个Adapter,通过这个Adapter提供的接口,我们就可以透明的调用银行A系统的功能、或者银行B系统的功能了;
  Facade模式:有银行A系统,它具有非常复杂的功能,有融资,贷款,存款,取款,信用审查,担保等等功能。如果需要在C系统中,只需用到A系统的一部分功能,比如融资,我们就可以为需要用到的融资功能封装一个Facade,通过这个facade,我们不用管A系统的内部是怎么实现融资业务的,也不用管信用审查是否通过,是否具有充分的担保资源等等,大大简化了客户端的调用。
  C++程序员经常会使用全局函数来实现一些通用方法,而Java中由于语言本身的限制,所有成员和方法都必须封装在类中,因此, 引入了很多辅助类(这些类的成员方法通常都是static的),用于提供一些相互独立的通用方法,如java.lang.System、java.util.Arrays、java.util.Collections、javax.swing.SwingUtilities等,这些类往往也被认为是Facade类。
  在遇到以下情况使用Facade模式:

  1、当你要为一个复杂子系统提供一个简单接口时。子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类。这使得子系统更具可重用性,也更容易对子系统进行定制,但这也给那些不需要定制子系统的用户带来一些使用上的困难。Facade可以提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过Facade层。
  2、客户程序与抽象类的实现部分之间存在着很大的依赖性。引入Facade将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
  3、当你需要构建一个层次结构的子系统时,使用Facade模式定义子系统中每层的入口点,如果子系统之间是相互依赖的,你可以让它们仅通过Facade进行通讯,从而简化了它们之间的依赖关系。
   Facade模式有下面一些优点:
  1、它对客户屏蔽子系统组件,因而减少了客户处理的对象的数目并使得子系统使用起来更加方便。
  2、它实现了子系统与客户之间的松耦合关系,而子系统内部的功能组件往往是紧耦合的。
  松耦合关系使得子系统的组件变化不会影响到它的客户。Facade模式有助于建立层次结构系统,也有助于对对象之间的依赖关系分层。Facade模式可以消除复杂的循环依赖关系。这一点在客户程序与子系统是分别实现的时候尤为重要。
   在大型软件系统中降低编译依赖性至关重要。 在子系统类改变时,希望尽量减少重编译工作以节省时间。用Facade可以降低编译依赖性,限制重要系统中较小的变化所需的重编译工作。Facade模式同样也有利于简化系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。
  3、如果应用需要,它并不限制它们使用子系统类。因此你可以在系统易用性和通用性之间加以选择。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值