结构模式
结构模式描述如何将类或者对象结合在一起形成更大的结构。可以分为类的结构模式与对象的结构模式两种。
类的结构模式:使用继承来把类、接口组合在一起,以形成更大的结构。典型例子是适配器模式。
对象的结构模式:描述怎样把各种不同的类型的对象组合在一起,以实现新的功能的方法。对象结构模式是动态的。典型例子有合成模式、装饰模式、对象适配器模式。
适配(Adapter)器模式
把一个类的接口变的成客户端所期待的另一种接口,从而使用原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
GoF:将一个类的接口转换成客户希望的另外一个接口。此模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
它有两种形式:类适配器、对象适配器。
类适配器模式结构
目标接口,这就是所期待得到的接口,现用系统就是构建在这个接口之上的。
public interface Target {
//需要的方法
void request();
}
虽然Adaptee类已经具有Target所要的功能,但是Adaptee中的提供的接口specificRequest不是Target中所提供的接口request,现在如果要使Adaptee来实现Target,则需要提供一个适配器,因为Adaptee是你不能修改的类,它是第三所提供的。
//源接口,其所拥有的方法需要适配
public class Adaptee {
//需要适配的方法
public void specificRequest() {
}
}
将上面Adaptee转换成符合Target接口的实现类,让Adaptee实现可以用在Target所使用的地方。它在继承Adaptee的同时又扩展了Target接口,将已有的但并不符合目标接口实现转换成符合目标接口的实现。
//适配器,模式的核心
public class Adapter extends Adaptee implements Target {
//适配后的方法
public void request() {
this.specificRequest();//调用已有的实现
}
}
对象适配器结构
与类适配器模式一样,对象适配器模式把被适配的类的API转换成为目标类的API,与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是使用委派关系连接到Adaptee类。
适配器,这里不是直接继承直接源接口,而是采用组合。其他的类与类适配器模式没有区别。
public class Adapter implements Target {
//持有的待适配对象
private Adaptee _adaptee;
public Adapter(Adaptee adaptee) {
_adaptee = adaptee;
}
public void request() {//适配后的方法
_adaptee.specificRequest();//通过委派调用现有实现
}
}
最后需要的是,适配器模式的用意是将接口不同而功能相同或者“相近”的两个接口加以转换,这里面包括适配器角色可补充一个源角色没有的方法。上面的源与目标接口的功能是完全一样的,如果目标接口中要的功能接口比源要多的话,我们可以在适配源的基本之上或以补充源没有的功能,这是允许的,不要误以为源与目标接口要一一对应才能使用适配器。换言这,适配器允许转换之外还可以补充,而且,对象适配器还可以同时适配多个源,而类适配器是不易做到的。
从Iterator到Enumeration的适配
如果使用JDK中新的Iterator功能来替换掉老系统中Enumeration,则需要将Iterator适配成Enumeration。
public class Itermeration implements Enumeration{//适配器
Iterator it;
public Itermeration(Iterator it){//可将指定的Iterator转换为Enumeration
this.it = it;
}
public boolean hasMoreElements(){
return it.hasNext();
}
public Object nextElement() throws NoSuchElementException{
return it.next();
}
}
从Enumeration到Iterator的适配
反过来,可以将Enumeration到Iterator,即使用旧的迭代器功能完成新的迭代接口,好比新瓶装老洒的感觉。
public class Enuterator implements Iterator{
Enumeration enum;
public Enuterator(Enumeration enum) {
this.enum = enum;
}
public boolean hasNext(){
return enum.hasMoreElements();
}
public Object next() throws NoSuchElementException{
return enum.nextElement();
}
public void remove(){
throw new UnsupportedOperationException();
}
}
缺省适配(Default Adapter)器模式
缺省适配模式为一个接口提供缺省实现,这样子类型可以从这个缺省实现进行扩展,而不必从原有接口进行扩展。
在很多情况下,必须让一个具体类实现某一个接口,但是这个类又用不到接口所规定的所有方法。通常的处理方法是,这个具体类要实现所有的方法,那些有用的方法要有实现,那些没有用的方法也要有空的、平庸的实现。
比如java.awt.event.WindowAdapter(WindowAdapter实现了WindowListener,而WindowListener又继承自EventListener)就是一接收窗口事件的抽象适配器类,此类中的所有方法为空。此类存在的目的是方便用户从它扩展重写自己感兴趣的事件方法。
模式结构
public interface AbstractService{
void serviceOperation1();
int serviceOperation2();
String serviceOperation3();
}
缺省适配器,提供平庸的实现,用户可以从它进行扩展,只需提供自己感兴趣的方法实现。
public abstract class ServiceAdapter implements AbstractService{
public void serviceOperation1(){ }
public int serviceOperation2(){ return 0; }
public String serviceOperation3(){ return null; }
}
与适配器模式区别
适配器模式的用意是要改变源的接口,以便与目标类接口兼容。而缺省适配器的用意是为了方便建立一个不平庸的适配器类而提供的一种平庸实现,此类应当是抽象类。
J2EE中的缺省适配模式
合成(Composite)模式
合成模式将对象组织到树结构中,可以用来描述整体与部分的关系。可以使客户端将单纯元素与复合元素同等看待。
根据在哪里提供子对象的管理方法,合成模式可要成安全方式和透明方式。
透明方式
在Component里面声明所有的用来管理子类对象的方法,包括add()、remove(),以及getChild()方法。这样做的好处是所有的构件类都有相同的接口。在客户端看来,树叶类对象与合成类对象的区别起码在接口层次上消失了,客户端可以同等地对待所有的对象。这就是透明形式的合成模式。
缺点是不够安全,因为树叶类对象和合成类对象在本质上是有区别的。树叶类对象不可能有下一个层次的对象,因此add()、remove()以及getChild()方法没有意义,但是在编译时期不会出错,而只会在运行时期才会出错。
安全方式
节点管理方法从Component移到了Composite类里面。这样的做法 是安全的做法,因为树叶类型的对象根本就没有管理子类对象的方法,因此,如果客户端对树叶类对象使用这些方法时,程序会在编译时期出错。编译通不过,就不会出现运行时期错误。
这个选择的缺点是不够透明,因为树叶类和合成类将具有不同的接口。
安全方式的合成模式结构
public interface Component {//抽象构件
//返回自身:如果是复合节点返回自身,如果是叶子节点返回null
Composite getComposite();
//简单的业务
void sampleOperation();
}
public class Composite implements Component {//复合节点
private Vector componentVector = new Vector();
public Composite getComposite() {
return this;//如果是复合节点返回自身
}
public void add(Component component) {//添加子节点
componentVector.addElement(component);
}
public void remove(Component component) {//删除子节点
componentVector.removeElement(component);
}
public Enumeration getChild() {//返回所有孩子节点
return componentVector.elements();
}
//客户端不应当直接调用树叶类,应当由其父类向树叶类进行委派,
//这样可以增加代码的复用性
public void sampleOperation() {//调用所节点上的业务方法
Enumeration enumeration = getChild();
while (enumeration.hasMoreElements()) {
((Component) enumeration.nextElement()).sampleOperation();
}
}
}
public class Leaf implements Component {//叶子节点
public Composite getComposite(){
return null;//如果是叶子节点返回null
}
public void sampleOperation(){
System.out.println("Leaf");
}
}
public class Client {//场景
public static void main(String[] args) {
Component c = new Composite();
c.getComposite().add(new Leaf());
c.getComposite().add(new Leaf());
Component c2 = new Composite();
c2.getComposite().add(new Leaf());
c.getComposite().add(c2);
c.sampleOperation();
}
}
其实我们还可以在每个节点上定义一个父节点的引用,这样可以搜索出子节点的父节点,从而在遍历时可以回溯。
透明方式的合成模式结构
public interface Component {
void sampleOperation();
Composite getComposite();
void add(Component component);
void remove(Component component);
Enumeration components();
}
public class Composite implements Component {
private Vector componentVector = new Vector();
public void add(Component component) {
componentVector.addElement(component);
}
public void remove(Component component) {
componentVector.removeElement(component);
}
public Enumeration components() {
return componentVector.elements();
}
public Composite getComposite() {
return this;
}
public void sampleOperation() {
java.util.Enumeration enumeration = components();
while (enumeration.hasMoreElements()) {
((Component) enumeration.nextElement()).sampleOperation();
}
}
}
public class Leaf implements Component {
public void sampleOperation() {
System.out.println("Leaf");
}
public void add(Component component) {}
public void remove(Component component) {}
public Composite getComposite() {
return null;
}
public Enumeration components() {
return null;
}
}
AWT库中的例子
AWT与Swing图形界面构件是建立在AWT库中的Container类和Component类上的,从下面的AWT合成模式类图可以看出,Button和CheckBox是树的叶子节点,而Container则是复合节点。在Container类中有操作集合的对应方法,而Component类中则没有这样的方法,AWT是安全形式的合成模式。
应用场景
需要描述对象的部分和整体的等级结构。
希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
装饰(Decorator)模式
装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。
GoF:动态地给一个对象添加一些额外的职责。
模式结构
//一个可以扩展功能的对象接口
public interface Component {
//可扩展的某种操作
public void operation();
}
//实现基本功能的具体类,即将要接受附加职责的类,它是构造修饰链的起点
public class ConcreteComponent implements Component {
public void operation() {
System.err.println("ConcreteComponent");
}
}
//装饰者,持有一个Component对象的实例
public abstract class Decorator implements Component {
//被修饰的Component对象
private Component comp;
public Decorator(Component myC) {
comp = myC;
}
public void operation() {
if (comp != null)
comp.operation();
}
}
//具体修饰者,给被修饰对象加上A的职责
public class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component comp) {
super(comp);
}
public void operation() {
//添加额外操作
System.out.println("ConcreteDecoratorA");
//再运动修饰对象本身的操作
super.operation();
}
}
//具体修饰者,给被修饰对象加上B的职责
public class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component comp) {
super(comp);
}
public void operation() {
//添加额外操作
System.out.println("ConcreteDecoratorB");
//再运动修饰对象本身的操作
super.operation();
}
}
public class Client {//场景
public static void main(String[] args) {
//最里面的是具体的基本功能类,并作为修饰链的起点
Component comp = new ConcreteDecoratorA(new ConcreteDecoratorB(
new ConcreteComponent()));
comp.operation();
}
}
注意:对象链总是以ConcreteComponent对象结束。
应用场景
允许扩展一个对象的功能,而不必借助于继承。
动态地给一个对象添加一些额外的职责,这些功能可以再动态地撤销。
需要增加由一些基本功能的排列组合而产生的非常大的功能,而使继承关系变得不现实。
半透明的装饰模式
透明的(理想的)装饰模式在对被装饰对象进行功能增强的同时,要求具体构件角色、装饰角色的接口与抽象构件角色的接口完全一致。而适配器模式则不然,一般而言,适配器模式并不要求对源对象的功能进行增强,但是会改变源对象的接口,以便和目标接口相符合。但是,装饰模式有透明的和半透明的两种。这两种的区别就在于装饰角色的接口与抽象构件角色的接口是否完全一致。
如果装饰角色的接口与抽象构件角色接口不一致,也就是说装饰角色的接口比抽象构件角色的接口宽的话,装饰角色实际上已经成了一个适配器角色,这种装饰模式在本书也是可以接受的,称为“半透明”的装饰模式,如下图所示。
I/O中的设计模式
Java I/O库的两个对称性
输出—输入对称:比如InputStream和OutputStream各自占据Byte流的输入与输出的两个平行的等级结构的根部;而Reader和Writer各自占据Char流的输入与输出的两个平行的等级结构的根部。
byte-char对称:InputStream与Reader的子类分别负责Byte和Char流的输入;OutputStream与Writer的子类分别负责Byte和Char流的输出,它们分别形成平行的等级结构。
Java I/O库的两个设计模式
Java I/O库的总体设计是符合装饰模式和适配器模式的。
适配器模式应用到了原始流处理器的设计上面,构成了I/O库所有流处理器的起点。
I/O中的装饰模式抽象结构图:
为什么要在Java I/O库中使用装饰模式
由于Java I/O库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一个类,这样就会造成大量的类出现。而如果采用装饰模式,那么类的数目就会人大减少。
Java I/O库是由—些基本的原始流处理器和围绕它们的装饰流处理器所组成。
从byte流到char流的适配
InputStreamReader(InputStream in)是从byte输入流到char输入流的一个适配器。
OutputStreamWriter(OutputStream out)是从OutputStream到Writer的一个适配器。
InputStrearn
根据输入流的源的类型,可以将这些流类分成两种,即原始流和链接流。
原始流处理器
ByteArrayInputStream(byte[] buf)
FileInputStream(File file)
PipedInputStream(PipedOutputStream src)
StringBufferInputStream(String s)
注,已过时,已经由StringReader所代替。
链接流处理器
l FilterlnputStream:称为过滤输入流,它将另一个输入流作为流源。这个类的子类包括以下几种:BufferedInputStream(InputStream in)、DataInputStream(InputStream in)、LineNumberInputStream(InputStream in)、PushbackInputStream(InputStream in)。
l ObjectlnputStream可以将使用ObjectOutputStream串行化的原始数据类型和对象重新并行化。
l SeqcueneInputStream可以将两个已有的输入流连接起来,形成一个输入流,从而将多个输入流排列构成—个输入流序列。
值得指出的是,虽然PipedlnputStream接收一个流对象PipedOuputStream作为流的源,但是,PipedOutputStream流对象的类型不是InputStream。因此,PipedInputStream流处理器仍然属于原始流处理器。
适配器模式
ByleArraylnpuCStream将—个byte数组的接口适配成InputStream流处理器的接口。
FileInputStream将PileDescdptor对象适配成InputStream类型的对象形式的适配器模式。
StringBufferInputStream将String对象适配成InputStream类型的对象形式的适配器模式。
装饰模式各角色
链接流其实就是装饰角色,而原始流就是具体构件角色,如下图所示(底色为灰色的表示是链接流):
OutputStrearn
原始流处理器
ByteArrayOutputStream(int size)
FileOutputStream(File file)
PipedOutputStream(PipedInputStream snk)
链接流处理器
l FilterOutputStream:称为过滤输出流,它将另一个输出流作为流汇,这个类的子类有如下几种:BufferedOutputStream(OutputStream out)、DataOutputStream(OutputStream out)、PrintStream(OutputStream out)。
l ObjectOutputStream:可以将原始数据类型和对象串行化。
适配器模式
ByteArrayOutputStream把一个byte数组的接口适配成OutputStream类型的接口。
FileOutputStream将File接口适配成OutputStream接口的对象形式的适配器模式。
PipedOutputStream接收一个类型为PipedlnputStream的输入类型,并将之转换成类型为OutputStream类型的输出流。
装饰模式各角色
Reader
原始流处理器
CharArrayReader(char[] buf)
InputStreamReader(InputStream in)
PipedReader(PipedWriter src)
StringReader(String s)
链接流处理器
链接流处理器包括以下的几种:BufferedReader(Reader in)、FilterReader(Reader in)
适配器模式
CharArrayReader将—个char数组适配成为Reader类型的输入流。
StringReader将String的接口适配成Reader类型的接口。
装饰模式各角色
Writer
原始流处理器
CharArrayWriter(int initialSize)
OutputStreamWriter(OutputStream out)
PipedWriter(PipedReader snk)
StringWriter(int initialSize)
链接流处理器
BufferedWriter(Writer out)
FilterWriter(Writer out)
PrintWriter(File file)
适配器模式
CharArrayWriter将—个char数组适配成为Writer接口。
PipedWriter将一个PipedReader对象的接口适配成一个Writer类型的接口。
StringWriter将StringBuffer对象的接口适配成为了Writer类型的接口。
装饰模式各角色
由于抽象装饰角色与具体装饰角色发生合并,因此装饰模式在这里被简化了。
I/O中的适配器类
InputStreamReader:将InputStream适配成Reader类,可以指定编码方式
OutputStreamWriter:将OutputStream适配成Writer类,可以指定编码方式
PrintWriter:将OutputStream适配成Writer类。还可以以File、Writer来构造。
从I/O来看,也只有InputStreamReader、OutputStreamWriter转换流才涉及到编码方式。
附:涉及到编码的流
InputStreamReader(InputStream in, String charsetName)
OutputStreamWriter(OutputStream out, String charsetName)
PrintStream(OutputStream out, boolean autoFlush, String encoding)
PrintWriter(String fileName, String csn)
代理(Proxy)模式
为其他对象提供一种代理以控制对这个对象的访问。
模式结构
抽象主题,即代理接口,即被代理的对象(真实现主题)都一定要实现这个接口,这允许任何客户都可以像处理RealSubject对象一样地处理Proxy对象。
abstract public class Subject {//抽象主题
abstract public void request();
}
真实主题,即真真被代理的类。通常是真真做事的对象,Proxy会控制对RealSubject的访问。
public class RealSubject extends Subject {//真实主题
public void request() {
System.out.println("From real subject.");
}
}
代理角色,对真实主题的替代,在内部含有对真实主题的引用,可以控制对它的引用。代理角色通常在将客户端调用传递给真实的主题之前或之后,都要执行某个操作,而不是单纯地将调用传递给真实主题对象。
public class Proxy extends Subject {
private RealSubject realSubject;
public void request() {
preRequest();//之前额外操作
if (realSubject == null) {
realSubject = new RealSubject();
}
realSubject.request();
postRequest();//之后额外操作
}
private void preRequest() {
//something you want to do before requesting
}
private void postRequest() {
//something you want to do after requesting
}
}
public class Client {
private static Subject subject;
static public void main(String[] args) {
subject = new Proxy();
subject.request();
}
}
代理模式是如何工作的?首先,代理主题并不改变主题的接口,因为模式的用意是不让客户端感觉到代理的存在;其次,代理使用委派将客户端的调用委派给真实的主题对象,换言这,代理主题起到的是一个传递请求的作用;最后,代理主题在传递请求之前和之后都可以执行特定的操作,而不是单纯传递请求。
Java动态代理结构
// 代理接口,Java中的动态代理对象一类要实现某个接口
interface Subject {
void request();
}
// 真真被代`理的类
class RealSubject implements Subject {
public void request() {
System.out.println("request");
}
}
//实现调用处理器接口
class MyInvocationHandler implements InvocationHandler {
private Object proxied;// 引用被代理的真真对象
public MyInvocationHandler(Object proxied) {
this.proxied = proxied;
}
// 实现接口,动态代理可以将所有调用重定向到该方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在这里做额外的工作
System.out.println("Proxy detected the interesting method");
return method.invoke(proxied, args);
}
}
class Client {
public static void main(String[] args) {
// 创建代理对象 第一个参数为类加载器;第二个为所实现的接口,可有多个;第
// 三个为处理器,构建处理器时需指定真真被代理的对象。返回的是代理对象
Subject proxy = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(),
new Class[] { Subject.class }, new MyInvocationHandler(new RealSubject()));
// 通过代理对象调用
proxy.request();
}
}
new Class[] { Subject.class } 等效于 new RealSubject().getClass().getInterfaces()
注,如果使用反射创建一个对象时,而需要调用带参的构造函数,则可以使用Constructor的newInstance(Object[] initargs)方法来代替Class的newInstance()方法。
示例:Vector动态代理
使用动态代理来代理Vector对象,对Vector的add方法进行控制。
public class VectorProxy implements InvocationHandler {
private Object proxyobj;
public VectorProxy(Object obj) {
proxyobj = obj;
}
public static Object factory(Object obj) {
Class cls = obj.getClass();
// cls.getInterfaces()获取代理对象的所有已实现的接口,这样便于代理对象向下转型成各种接口类型
return Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), new VectorProxy(obj));
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before calling " + method);
if (args != null) {
for (int i = 0; i < args.length; i++) {
System.out.println(args[i] + "");
}
}
Object o = method.invoke(proxyobj, args);
System.out.println("after calling " + method);
return o;
}
public static void main(String[] args) {
List v = null;
v = (List) factory(new Vector(10));
v.add("New");
v.add("York");
}
}
代理种类
代理各类有很多,常用有以下四种:
远程代理:为一个对象在不同的地址空间提供局部代表。优点是系统可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户完全可以认为被代理的对象是局域的而不是远程的,而代理对象承担了大部分的网络通信工作。
虚拟代理:根据需要创建一个资源消耗较大的对象,使得此对象只在需要时才会被真正创建。
保护代理:控制一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。
智能引用代理:当一个对象被引用时,提供一些额外的操作,比如将对此对象调用的次数记录下来等。
享元(Flyweight)模式
运用共享技术有效地支持大量细粒度的对象。
享元对象能做到共享的关键是区分内蕴状态和外蕴状态。一个内蕴状态是存储在享元对象内部的,并且是不会随环境改变而改变,在对象创建后不会再改变,因此,一个享元可以具有内蕴状态并可能共享;一个外蕴状态是随环境改变而改变的、不可以共享的状态。享元对象的外蕴状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象的内部。外蕴状态不可以影响享元对象的内蕴状态,它们是相互独立。
享元模式可以分成单纯享元模式和复合享元模式两种形式。
模式的应用
享元模式在编辑器系统中大量使用,一个文本编辑器往往会提供很多种字体,而通常的做法就是将侮一个字母做成一个享元对象,享元对象的内蕴状态就是这个字母。而字母在文本中的位置和字母风格等其他信息则是外蕴状态,比如.字母a可能出现在文本的很多地方。虽然这些字母a的位置和字母风格不同. 但是所有这些地方使用的都是同一个字母对象。这样一来,字母对象就可以在整个系统*享。
在Java,String类型就便用了享元摸式。String对象是不变对象,一旦创建出来就不能改变,如果需要改变一个字符串的值,就只好创建一个新的String对象,在JVM内部,String对象都是共享的。如果一个系统中有两个String对象所包含的字符串相同的话,JVM实际上只创建一个String对象提供给两个引用,从而实现String对象的共享,String的intern()方法给出这个字符串在共享池中的惟一实例。
单纯享元模式的结构
abstract public class Flyweight {//抽象享元
//参数state为外蕴状态
abstract public void operation(String state);
}
享元对象的内蕴状态必须存储在对象内部,且与周围环境无关,从而使得对象可以在系统内共享。所有的内蕴状态在对象创建之后,就不会再改变了。如果一个享元对象有外蕴状态的话,所有的外部状态都必须存储在客户端,在使用享元对象时,再由客户端传入。
public class ConcreteFlyweight extends Flyweight {//具体享元角色
//内蕴状态,一旦构造好后不能再修改
private Character intrinsicState = null;
public ConcreteFlyweight(Character state) {
this.intrinsicState = state;
}
//外蕴状态作为参数传入方法中,改变方法的行为,但并不改变对象的内蕴状态
public void operation(String state) {
System.out.print("\nIntrinsic State = " + intrinsicState
+ ", Extrinsic State = " + state);
}
}
必须指出的是,客户端不可以直接将具体享元类实例化,而必须通过一个工厂对象,利用一个factory方法得到享元对象,一般可用单例模式,因为整个系统中只有一个。
public class FlyweightFactory {//享元工厂
private HashMap flies = new HashMap();//缓存所有可共享的享元对象
private static FlyweightFactory flyf = new FlyweightFactory();
//单例
private FlyweightFactory() {}
public static FlyweightFactory getInstance() {
return flyf;
}
//享元对象的工厂方法
public Flyweight factory(Character state) {
//如果存在某种内蕴状态的对象,则直接返还
if (flies.containsKey(state)) {
return (Flyweight) flies.get(state);
} else {//如果不存在某种内蕴状态的对象,则新建并缓存
Flyweight fly = new ConcreteFlyweight(state);
flies.put(state, fly);
return fly;
}
}
}
需要自己存储所有享元对象的外蕴状态。
public class Client {//场景
static public void main(String[] args) {
FlyweightFactory factory = FlyweightFactory.getInstance();
Flyweight fly = factory.factory(new Character('a'));
//调用享元对象方法时,外蕴状态由客户端自己保存,并在使用进传入
fly.operation("First Call");
fly = factory.factory(new Character('b'));
fly.operation("Second Call");
//享元对象已存在,所以不会产生新的享元对象
fly = factory.factory(new Character('a'));
fly.operation("Third Call");
}
}
复合享元模式的结构
单纯享元对象是可以直接共享的,但复合享对象本身不能被共享的,因为它的内蕴状态就是组成它单纯享元对象集合,而集合是变化的。但是它们可以分解成单纯享元对象后通过共享单纯享元对象来达到共享的目的。
abstract public class Flyweight {//抽象享元
abstract public void operation(String state);
}
public class ConcreteFlyweight extends Flyweight {//具体单纯享元
//内蕴状态,一旦初始后不能再修改
private Character intrinsicState = null;
public ConcreteFlyweight(Character state) {
this.intrinsicState = state;
}
//外蕴状态作为参数传入,但不改内蕴状态
public void operation(String state) {
System.out.print("\nInternal State = " + intrinsicState
+ " Extrinsic State = " + state);
}
}
复合享元对象两个特点:
l 复合享元对象是由单纯的享元对象通过复合而成,因此它提供了add这样的聚集管理方法。由于一个复合享元时象具有不同的聚集元素,这些聚集元素在复合享元对象被创建之后才加入,这本身就意味着复合享元对象的状态是会改变的,因此复合享元对象是不能共享的。
l 复合享元角色实现了抽象享元角色所规定的接口也就是。也就是operation()方法,这个 方法有一个参量,代表复合拿元对象的外蕴状态,一个复合享元对象的所有单纯享元对象元素的外蕴状态都是与复合享元对象的外蕴状态相等的,但一个复合享元对象所含有的单纯享元对象的内蕴状态一般是不相等的,不然就没有使用价值了。
public class ConcreteCompositeFlyweight extends Flyweight {//具体复合享元
/*
* flies为内蕴状态,不过它是变化的,因为它依赖于组成复合享
* 元对象的单纯享元对象的状态,所以随时是变化的。因 此,复合
* 享元元素是不能共享的。
*/
private HashMap flies = new HashMap(10);
public void add(Character key, Flyweight fly) {
flies.put(key, fly);
}
/**
* @param extrinsicState 复合享元对象的外蕴状态,它也是所有
* 组成它的单纯享元对象的外蕴状态,即所有单纯享元对象的外蕴状
* 态是相等的,但内蕴状态是不相同的,这已单纯享元对象在被创建
* 后就已确定。
*/
public void operation(String extrinsicState) {
Flyweight fly = null;
for (Iterator it = flies.entrySet().iterator(); it.hasNext();) {
Map.Entry e = (Map.Entry) it.next();
fly = (Flyweight) e.getValue();
fly.operation(extrinsicState);
}
}
}
public class FlyweightFactory {//享元工厂
private static FlyweightFactory flf = new FlyweightFactory();
//缓存所有可共享的单纯享元对象
private HashMap flies = new HashMap();
private FlyweightFactory() {}
public static FlyweightFactory getInstance() {
return flf;
}
/*
* 复合享元工厂方法,所需状态以参量形式传入,这里的这个参数恰好
* 可以使用String类型。如果复合享元所包括的单纯享元对象的内蕴状
* 态不是char时,则读者完全可以使用一个聚集,如Vector对象来传递
* 这些复合状态。
*/
public synchronized Flyweight factory(String complexState) {
/*
* 因为复合享元对象的内蕴状态是变化的,所以不能共享,此因此
* 这里只要调用一次factory(String complexState)工厂方法,
* 就会创建出一个新的复合享元对象,而不是单纯享元对象工厂方
* 法那样,先判断要创建的享元对象是否存在,只有在不存在的情况
* 下才创建一个新的单纯享元对象。
*/
ConcreteCompositeFlyweight complexFly = new ConcreteCompositeFlyweight();
int length = complexState.length();
Character state = null;
//将复合状态分解成多个单纯状态
for (int i = 0; i < length; i++) {
state = new Character(complexState.charAt(i));
System.out.println("factory(" + state + ")");
//复合享元对象由多个单纯享元对象集合而成,但不能重复
complexFly.add(state, this.factory(state));
}
return complexFly;
}
//单纯享元对象工厂方法
public Flyweight factory(Character state) {
if (flies.containsKey(state)) {
return (Flyweight) flies.get(state);
} else {
Flyweight fly = new ConcreteFlyweight(state);
flies.put(state, fly);
return fly;
}
}
//辅助方法,查看所有已创建的可共享的单纯享元对象
public void checkFlyweight() {
Flyweight fly;
int i = 0;
System.out.println("\n==========checkFlyweight()=============");
for (Iterator it = flies.entrySet().iterator(); it.hasNext();) {
Map.Entry e = (Map.Entry) it.next();
System.out.println("Item " + (++i) + " : " + e.getKey());
}
System.out.println("==========checkFlyweight()=============");
}
}
public class Client {//场景类
public static void main(String[] args) {
FlyweightFactory factory = FlyweightFactory.getInstance();
//创建复合享元对象,这里会创建3个单纯享元对象
Flyweight fly = factory.factory("abca");
//"Composite Call"为外蕴状态
fly.operation("Composite Call");
// 查看所有已创建的单纯享元对象
factory.checkFlyweight();
}
}
使用不变对象实现享元角色
享元模式里的享元对象不一定非得是不变对象,但是很多的享元对象确实被设计成了不变对象。不变对象的状态在被创建之后就不同再变化,因此不变对象满足享元模式对享元对象的要求。
应用场景
1、 一个系统有大量的对象。
2、 这些对象消耗大量的内存。
3、 这些对象的状态中的大部分都可以外部化。
4、 这些对象可以按照内蕴状态分成很多的组,当把外蕴对象从对象中剔除时,每一个组都可以仅用一个对象代替。
5、 软件系统不依赖于这些对象的身份,换言这,这些对象可以是不可分辨的。
最后,使用享元模式需要维护一个记录了系统已有的所有享元的表,而这需要耗费资源,因此,应当在有足够多的享元实例可供共享时才值得使用享元模式。
门面(Facade)模式
为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
原来需要调用多个类才能完成一件事。
现在只需要调用一个类就能完成。
结构示意图
门面模式没有一个一般化的类图描述,最好的描述方法实际上就是以一个例子说明。下面就是一个例子。
public class SubSystem1 {//子系统类1
public void operation(){
System.out.println("SubSystem1");
}
}
public class SubSystem2 {//子系统类2
public void operation(){
System.out.println("SubSystem2");
}
}
门面类实例一般只有一个,所以适合设计成单例。另外,不要通过给门面类来新增功能而对子系统进行功能扩充,这是错误的,门面模式的用意是简化子系统的调用,它只是去调用子系统现有功能实现。
public class Facade {//门面角色
private static Facade facade = new Facade();
private SubSystem1 s1 = new SubSystem1();
private SubSystem2 s2 = new SubSystem2();
public static Facade getInstance(){
return facade;
}
//把子系统中的动作打包
public void operationWrapper() {
s1.operation();
s2.operation();
}
}
public class Client {//场景
public static void main(String[] args) {
Facade f = Facade.getInstance();
f.operationWrapper();
}
}
门面模式符合迪米特法则。
应用场景
当你要为一个复杂子系统提供一个简单接口时。
客户程序与抽象类的实现部分之间存在着很大的依赖性。
当你需要构建一个层次结构的子系统时,使用此模式定义子系统中每层的入口点(比如WEB应用,层与层之间可以使门面,减少耦合)。
桥梁(Bridge)模式
将抽象部分与它的实现部分解耦,使它们都可以独立地变化。
可用来解决继承的不灵活的问题。
强关联,就是在编译时期已经确定的,无法在运行时间动态改变的关联;所谓弱关联,就是可以动态地确定并且可以在运行时期动态地改变的关联。继承是强关联,聚合是弱关联。
桥梁模式就是将继承变向地转换为组合关系,通过组合解耦了两种变化因素。
使用桥梁模式的关键在于准确地找出系统的抽象化角色和具体化角色。
模式结构
//对抽象做出定义,并定义实现的引用
public abstract class Abstraction {//抽象的抽象
//实现
private Implementor imp = null;
//动态地改变实现
public Abstraction(Implementor imp) {
this.imp = imp;
}
protected void print(String str) {
imp.operationImp(str);
}
//抽象层操作,由修正抽象去实现
public abstract void operation();
}
//扩展抽象类,改变和修正父类对象的定义
public class RefinedAbstraction extends Abstraction {//修正抽象
public RefinedAbstraction(Implementor imp) {
super(imp);
}
public void operation() {
super.print("RefinedAbstraction");
}
}
这个角色给出实现化角色接口,但不给出具体的实现,必须指出的是,这个接口不一定和抽象化角色的接口定义相同,实际上,这两个接口可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作。
public interface Implementor {//实现的抽象
//底层操作实现方法
void operationImp(String inputText);
}
public class ConcreteImplementorA implements Implementor {//具体实现A
public void operationImp(String inputText) {
System.out.println("A:"+inputText);
}
}
public class ConcreteImplementorB implements Implementor {//具体实现B
public void operationImp(String inputText) {
System.out.println("B:"+inputText);
}
}
public class Client {//场景
public static void main(String[] args) {
Implementor imp = new ConcreteImplementorA();
Abstraction abs = new RefinedAbstraction(imp);
abs.operation();
Implementor imp2 = new ConcreteImplementorB();
Abstraction abs2 = new RefinedAbstraction(imp2);
abs2.operation();
}
}
一般而言,实现化角色中的每一个方法都应当有一个抽象化角色中的某一个方法与之相对应,但是,反过来则不一定。换言之,抽象化角色的接口比实现化角色的接口宽。抽象化角色除了提供与实现化角色相关的方法之外,还有可能提供其他的商业方法;而实现化角色则往往仅为实现抽象化角色的相关行为而存在口。
示例:飞机与制造商
空中巴士(Airbus)、波音(Boeing)和麦道(McDonnell-Douglas)都是飞机制造商,它们都生产载客飞机(Passenger Plain)和载货飞机(Cargo Plane)。现在需要设计一个系统,描述这些飞机制造商以及它们所制造的飞机种类。
第一种方案:系统是关于飞机的,因此可以设计一个总的飞机接口,叫Airplane。其他所有的飞机都是这个总接口的子接口或都具体实现。
上面方案中,每个具体飞机都带有两个超类型:制造商类型、客货机类型,上面有3种制造商,2种客货机类型,这样共有3*2=6种具体飞机。现在如果再增加一个制造商与客货机类型,则会导致类增加到4*3=12,这是成几何倍数增加,会导致类爆炸。
第二种设计方案:使用桥梁模式。
使用桥梁模式的关键在于准确地找出这个系统的抽象化角色和具体化角色。从系统所面对的问题不难看出,代表飞机的抽象化的是它的类型,也就是“客机”或“货机”;而代表飞机的实现化的则是飞机制造商。将载客飞机(Passenger Plain)和载货飞机(Cargo Plane)抽象成Airplane接口,即飞机接口,而产商抽象成AirplaneMaker接口。
现在,我们不需要组装各种类型的飞机了,而是共用户在使用时自行组合就可以了,而不用向上面那样你得要先给它们组装好(因为继承是静态的,组合是动态的,所以上面需要在运行前就将飞机种类确定下来,而组合则不需要,到用时用户可以*地组合)。如果需要增加亲的飞机制造商,或者新的飞机种类的话,只需要向系统引进一个新的修正抽象化角色,或者一个新的具体实现化角色就可以了,或者说,系统的功能可以在不修改已有代码的情况下得到扩展,这完全满足了“开-闭”原则。
对变化的封装
“找到系统的可变因素,将它封装起来”,就叫做“对变化的封装”。抽象化与实现化的最简单实现,也就是“开-闭”原则在类层次上的最简单实现,如下图:
一般来说,一个继承结构中的第一层是抽象层,封装了抽象的业务逻辑,这是系统中不变的部分。第二层是实现层,封装了设计中会变化的因素,这个实现允许实现化角色有多态性变化。
换言这,客户端可以持有抽象化类型的对象,不在意对象的真实类型是“实现化”,还是“实现化2”、 “实现化3”等。
显然每一个继承关系都封装了一个变化因素,而一个继承关系不能同时处理两个变化因素。换言这,这种简单实现不能名处理抽象化与实现化都面临变化的情况:
上图中的两个变化因素应当是彼此独立的,可以在不影响另一者的情况下独产演化,比如,下面的两个等级结构分别封装了自己的变化因素,由于每一个变化因素都是可以通过静态关系表达的,因此分别使用继承关系实现:
从另一个角度讲,一个好的设计通常没有多于两层的继承等级结构,或者说,如果出现两个以上的变化因素,就需要找出哪一个因素是静态的,可以使用继承关系,哪一个是动态的,必须使用聚合关系。
桥梁模式是“对变化的封装”原则以及组合/聚合复用原则的极好例子。在前面飞机制造系统中。飞机的种类和制造商代表两个不同的变化因素。而这两个变化因素需要独立地变化。按照对变化的封替原刚,它们应当被封装到继承的等级结构中。而两个等级结构之间应当选择便用聚合关系.邀免出现静态的强耦合关系,这就导致了桥梁核式的设计方案。
应用场景
如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的联系。
设计要求实现化角色的任何改变不应当影响客户端,或者说实现化角色的改变对客户端是完全透明的。
一个构件有多于一个的抽象化角色和实现化角色,系统需要它们之间进行动态耦合。
虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化。
一个抽象类的派生类必须使用多种实现部分,但又不能引起类数量的爆炸。