转换接口的适配器 – Adapter
将一个类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
适用性
以下情况使用Adapter模式:
- 使用一个已经存在的类,而它的接口不符合需求
- 创建一个可复用的类,该类可以与其他不相关的类或不可预见的类协同工作
- 想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配他们的接口,对象适配器可以适配它的父类接口
结构
类适配器使用多重继承对一个接口与另一个接口进行匹配:
对象匹配器依赖于对象组合:
参与者
- Target: 定义Client使用的与特定领域相关的接口
- Client: 与符合 Target 接口的对象协同
- Adaptee: 定义一个已经存在的接口,这个接口需要适配
- Adapter: 对Adaptee 的接口与 Target 接口进行适配
效果
类适配器和对象适配器有不同的权衡,其中类适配器:
- 用一个具体的Adapter类对Adaptee和Target进行匹配,结果是当我们想要匹配一个类以及它的所有子类时,类Adapter将不能胜任工作。
- 使得Adapter可以重定义Adaptee的部分行为,因为Adapter是Adaptee的一个子类
- 仅仅引入了一个对象,并不需要额外的指针以间接得到adaptee
对象适配器:
- 允许一个Adapter与多个Adaptee(Adaptee以及其子类)一同工作。Adapter也可以一次给所有的Adaptee添加功能
- 使得重定义Adaptee的行为比较困难。这就需要生成Adaptee的子类并且使得Adapter引用这个子类而不是引用Adaptee本身 (作为参数传递)
使用适配器模式需要考虑的因素:
- Adapter的匹配程度。 对Adaptee的接口与Target的接口进行匹配的工作量,各个Adapter可能不一样。可能从简单的接口转换到支持完全不同操作集合
- 可插入Adapter。 当其他类使用一个类时,所需的假定条件越少,就具有越高的可复用性。
- 使用双向适配器提供透明操作。 被适配的对象不再兼容Adaptee的接口,因此并不是所有Adaptee对象可以被使用的地方它都可以被使用,双向适配器可以提供这样的透明性(接口)。
实现
Shape
根据相对的两个角定义一个边框,而TextView
根据原点、宽度和高度定义一个边框。
public interface Shape {
void boundingBox (Point bottomLeft, Point topRight);
Manipulator createManipulator ();
}
public interface TextView {
void getOrigin(Coord x, Coord y);
void getExtent (Coord width, Coord height);
boolean isEmpty();
}
对象适配器采用对象组合的方法将具有不同接口的类组合在一起,适配器中维护了一个指向TextView
的指针
public class TextShapeAdapter implements Shape, TextView {
private TextView textView;
public TextShapeAdapter(TextView textView) {
this.textView = textView;
}
@Override
public void boundingBox(Point bottomLeft, Point topRight) {
Coord bottom = new Coord(), left= new Coord(), width= new Coord(), height= new Coord();
getOrigin(bottom, left);
getExtent(width, height);
bottomLeft.setBottom(bottom.getCenter());
bottomLeft.setLeft(left.getCenter());
topRight.setBottom(height.getCenter() + bottom.getCenter());
topRight.setLeft(left.getCenter() + width.getCenter());
}
@Override
public Manipulator createManipulator() {
return null;
}
@Override
public void getOrigin(Coord x, Coord y) {
}
@Override
public void getExtent(Coord width, Coord height) {
}
@Override
public boolean isEmpty() {
return textView.isEmpty();
}
}
相关模式
- 模式Bridge的结构与对象适配器类似,但是Bridge的目的是将接口部分与实现部分分离,从而对他们可以较容易也相对独立的加以改变
- Decorator模式增强了其他对象的功能而同时又不改变他们的接口,而且其支持递归组合。
- Proxy在不改变它的接口的条件下,为另一个对象定义了一个代理。
分离抽象和实现 – Bridge
将抽象部分与它的实现部分分离,使它们都可以独立地变化。 继承机制将抽象部分与它的实现部分固定在一起,是的难以对抽象部分和实现部分独立地进行修改、扩充和重用。
适用性
以下情况使用Bridge:
- 不希望抽象和它的实现部分之间有固定的绑定关系,例如:在程序运行时刻实现部分应该可以被选择或者切换。
- 类的抽象以及他的实现都应该可以通过生成子类的方法加以扩充。
- 对一个抽象的实现部分的修改应该对客户不产生影响,即客户的代码不需要重新编译
- 想在多个对象间共享实现,但同时要求客户并不知道这一点
结构
参与者
Abstraction
: 定义抽象接口的类;维护一个指向Implementor
类型对象的指针RefinedAbstraction
: 扩充Abstraction
定义的接口Implementor
: 定义实现类的接口,该接口不一定要与Abstraction
接口完全一致;一般来讲,Implementor
接口仅提供基本操作,而Abstraction
则定义了基于这些操作的较高层次的操作ConcreteImplementor
: 实现Implementor
接口并定义了它的具体实现。
效果
Bridge模式有以下优点:
- 分离接口及其实现部分。一个实现未必不变地绑定在一个接口上。抽象类的实现可以在运行时刻进行配置,一个对象甚至可以在运行时刻改变它的实现。将
Abstraction
与Implementor
分离有助于降低对实现部分编译时刻的依赖,当改变实现类时,并不需要重新编译Abstraction
类和它的客户程序。为了保证一个类库的不同版本之间的兼容性,一定要有这个性质 - 提高可扩充性。可以独立地对
Abstraction
与Implementor
进行扩充 - 实现细节对客户透明。
- Bridge模式要求正确识别出系统中两个独立变化的维度。因此其使用范围具有一定的局限性。
实现
- 仅有一个Implementor。在仅有一个实现时,没必要创建一个抽象的Implementor,这是Bridge退化的情况。这时,Abstraction和Implementor是一一对应的,但是当改变一个类的实现不会影响已有的客户程序,模式分离的机制还是有用的
- 创建正确的Implementor对象。当Abstraction知道所有的ConcreteImplementor类,它就可以在构造器中对其中的一个类进行实例化,可以通过传递参数给构造器确定实例化哪个类;还可以选择一个缺省实现,然后根据需要改变这个实现;还可以代理给一个对象,由它决定,比如Abstract Factory。
- 共享Implementor对象。
相关模式
Abstract Factory模式可以用来创建和配置一个特定的Bridge模式 Adapter模式用来 帮助无关的类协同工作,它通常在系统设计完成后才会被使用。然而,Bridge模式则是在系统开始时使用,它使得抽象接口和实现部分可以独立进行改变。
部分整体递归组合 —— Composite
将对象组合成树形结构以表示 “部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。
适用性
以下情况适用于Composite模式:
- 想表示对象的部分-整体层次结构
- 希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象
结构
参与者
Component
: 为组合中的对象声明接口;在适当的情况下,实现所有类共有接口的缺省行为;声明一个接口用于访问和管理Component的子组件;在递归结构中定义一个接口,用于访问一个父部件Leaf
: 在组合 中表示叶节点对象,叶节点没有子节点;在组合中定义图元对象的行为Composite
: 定义有子部件的那些部件的行为;存储子部件;在Component接口中实现与子部件有关的操作
协作
用户使用Component
类接口与组合结构中的对象进行交互。不可接受接收者是一个叶节点,则直接处理请求。如果接收者是Composite
,它通常将请求发送给它的子部件,在转发请求之前或之后可以执行一些辅助操作。
效果
Composite模式的效果如下:
- 定义了包含基本对象和组合对象的类层次结构
- 客户可以一致地使用组合结构和单个对象
- 使得更容易增加新类型的组件。新定义的Composite或Leaf子类自动地与已有的结构和客户代码一起工作,客户程序不需要因新的Component类而改变
- 使你的设计更加一般化。容易增加新组件也会产生一些问题,那就是很难限制组合中的组件。如果希望一个组合只能有某些特定的组件,不能依赖类型系统施加这些约束,而必须在运行时刻进行检查
实现
我们在实现Composite模式进需要考虑以下问题:
- 显式的父部件的引用。保持从子部件到父部件的引用能简化组合结构的遍历和管理。父部件引用可以简化结构的上移和组件的删除。
- 共享组件。共享组件是比较有用的,比如它可买减少对存储空间的需求,但是当一个组件只用一个父部件时,很难共享组件
- 最大化Component接口。Composite模式的目的之一是使得用户不知道他们正在使用的具体的Leaf和Composite类。因此,Composite类应为Leaf和Composite类尽可能多定义一些公共操作。Composite类通常为这些操作提供缺省的实现。
- 在什么地方声明管理子部件的操作。我们是应该在Component中声明这些操作,并使这些操作对Leaf类有意义,还是只应该在Composite和它的子类中声明这些操作?这需要在安全性和透明性之间做出权衡选择:
- 类层次结构的根部,即在Components中定义节点管理接口的方法具有良好的透明性,但是会牺牲安全性。因为客户有可能会做一些无意义的操作
- 在Composite类中定义具有良好的安全性,但是Leaf和Composite具有不同的接口
代码
计算机和立体声组合音响这样的设备经常被组装成部分-整体层次结构或者是容器层次结构。例如,底盘可包含驱动装置和平面板,总线含有多个插件,机柜包括底盘、总线等。这种结构很自然地用Composite模式进行模拟。
/**
* Equipment为 部分-整体 层次结构中的所有设备定义一个接口
*/
public abstract class EquipmentComponent {
private String name;
EquipmentComponent(String name) {
this.name = name;
}
public String getName() {return name;}
public abstract int netPrice();
public abstract int discountPrice();
public abstract void add(EquipmentComponent e);
public abstract void remove(EquipmentComponent e);
public abstract Iterator<EquipmentComponent> createIterator();
}
EquipmentComposite
是包含其他设备的基类,也是 EquipmentComponent
的子类
public abstract class EquipmentComposite extends EquipmentComponent {
private List<EquipmentComponent> equipments;
public EquipmentComposite (String name){
super(name);
}
@Override
public Iterator<EquipmentComponent> createIterator() {
return equipments.iterator();
}
@Override
public int netPrice() {
Iterator<EquipmentComponent> it = createIterator();
int total = 0;
while(it.hasNext()) {
total += it.next().netPrice();
}
return total;
}
@Override
public void add(EquipmentComponent e) {
equipments.add(e);
}
@Override
public void remove(EquipmentComponent e) {
equipments.remove(e);
}
}
还有 Leaf 类:
public class FloppyDiskLeaf extends EquipmentComponent {
FloppyDiskLeaf(String name) {
super(name);
}
@Override
public int netPrice() { return 0; }
@Override
public int discountPrice() { return 0; }
@Override
public void add(EquipmentComponent e) { }
@Override
public void remove(EquipmentComponent e) { }
@Override
public Iterator<EquipmentComponent> createIterator() {return null;}
}
相关模式
- Decorator模式经常与Composite模式一起使用,它们通常有一个公共的父类。
- Flyweight 让你共享组件,但不能引用它们的父部件
- Itertor 用来遍历 Composite
- Visitor 将本来应该分布在Composite和Leaf类中的扣件和行为局部化
动态修改对象职责 – Decorator
动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。
适用性
以下情况使用Decorator模式:
- 不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
- 处理那些可以撤消的职责
- 当不能采用生成子类的方法进行扩充时。可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。
结构
参与者
Component
: 定义一个对象接口,可以给这些对象动态地添加职责ConcreteComponent
: 定义一个对象,可以给这个对象添加一些职责Decorator
: 维持一个指向Component
对象的指针,并定义一个与Component
接口一致的接口ConcreteDecorator
: 向组件添加职责
Decorator
将请求转发给它的 Component
对象,并有可能在转发请求后执行一些附加动作
效果
Decorator模式主要有如下优点:
- 比静态继承更灵活
- 避免在层次结构高层的类有太多的特征。 定义一个简单的类,并且用Decorator类给它逐渐添加功能,这样可以从简单的部件组合出复杂的功能。
Decorator模式主要有如下缺点:
- Decorator与它的Component不一样。Decorator是一个透明的包装,如果我们从对象标识出发,一个被装饰了的组件与这个组件是有差别的,因此,使用装饰时不应该依赖对象标识
- 有许多小对象。采用Decorator模式时,往往会产生许多看上去类似的小对象,这些对象仅仅在他们相互连接的方式上有所不同,而不是它们的类或是它们的属性值不同,造成这样的系统很难学习。
实现
实现Decorator模式时应该注意以下几点:
- 接口的一致性。装饰对象的接口必须与它所装饰的Component的接口是一致的,因此,所有的ConcreteDecorator类必须有一个公共的父类。
- 省略抽象的Decorator类。当仅需要一个职责时,可以把Decorator向Component转发请求的职责合并到ConcreteDecorator中
- 保持Component类的简单性。因为Component是公共父类,它应定义接口而不是存储数据。赋予Component太多的功能也使得,具体的子类有一些它们并不需要的功能的可能性大大增加。
- 改变对象外壳与改变对象内核。Decorator改变的是对象在外壳,Strategy模式改变的是对象在内核
代码示例
Component类如下:
public abstract class VisualComponent {
public VisualComponent(){}
public abstract void draw();
public abstract void resize();
}
将生成Decorator的子类以获取不同的装饰,Decorator装饰由component
实例变量引用的VisualComponent,这个实例变量在构造器中被初始化:
public class Decorator extends VisualComponent {
private VisualComponent component;
Decorator (VisualComponent component) {
this.component = component;
}
@Override
public void draw() {
component.draw();
}
@Override
public void resize() {
component.resize();
}
}
BorderDecorator类为它所包含的组件添加了一个边框。BorderDecorator是Decorator的子类,它重定义Draw操作用于绘制边框:
public class BorderDecorator extends Decorator{
private int borderWidth;
public BorderDecorator(VisualComponent component, int borderWidth) {
super(component);
this.borderWidth = borderWidth;
}
@Override
public void draw() {
super.draw();
drawBorder();
}
private void drawBorder(){}
}
可以用如下方式使用 Decorator:
VisualComponent component = new BorderDecorator(new TextView(), 1);
相关模式
- Adapter模式:适配器给对象一个全新的接口
- Composite模式: 将装饰视为一个退化的、仅有一个组件的组合。然而,装饰仅给对象添加一些额外的职责,而不在于对象聚集
- Strategy模式:用一个装饰你可以改变对象的外表;Strategy模式使得你可以改变对象的内核。
封装复杂子系统接口 —— Facade
为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
适用性
以下情况适用于Facade模式
- 为一个复杂系统提供一个简单接口。Facade可以为一个复杂系统提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需要更多功能的用户可以越过Facade层
- 构建一个层次结构的子系统时,可以使用Facade模式定义子系统中每层的入口点。
- 提高子系统的独立性和可移植性。
结构
参与者
Facade
: 知道哪些子系统类负责处理请求;将客户的请求代理给适当的子系统对象- 子系统类: 实现子系统的功能;处理有Facade对象指派的任务;没有Facade的任何信息
协作
客户程序通过发送请求给Facade的方式与子系统通讯,Facade将这些消息转发给适当的子系统对象。Facade模式本身需要将它的接口转换成子系统的接口
效果
- 屏蔽了子系统组件,减少了客户处理对象的数目并使子系统用起来更方便
- 实现了子系统与客户之间的松耦合关系,而子系统内部的功能组件往往是紧耦合的
实现
- 降低客户-子系统之间的耦合度。 用抽象类实现Facade,而它的具体子类对应于不同的子系统实现,可以进一步降低客户与子系统的耦合度。
- 公共子系统类与私有子系统类。 子系统的公共接口包含所有的客户程序可以访问的类;私有接口仅用于对子系统进行扩充。Java中可以使用抽象类实现私有接口。
相关模式
- Abstract Factory 模式可以与Facade模式一起使用以提供一个接口,这个接口可以用来以一种子系统独立的方式创建子系统对象,Abstract Factory也可以代替Facade模式隐藏那些与平台相关的类。
- Mediator模式与Facade模式的相似之处是,它抽象了一些已有的类的功能。然而,Mediator的目的是对同事之间的任意通讯进行抽象,通常集中不属于单个对象的功能;Facade模式仅仅是对子系统对象的接口进行抽象,从而使它们更容易使用。
对象池 —— Flyweight
运用共享技术有效地支持大量细粒度对象,适用于那些因为数量太大而难以用对象来表示的概念或实体进行建模
适用性
Flyweight 模式的有效性很大程度上取决于如何使用它以及在何处使用它:
- 一个应用程序使用了大量的对象
- 完全由于使用大量对象,造成很大的存储开销
- 对象的大多数状态都可变为外部状态
- 如果删除对象中的外部状态,那么可以用相对较少的共享对象取代很多组对象
- 应用程序不依赖于对象标识,由于Flyweight对象可以被共享,对于概念上明显有别的对象,标识测试将返回真值
结构
下面的对象图说明了如何共享flyweight
参与者
Flyweight
: 描述一个接口,通过这个接口Flyweight可以接受作用于外部状态ConcreteFlyweight
: 实现Flyweight接口,并为内部状态增加存储空间。ConcreteFlyweight
对象必须是可共享的,它存储的状态必须是内部的,必须独立于ConcreteFlyweight
对象的场景UnsharedConcreteFlyweight
: 并非所有的Flyweight子类都需要被共享,Flyweight对象结构的某些层次,UnsharedConcreteFlyweight
对象通常将ConcreteFlyweight
对象作为子节点FlyweightFactory
: 创建并管理Flyweight对象;确保合理地共享flyweightClient
: 维持一个对flyweight的引用;计算或存储一个或多个flyweight的外部状态
协作
- Flyweight执行时需要两种状态——内部的和外部的 内部状态存储于ConcreteFlyweight对象之中;外部状态则由Client对象存储或计算。
- 用户只能从FlyweightFactory对象中得到ConcreteFlyweight对象 这样才能保证这些对象被适当地共享
共享的Flyweight越多,存储节约也就越多。
实现
在使用Flyweight模式时,应该注意以下几点:
- 删除外部状态。该模式的可用性在很大程度上取决于是否容易识别外部状态并将它从共享对象中删除。如果不同种类的外部状态和共享前对象的数目相同的话,删除外部状态不会降低存储消耗。理想的状况是,外部状态可以由一个单独的对象结构中计算得到,且该结构的存储要求非常小
- 管理共享对象。因为对象是共享的,所以用户不能直接对它实例化。用户需要通过FlyweightFactory来操纵共享对象。而且若Flyweight对象数量多的话,还会涉及到引用计数和垃圾回收。
相关模式
Flyweight模式通常和Composite模式结合起来,用共享叶结点的有向无环图实现一个逻辑上的层次结构。 通常,最好使用Flyweight来实现State和Strategy对象。
Proxy
为其他对象提供一种代理以控制对这个对象的访问。 对一个对象进行访问控制的一个原因是为了在我们确实需要这个对象时,才对它进行创建和初始化。这种策略对于那种创建开销很大而且并不一定能用到的对象很适合。使用Proxy代替真正的对象,并且在需要的时候实例化这个图像的对象。
适用性
在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用Proxy模式:
- 远程代理为一个对象在不同的地址空间提供局部代表
- 虚代理根据需要创建开销很大的对象
- 保护代理控制对原始对象的访问,适用于对象应该有不同的访问权限的时候
- 智能指引取代了简单的指针,在访问对象时会执行一些附加操作,比如:
- 对指向实际对象的引用计数,当该对象没有引用时,可以自动释放它
- 第一次引用一个持久对象时,将它装入内存
- 在访问一个对象前,检查是否已经锁定了它,以确保其他对象不能改变它
结构
参与者
- Proxy: 保存一个引用使得代理可以访问实体。若RealSubject和Subject的接口相同,Proxy会应用Subject;提供一个与Subject的接口相同的接口,这样代理就可以用来代替实体
- Subject: 定义RealSubject和Proxy的共用接口,这样就在任何使用RealSubject的地方都可以使用Proxy
- RealSubject: 定义Proxy所代表的实体
效果
Proxy模式在访问对象时引入了一定程度的间接性。根据代理的类型,附加的间接性有很多种用途:
- Remote Proxy 可以隐藏一个对象存在于不同地址空间的事实
- Virtual Proxy 可以进行最优化,例如根据要求创建对象
- Protection Proxy 允许在访问一个对象时有些附加的任务处理
Proxy模式还可以实现copy-on-write方式的优化。在实现copy-on-write时必须对实体进行引用计数,拷贝代理仅增加引用计数,只有当用户请求修改该实体的操作时,代理才会真正拷贝该对象。
代码示例
定义一个Graphic
接口:
public interface Graphic {
void draw();
void handleMouse();
void load();
void save();
String getExtent();
}
Image
是实现了 Graphic
接口的类
public class Image implements Graphic {
@Override
public void draw() {
}
@Override
public void handleMouse() {
}
@Override
public void load() {
}
@Override
public void save() {
}
@Override
public String getExtent() {
return null;
}
}
ImageProxy
是对 Image
类的代理类:
/**
* ImageProxy 同 Image 有相同的接口
*/
public class ImageProxy implements Graphic{
/** proxy 中需要保存对 Image 的引用 */
private Image image;
private String fileName;
private String extend;
private String imageAttr;
public ImageProxy(String fileName) {
this.fileName = fileName;
}
/** 延时实例化 image 对象 */
protected Image getImage() {
if (image == null) {
image = new Image();
}
return image;
}
/** 在代理中保存Image的某个属性 */
@Override
public String getExtent() {
if (extend == null) {
extend = getImage().getExtent();
}
return extend;
}
@Override
public void draw() {
getImage().draw();
}
@Override
public void handleMouse() {
getImage().handleMouse();
}
@Override
public void load() {
}
@Override
public void save() {
}
}
相关模式
- Adapter: 适配器模式为它所适配的对象听了一个不同的接口,而代理提供了与它实体相同的接口。用于访问保护的代理可能会拒绝执行实体会执行的操作,因此它的接口实际上可能只是实体接口的一个子集
- Decorator: 装饰者模式为对象添加一个或多个功能,而代理控制对象的访问
结构型模式总结
结构型模式之间具有相似性,尤其是它们的参与者和协作者之间。
Adapter 与 Bridge
这两个模式都是给另一个对象提供了一定程度上的间接性,有利于系统的灵活性,都是从自身以外的一个接口向这个对象转发请求。 但是这两种模式的用途不同:
- Adapter模式主要是为了解决两个已有接口之间不匹配的问题,不考虑这些接口是怎么实现的,也不考虑它们各自可能会如何演化
- Bridge模式对抽象接口与它的实现部分进行桥接,虽然它允许你修改实现它的类,它仍然为用户提供一个稳定的接口,Bridge也会在系统演化时适应新的实现
由于两种模式的不同点,Adapter模式在类设计好后实施,Bridge在设计类之前实施
Composite、Decorator与Proxy
Composite模式同Decorator模式具有类似的结构图,这说明它们都 基于递归组合来组织可变数目的对象 Decorator旨在使你能够不需要生成子类即可给对象添加职责,避免了静态实现所有功能组合,从而导致子类数量急剧增加。Composite则旨在构造类,使多个相关的独享能够以统一的方式处理,而多重对象可以被当作一个对象来处理,其重点不在于修饰,而在于表示。