二十三个设计模式
模式的四个基本要素:
(1)模式名称(Pattern Name)
(2)问题 Problem
(3)解决方案Solution
(4)效果(Consequences)
使用场景:在编码的时候不能预见需要创建哪种类的实例;
系统不应该依赖于产品类实例如何被创建、组合和表达的细节;
工厂模式主要是为了创建对象提供了接口,一般来说,分为3类:
客户端通过简单工厂创建了一个实现了接口的对象,然后通过接口编程,从客户端来看,它根本就不知道具体的实现是什么,也不知道是如何实现的,它只知道通过工厂获得了一个接口对象,然后就能够通过这个接口来获取到想要的功能。事实上,简单工厂能够帮助我们真正的开始面向接口编程,像以前的做法,其实只是用到了接口的多态的那部分的功能,最重要的“封装隔离性”并没有体现出来;
对于客户端来讲,只是知道了接口API和简单工厂Factory,通过Factory就可以获得API了,这样实现了让Client在不知道具体实现类的情况下可以获取Api接口。
简单工厂方法的内部主要实现的功能就是:选择合适的接口实现类来创建实例对象。
既然要实现选择,选择条件或者是选择的参数一般如下:
(1)来源于客户端:由Client来传入参数;
(2)来源于配置文件:从配置文件获取用于判断的值;
(3)来源于程序运行期的某个值:比如从缓存中获取某个运行期的值;
问题讨论:如果每次增加一个实现类,都来修改工厂类的实现,这肯定不是一个好的实现方式。
解决方法:通常使用配置文件来解决这个问题,当有了新的实现类的时候,只要在配置文件中配置新的实现类就可以了,在简单工厂的方法里面可以使用反射,当然也可以使用IOC/DI(控制反转/依赖注入)来实现;
读取配置文件方式:
例如配置文件中:ImplClass=com.lei.Impl
则:
Properties p = new Properties();
InputStream in = 类名.class.getResourceAsStream(文件名.properties);
p.load(in);
//使用反射机制来创建具体的实现类
Class.forName(p.getProperty(“ImplClass”)).newInstance();
简单工厂的优点:
(1)帮助封装:然组件外部能够真正的面向接口编程;
(2)解耦:实现了客户端和具体实现类的解耦;
缺点:
(1)可能增加客户端的复杂度,
(2)不方便扩展子工厂;
简单工厂的本质:选择实现;
使用场景:
(1)如果想要完全的封装隔离具体的实现,让外部只能通过接口来操作封装体,那么就可以选择简单工厂,让客户端通过工厂来获取到相应的接口,而无需关心具体的实现;
(2)如果想要把对外创建对象的职责集中管理和控制,可以选择简单工厂 ,一个简单工厂可以创建很多的、不相关的对象;
抽象工厂模式和工厂方法模式的区别在于需要创建对象的复杂度上。而且抽象工厂模式是三个里面最为抽象、最具一般性的;
抽象工厂模式的目的是给客户端提供一个接口,可以创建多个产品族中的产品对象。而且使
抽象工厂模式中的角色和工厂方法模式的完全相同,由如下4部分组成:
(1)抽象工厂角色:
(2)具体工厂角色:
(3)抽象产品角色:
(4)具体产品角色:
抽象工厂模式可以向客户端提供一个接口,使得客户端在不必指定产品具体类型的情况下,创建多个产品族的产品对象。
每个模式都是针对一定问题的解决方案。抽象工厂模式面对的问题是多产品等级结构的系统设计;
重要概念:
(1)产品族:是指位于不同产品等级结构中功能相关的产品组成的家族。比如:AMD的CPU和ADM芯片主板,组成一个家族。Intel的CPU和Intel芯片的主板,由组成一个家族;
(2)产品等级:上述两个家族都来自于两个产品等级:CPU和主板。一个等级结构是由相同结构的产品组成的。
抽象工厂模式的每个工厂创造出来的都是一族产品,而不是一个或者一组。组是可以随意组合的;
抽象工厂模式和工厂方法模式的最大的区别在于:工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构。
一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中所有对象。每一个具体工厂负责创建属于同一个产品族,但是分属于不同等级结构的产品;
【抽象工厂模式的结构】
抽象工厂模式是对象的创建模式,它是工厂方法模式的进一步推广;
抽象工厂的功能是为了一系列相关对象或者相互依赖的对象创建一个接口。
注意:该接口内的方法不是随意的堆砌的,而是一些列相关或者相互依赖的方法;
【抽象工厂的使用场景】
(1)一个系统不应当依赖与产品类实例如何被创建、组合和表达的细节,这对于所有的形态的工厂模式都是重要的;
(2)这个系统的产品有多于一个的产品族,而系统只消费其中的某一族产品;
(3)同属于一个产品族的产品是在一起使用的;
(4)系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于实现;
【抽象工厂模式的优点和缺点】
优点:
(1)分离接口和实现:客户端从具体的产品实现解耦;
(2)使得切换产品族变得容易;
缺点:
不太容易扩展新的产品,如果需要给整个产品族添加一个新的产品,那么就需要修改抽象工厂,这样就会导致修改所有的工厂实现类;
建造者模式能够将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
与抽象工厂的区别:
(1)在建造者模式中,有一个指导者,由指导者来管理建造者,用户是与指导者相互联系,指导者联系建造者,最后得到产品。即建造者模式可以强制实行一种分步骤进行的建造过程;
(2)建造者模式返回的是一个完成的复杂产品,而抽象工厂模式返回的是一系列相关的产品;
抽象工厂模式中,客户端通过选择具体工厂来生成所需的对象,而在建造者模式中,客户端通过具体建造者类型并指导Director类如何去生成对象,侧重于一步步构造一个复杂对象,然后将结果返回;
如果将抽象工厂模式看成是一个汽车配件生产厂,生成不同类型的汽车配件,那么建造者模式就是一个汽车组装厂,通过对配件进行组装,返回一辆完整的汽车;
【建造者模式和工厂模式的区别】
建造者模式与工厂模式有点类似,不过关注点不同。工厂模式往往只关心你要的是什么,而不关心这个东西的具体细节是什么。而建造者模式则关心的是这个东西的具体细节的创建。
比如创建任务,我们关心的不仅是创建一个任务,还要关心他的性别、肤色和名字,则可以使用建造者模式。
【建造者模式的使用场景】
(1)需要生成的产品有复杂的内部结构;
(2)需要生成的产品对象的属性相互依赖;
(3)在对象的创建过程中会使用到其他的对象;
【建造者模式的结构】
(1)Builder(抽象建造者):
该接口中一般有两类方法:一类方法是buildPartX(),它们用于创建复杂对象的各个部件;
另一类方法是getResult():它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口;
(2)ConcreteBuilder(具体建造者):
实现Builder接口
(3)Product(产品角色):
它是被构建的复杂对象,具体建造者创建该产品内部并定义它的装配过程;
(4)Director(指挥者):
它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其建造方法construc()中调用建造者对象的部件构造与装配方法,完成建造复杂对象的任务。
客户端一般只需要和指挥者进行交互,在客户端确定具体建造者类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者setter方法将该对象传入指挥者类中;
【复杂对象定义】
复杂对象是指那些包含多个成员属性的对象,这些成员属性也称为部件或者零件;
例如汽车:包括方向盘、发动机、轮胎等部件;
电子邮件:包括发件人、收件人、主题、内容、附件等部件;
Builder builder = new ConcreteBuilder();//具体的建造者
Director director = new Director();//指挥者
director.order(builder);//指挥者指挥具体的建造者实施建造
Product product = builder.getProduct();//产品
【对建造者模式的总结】
(1)建造者模式的优点:
1.封装性:使用建造者模式可以使客户端不必知道产品内部的细节
2.建造者独立,容易扩展:不同类型的产品建造者之间相互独立,对系统的扩展非常有利;
3.便于控制细节风险:因为具体的建造者是相互独立的,因此可以对建造过程逐步的细化,而不对其他的模块产生任何的影响;
(2)建造者模式的使用场景:
1.相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式
2.多个部件或者零件,都可以装配到一个对象中,但是产生的运行结果又不相同的时候,可以使用该模式;
3.产品类非常的复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式是非常合适的;
4.在对象创建过程中会使用到系统中的一些其他的对象,这些对象在产品对象的创建过程中不易得到的时候,也可以采用建造者模式封装该对象的创建过程。
这种场景只能是一个补偿的方法;
建造者模式关注的是零件类型和装配工艺(顺序),这是它与工厂方法模式最大不同的地方,虽然同为创建者模式,但是重点是不同的;
简单工厂模式最大的缺点就是不满足:开闭原则;因为每次增加接口的实现类的时候,需要修改工厂中的相关方法;
工厂方法模式和简单工厂模式最大的不同之处在于:简单工厂模式只有一个(对于一个项目或者一个独立的模块而言)的工厂类,而工厂方法模式有一组实现了相同接口的工厂类;
工厂方法模式是类的创建模式:又叫虚拟构造器(Virtual Constructor)模式或者多态性工厂(Polymorphic Factory)模式。其用意是定义一个创建产品对象的工厂接口,将实际工作推迟到子类中;使这种工厂方法模式可以用来允许系统在不修改具体工厂角色的情况下引进新的产品;——抽象工厂对应抽象产品,具体工厂对应具体产品;
角色分类:
(1)抽象工厂角色:
(2)具体工厂角色:
(3)抽象产品角色:
(4)具体产品角色:
通过一个原型对象,拷贝出多个一模一样的对象,这个模式称为原型模式;
原型模式(Property Pattern)是指使用原型实例来指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建模式;
【原型模式中的角色】
(1)Property(抽象原型类):
声明拷贝方法;
(2)ConcreteProperty(具体原型类):
实现在抽象原型类中声明的拷贝方法,在拷贝方法中返回自己的一个拷贝对象;
(3)Client(客户类):
让一个原型对象拷贝自身,从而创建一个新的对象。
在客户类中只需要直接实例化或者通过工厂方法等方式创建一个原型对象,再通过调用该对象的拷贝方法,即可得到多个相同的对象。
由于客户类是针对抽象原型类Property编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或者更换具体原型类都很方便;
【实现拷贝的方法】
(1)通用实现方法:
通用的拷贝的实现是在具体的原型类的自定义拷贝非返回总实例化一个与自身类型相同的对象并将其返回,并将相关的属性和参数传入到新创建的对象中,保证它们的成员属性相同;
(2)Java语言提供的clone()方法:
Object类中提供了一个clone方法可以将一个Java对象复制一份;
注意:能够实现拷贝的Java类必须实现一个标识接口Cloneable,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了clone方法,会抛出异常;
实现步骤:
1》在派生类中覆盖clone()方法,并声明为public
2》在派生类的clone()方法中,调用super.clone()
3》派生类需要实现Cloneable接口;
Java语言中的clone方法满足如下关系:
1》对于任何的对象x,都有x.clone()!=x,即拷贝对象与原型对象不是同一个对象;
2》对于任何对象x,都有x.clone().getClass()==x.getClass(),类型相同;
3》如果对象x的equals方法定义恰当,那么x.clone().equals(x)应该成立;
【原型模式浅拷贝和原型模式深度拷贝】
《浅拷贝》:
基本数据类型会被拷贝,而引用数据类型只是拷贝的地址,与原来的对象公用一个引用类型的对象;
当使用Object类中的clone方法时,只有满足如下两个条件的对象才不会被拷贝:
(1)类的成员变量,而不是方法内的变量;
(2)必须是一个对象,而不是一个原始类型;
《深拷贝》:
被拷贝对象的所有的基本类型的变量和原对象的值相同,对引用类型的变量自己也重新拥有了一份拷贝,与原对象的引用类型变量不再有联系;
深度拷贝的实现方式:
方式一:让引用类型的变量继续调用clone方法进行拷贝,然后将拷贝的值赋值给已经clone的类对象的属性即可;
方式二:可以通过写二进制流来操作对象,然后实现对象的深度拷贝;
注意:并不是所有的对象都能够实现深度克隆的,例如StringBuffer没有重载Clone方法,而写StringBuffer类还是一个final类,所以不能自动的实现深度克隆;
在Spring中,深度克隆的应用非常广泛,在实体模型上面有这样一个标注:
例如:
@Component(“lawsuitArchive”) @scope(“prototype”)
表明在别的地方引用的时候,是引用其原型,同时Entity也要实现Cloneable接口和复写clone方法。因为原型在初始化的时候就已经建立了一部分有价值的东西,减少了容器的压力,当我们在例如action中引用该对象的时候,直接可以使用@Resource(name=” lawsuitArchive”)引用对象,Spring会自动的装配好我们想要的对象;
【使用序列化的方式实现深度拷贝】
在Java语言中,如果需要实现深度拷贝,可以通过序列化(Serialization)等方式来实现。序列化就是将对象写到流的过程,写到流中的对象那个是原对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此可以通过序列化将对象写入到一个流中,再从流中将其读取出来,可以实现深度拷贝。
注意:能够实现序列化的对象必须实现Serializable接口,否则无法实现序列化操作;
//将对象写入流中
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(this);
//将对象从流中读出
ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
【原型模式的总结】
【优点】:
(1)性能优良:
原型模式是内存中的二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量的对象的时候,原型模式可以更好的体现其优点;
(2)逃避构造函数的约束(既是优点也是缺点)
直接在内存中拷贝,构造函数是不会被执行的;
【原型模式的使用场景】
(1)类的初始化会消耗非常多的资源(数据、硬件资源等);
(2)性能和安全要求的场景:
通过new产生一个对象需要非常频繁的数据准备或者访问权限,则可以使用原型模式;
(3)一个对象多个修改者的场景;
Obeject类的clone方法的原理是从内存中(具体的说是堆内存)以二进制流的方式拷贝,重新分配一个内存块,那构造函数没有被执行是很正常的;
单例模式的功能时候保证这个类在运行期间只会被创建一个单实例;
【单例模式的范围】
目前Java里面实现的单例是一个ClassLoader以及其子ClassLoader的范围。
这就说明如果一个虚拟机中有多个ClassLoader,而且这些ClassLoader都装载某个类的话,就算这个类使用了单例模式,它也会产生很多个实例。如果一个机器上装有多个虚拟机,那么每个虚拟机里面都应该至少有一个这个类的实例,也就是说整个机器上就有很多个实例。
注意:这里讨论的单例模式并不适用于集群环境。
登记式单例实际上维护的是一组单例类的实例,将这些实例存放在一个Map(登记簿)中,对于已经登记过得实例,则从工厂直接返回,对于没有登记的,则先登记,而后返回。
【双重检查加锁】
public class Singleton{
private volatile static Singleton instance = null;
private Singleton(){}
private static Singleton getInstance(){
//先检查实例是否存在,如果不存在才进入下面的代码块
if(instance==null){
//同步块,线程安全的创建实例
synchronized(Singleton.class){
//再次检查实例是否存在,如果不存在,才真正的创建实例
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
所谓的“双重检查加锁”机制,是指并不是每次进入getInstance方法都需要同步,而是先不同步。当进入方法之后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查,进入到同步块过后,再次检查实例是否存在。如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,整个同步过程只需要一次同步,从而减少了多次在同步情况下进行判断所浪费的时间。
在使用“双重检查加锁”机制实现时,需要使用关键字 volatile,含义是被volatile修饰的变量的值不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能够正确的处理该变量。
这种实现方式不仅可以实现线程安全的创建实例,而且又不会对性能造成太大的影响。它只是在第一次创建实例的时候需要同步,以后就不需要同步了,从而加快了运行速度。
但是因为volatile关键字可能会屏蔽掉虚拟机中的一些必须的代码优化,所以运行效率并不是很高,如果没有特别的需要,不要使用volatile关键字;
【既能够实现延迟加载,又提供线程安全的单例模式】
public class Singleton {
private Singleton(){}
private static class SingletonHolder{
//静态初始化器,由JVM来保证线程安全
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
解决方案:Lazy initialization holder class 模式,这个模式综合使用了Java的类级内部类和多线程默认同步锁的知识,很巧妙地同时实现了延迟加载和线程安全;
类级的内部类(静态的成员式内部类)该内部类的实例与外部类的实例没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载;
这样,当getInstance方法第一次被调用的时候,它第一次读取SingletonHoler.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会被JVM在装载类的时候初始化一次,并由JVM来保证它的线程安全性;
这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问的开销;
【单例和枚举】
单元素的枚举类型已经成为实现Singleton的最佳方法。Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法。Java枚举类型的基本思想是通过公有的静态final域为每个枚举常量导出实例的类。从某个角度来讲,枚举是单例的泛型化,本质上是单元素的枚举。
public enum Singleton {
//定义一个枚举元素,它只代表了Singleton的一个实例
uniqueInstance;
//示意方法,单例可以有自己的操作
public void singletonOperation(){
}
}
由此可见,使用枚举来实现单实例控制,会更加的简洁,而且无偿的提供了序列化的机制,并由JVM从根本上提供保障,绝对防止多次实例化;
【单例模式的优点】
(1)减少内存的消耗,特别是一个对象如果需要频繁的被创建,销毁,而且创建或者销毁时候性能又无法优化,这时候就适合采用单例模式
(2)减少系统性能开销,(在 JavaEE中采用单例模式的时候,需要注意JVM垃圾回收机制)
(3)避免对资源的多重占用
(4)单例模式可以在系统设置全局的访问点,优化共享资源的访问,例如可以设计一个单例类,负责所有的数据表的映射处理;
【单例模式的缺点】
(1)单例模式没有接口,扩展困难;
(2)单例模式对测试是不利的,在并行开发环境中,如果单例模式没有完成,是不能完成测试的,没有接口也不能使用mock的方式虚拟一个对象;
(3)单例模式与单一职责模式有冲突。
【单例模式使用场景】
(1)要求生成唯一序列号的环境
(2)在整个项目中需要有访问一个共享访问点或者共享数据,例如一个Web页面上的计数器,可以不用每次刷新都记录到数据库中,使用单例模式来保持计数器的值,并确保是线程安全的;
(3)创建一个对象消耗的资源过多,如要访问i/O、访问数据库等资源
(4)需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式。当然,也可以直接声明为static的方式;
一般就是定义适配器继承现有的类并且实现特定功能的接口来达到想要的效果;
背景:
为什么要使用适配器模式,直接修改源码不就可以了吗?
其实适配器是为了某种目的而为一个源类暂时性的加上某种方法,所以不能破坏原类的结构。同时不这么做也符合Java的高内聚、低耦合的要求。既然
【适配器模式的分类】
(1)类的适配器模式,采用继承来实现;
面向类的适配器模式,这类适配器模式就是主要用于单一的为某个类而实现适配的这样一种模式。
(2)对象适配器模式,采用对象组合的方式实现;
对象适配器模式是把“源”作为一个对象聚合到适配器类中。
【适配器模式中的角色】
(1)目标抽象角色(Target):定义客户所期待要使用的接口,在这里可以抽象出来一个符合用户需求的类;
(2)原角色(Adaptee):需要被适配的接口。是指现成的但是不太适合的、需要被改造的接口或类;
(3)适配器角色(Adapter):用来把源接口转换成符合要求的目标接口的设备;
(4)客户端(Client):这里指的是目前的项目;
也就是说:通过适配器对原角色进行改造(并不是改造原角色的源代码)成目标抽象角色,最后返回给客户端;
【适配器模式的使用场景】
(1)系统想要使用现有的类,而此类的接口不符合系统的需要;
(2)想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。这些源类不一定有很复杂的接口;
针对对象适配器来说,在设计中需要改变多个已有子类的接口;
如果使用类的适配器模式,就要针对每一个子类做一个适配器,不太实际;
桥梁模式是一个非常重要的模式,也是比较复杂的一个模式。熟悉这个模式对于立即面向对象的设计原则,包括“开——闭”原则(OCP)以及组合/聚合复用原则(CARP)很有帮助。
桥梁模式的用意:将抽象化(Abstraction)与实现(Implementation)脱耦,使得二者可以独立的变化;
(1)抽象化
存在于多个实体中的共同的概念性联系,就是抽象化。作为一个过程,抽象化就是忽略掉一些信息,从而把不同的实体当做同样的实体来对待;
(2)实现
即针对抽象化给出的具体实现;
(3)脱耦:
耦合是指两个实体行为的某种强关联。如果将它们之间的强关联改换成弱关联。因此,桥梁模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现之间使用组合
【桥梁模式的结构】
桥梁模式是对象的结构模式,又称为柄体(Handle and Body)模式或者接口(Interface)模式。
【桥梁模式中的角色】
(1)抽象化角色(Abstraction):
抽象化给出的定义,并保存一个对实现化对象的引用;
(2)修正抽象化角色(RefinedAbstraction):
扩展抽象化角色,改变和修正父类对抽象化的定义;
(3)实现化角色(Implementor):
这个角色给出实现化角色的接口,但是不给出具体的实现。必须指出的是,这个接口不一定与抽象化角色的接口定义相同,实际上,这两个接口可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作;
(4)具体实现化(ConcreteImplementor)角色:
给出实现化角色接口的具体实现;
【角色之间的关联】
在桥梁模式的4个角色中,抽象化角色就像是一个水杯的手柄,而实现化角色和具体的实现化角色就像是水杯的杯身,手柄控制杯身,这就是该模式别名“柄体”的来源;
抽象化等级结构中的方法通过向对应的实现化对象的委派实现自己的功能,这意味着抽象化角色可以通过向不同的实现化对象委派,来达到动态的转换自己的功能的目的;
一般来讲,实现化角色中的每个方法都应当有一个抽象化角色中的某一个方法与之对应,但是反过来不一定;换言之,抽象化角色的接口比实现化角色的接口宽。抽象化角色除了提供与实现化角色相关的方法之外,还有可能提供其他的方法;而实现化角色则往往仅为实现抽象化角色的相关行为而存在;
【使用桥梁模式的场景】
如果一个接口或者类的抽象和实现是混杂在一起的,这样就导致了一个维度的变化会引起另一个维度进行相应的变化,从而使得程序扩展起来非常的困难;
如果想要解决这个问题,就必须把这两个维度分开,也就是将抽象部分和实现部分分开,让他们相互独立,这样就可以实现独立的变化,使扩展变得简单;
【桥梁模式在Java中典型应用】
桥梁模式在Java中的典型应用是实现JDBC驱动器。JDBC为所有的关系型数据库提供了一个通用的界面。一个应用系统动态地选择一个合适的驱动器,然后通过驱动器向数据库引擎发出指令。这个过程就是将抽象角色的行为委派给实现角色的过程;
抽象角色可以针对任何的的数据库引擎发出查询指令,因为抽象角色并不直接与数据库引擎打交道,JDBC驱动器负责这个底层的操作。
JDBC模式的架构把抽象部分和具体的部分分离开来,从而使得抽象部分和具体部分都可以独立的扩展。对于引用程序而言,只要选用不同的驱动,就可以让程序操作不同的数据库,而不需要更改应用程序,从而实现在不同数据库上移植;
对于驱动程序而言,为数据库实现不同的驱动程序,并不会影响应用程序;
总结:
基于JDBC的应用程序使用JDBC的API,相当于是对数据库操作的抽象的扩展,算作是桥梁模式的抽象部分;而具体的接口实现是由驱动来完成的,驱动这边自然就相当于桥梁模式中的实现部分了。而桥接的方式,不再是让抽象部分持有实现部分,而是采用类似于工厂的做法,通过DriverManager来把抽象部分和实现部分对接起来,从而实现抽象部分和实现部分解耦;
【桥梁模式的详解】
桥接的概念:
所谓的桥接,是指在不同的东西之间搭一个桥,让它们能够连接起来,可以相互通信和使用。具体就是在被分离的抽象部分和实现部分之间来搭一个桥;
注意:在桥梁模式中桥接是单向的,也就是只能是抽象部分的对象去使用具体实现部分的对象,而不能反过来;
(1)为什么需要桥接:
为了达到让抽象部分和实现部分都可以独立变化的功能,在桥梁模式中,是把抽象部分和实现部分分离开来的;
通过搭桥,让抽象部分通过这个桥就可以调用到实现部分的功能了,因此需要桥接;
(2)如何桥接:
只需要让抽象部分拥有实现部分的接口对象,就算桥接上了。在抽象部分可以通过这个接口来调用具体实现部分的功能。
具体可以通过在抽象部分的类的构造函数中引入实现部分的对象,也可以通过setter方法设置实现部分的对象;
(3)独立变化:
桥梁模式的意图就是:使得抽象和实现可以独立的变化,都可以分别得扩充。也就是说,抽象部分和实现部分是一种非常松散的关系,从某个角度来讲,抽象部分和实现部分是可以完全分开的、独立的,抽象部分不过是一个使用实现部分对外接口的程序罢了;
(4)动态变换功能:
由于桥梁模式中的抽象部分和实现部分是完全分离的,因此可以在运行时候动态的组合具体的真实实现,从而达到动态变换的目的;
即同一个真实实现可以被不同的抽象对象使用,反过来,同一个抽象也可以有多个不同的实现;
(5)桥梁模式和继承
继承是扩展对象的一种常见手段。通常情况下,继承扩展的功能 变化维度都是一维的,也就是变化的因素只有一类。对于出现变化因素有两类的,也就是有两个变化维度的情况,继承实现就会比较痛苦。
从理论上来讲,如果使用继承的方式来实现这种有两个维度变化的情况,最后实际的实现类应该是两个维度上可变数量的乘积那么多个。
其实桥梁模式主要是把继承改成了使用对象组合,从而把两个维度分开了,让每一个维度去单独的变化,最后通过对象对象组合的方式,把两个维度组合起来,每一种组合的方式就相当于原来继承中的一种实现,这样就有效的减少了实际实现的类的个数;
从理论上来讲,如果用桥梁模式的方式来实现这种有两个变化维度的情况,最后实际的实现类应该是两个维度上可变数量的和那么多个。
【谁来桥接的问题】
就是谁来负责抽象部分和实现部分的关系。
也就是谁来负责创建实现部分对象,并且把它设置到抽象部分的对象里面去。
这点对于桥梁模式来讲,非常的重要,大致如下:
(1)由客户端负责创建Implementor对象,并在创建抽象部分的时候,把它设置到抽象部分的对象里面去;
(2)可以在抽象部分的对象构建的时候,由抽象部分的对象自己来创建相应的Implementor的对象,当然可以给它传递一些参数,它可以根据参数来选择并创建具体的Implementor的对象;
(3)可以在Abstraction中选择并创建一个默认的Implementor的对象,然后子类可以根据需要改变这个实现;
(4)也可以使用抽象工厂或者是简单工厂来选择并创建具体的Implementor的对象,抽象部分的类可以通过调用工厂的方法来获取Implementor的对象;
(5) 如果使用IoC/DI容器的话,还可以通过IoC/DI 容器来创建具体的Implementor的对象,并注入回到Abstraction中;
【广义桥接】
当使用Java编写程序的时候,一个很重要的原则就是“面向接口编程”,更准确点就是“面向抽象编程”。接口把具体的实现与使用接口的客户程序分离开来,从而使得具体的实现和使用接口的客户程序可以分别扩展,而不会相互影响。
从广义的桥接模式的角度来看,广为熟悉的三层架构其实就是在组合使用桥接模式。桥接模式是可以连续组合使用的,一个桥梁模式的实现部分可以作为下一个桥梁模式的抽象部分。
如果从更本质的角度来看,基本上只要是面向抽象编写的Java程序,都可以视为桥梁模式的应用,都是让抽象和实现相分离,从而使得它们可以独立变化;
【桥梁模式的总结】
桥梁模式的本质是分离抽象和实现。
桥梁模式最重要的工作就是分离抽象部分和实现部分,这是解决问题的关键。
只有把抽象和实现分离开了,才能够让它们可以独立的变化;只有从抽象和实现可以独立的变化,系统才会有更好的可扩展性、可维护性;
【桥梁模式的优点】
(1)很好的实现了开闭原则;
在使用桥梁模式的时候,通常情况下,顶层的Abstraction和Implementor是不变的,而具体的Implementor的实现,是可变化的,由于Abstraction是通过接口来操作具体的实现,因此具体的Implementor的实现是可以扩展的,根据需要可以有多个具体的实现;
(2)多用对象组合、少用对象继承;
如果使用对象继承来扩展功能,不但会使得对象之间有很强的耦合性,而且会需要很多的子类来完成相应的功能,需要维护N个维度上可变化数量的乘积的个数的子类;
而采用对象的组合,松散了对象之间的耦合性,大大减少了子类的个数,大约需要N个维度的可变化数量之和的子类;
(3)分离抽象和实现部分
(4)更好的扩展性;
(5)可动态的切换实现;
(6)可以减少子类的个数;
组合(Composite)模式体现了整体与部分的关系,其典型的应用就是树形结构。组合涉及的是一组对象,其中有的对象可能含有其他的对象,因此有的对象可能代表一个对象群组,而有的则就是单个对象,即叶子(leaf)。
定义:将对象组合成树形结构以表示“整体-部分”的层次结构。Composite模式使得单个对象和组合对象的使用具有一致性;
组合模式中两个重要的建模概念:
(1)所涉及的群组既要能包含单个个体,还要能包含其他的群组。一个常见的错误是所设计的群组只能包含叶子;
(2)要定义出单个对象和对象组合的公共特性;
【组合模式中主要的构成元素】
(1)Component类:组合中的对象声明接口,在适当的情况下,实现所有类共有接口的行为。声明一个接口用于访问和管理Component的子部件;
(2)Leaf类:叶节点对象。叶节点行为用来存储叶节点集合;
(3)Composite类:实现Component的相关操作,比如Add或remove操作;
(4)children:用来存储叶节点集合;
【组合模式涉及到如下几个角色】
(1)抽象构件(Component)角色:
这是一个抽象角色,它给参加组合的对象规定一个接口。这个接口给出共有的接口以及其默认的行为;
(2)树叶构件(Leaf)角色:
代表参加组合的树叶对象。
(3)树枝构件(Composite)角色:
代表参数组合的所有的子对象的对象,并给出树枝构建对象的行为;
【组合模式的使用范例】
组合模式在程序设计中有着广泛的应用,比如Dom4j、资源管理器、Java GUI容器层次图等都是组合模式应用的典范。组合模式需要分析思考才能鉴别出来。
装饰模式是指给一个类添加一些额外的职责,并且在添加这些额外的职责的时候不会控制该类的执行逻辑。
【装饰模式的角色】
(1)抽象构件角色(Component):给出一个抽象的接口,以规范准备接受附加责任的对象。相当于I/O流中的InputStream/OutputStream和Reader/Writer
(2)具体的构件角色(ConcreteComponent):定义一个将要接受附加责任的类,相当于I/O流中的FileOutputStream和FileInputStream
(3)装饰角色(Docorator):持有一个抽象构件(Component)角色的引用,并定义一个与抽象构件一致的接口。相当于I/O 流中的FilterOutputStream和FilterInputStream
(4)具体装饰角色(ConcreteDecorator):负责给构建对象“贴上”附加的职责。相当于I/O流里面的BufferedOutputStream、BufferedInputStream、DataOutputStream和DataInputStream
【装饰模式使用场景距离】
1》奖金计算面临的问题:
计算逻辑复杂;
需要有足够的灵活性,可以方便的增加或者减少功能;
要能动态组合计算方式,不同的人参与的计算不同;
实现:
为了提高灵活性,可以把多种计算奖金的方式分散到不同的装饰器对象里面。然后采用动态组合的方式,给基本的计算奖金的对象添加计算奖金的功能,这样,每个装饰器相当于计算奖金的一个部分。这种方式要比为基本的计算奖金的对象增加子类来的更加灵活。。为了达到一层层组装的效果,装饰模式还要求装饰器要实现与被装饰对象相同的业务接口,这样才能以同一种方式一次组合下去;
【面向对象的设计规则】
尽量使用对象组合,而不是对象继承;
【Java中装饰者模式的应用】
1》最经典的应用就是I/O流;
2》装饰模式和AOP
从某个侧面来讲,装饰模式和AOP要实现的功能是类似的,只不过AOP的实现方法不同,会更加的灵活,更加的可配置;
【装饰者模式的总结】
(1)比继承更加的灵活,继承是静态的,而且一旦继承之后,是所有的子类都有同样的功能。
而装饰者模式则把功能分离到每个装饰器中,然后通过对象组合的方式,在运行时动态的组合功能,每个装饰的对象最终有哪些功能,是由运行期动态组合的功能来决定;
(2)更容易复用功能:
装饰者模式将一系列的复杂的功能分散到每个装饰器中,一般一个装饰器只实现一个特定的功能。
(3)简化高层定义
装饰模式可以通过组合装饰器的方式,给对象添加任意多的功能,因此在进行高层定义的时候,不用把所有的功能都定义出来,而是定义最基本的功能就可以了,可以在使用需要的时候,组合相应的装饰器来完成需要的功能;
(4)会产生很多细粒度对象
装饰模式是把一系列复杂的功能分散到每个装饰器中,一般一个装饰器只实现一个功能,这样会产生很多的细粒度对象,而且功能越复杂,所需要的细粒度对象就越多;
外观模式也被称为Façade模式,能够为子系统中的一组接口提供一个统一接口。Façade模式定义了一个更高层的接口,使子系统更加容易的使用。外观模式是一种结构型模式,它主要解决的问题是:组件的客户和组件中各种复杂的子系统有了过多的耦合,随着外部客户程序和各子系统的烟花,这种过多的耦合面临很多变化的挑战。
外观模式也是由代理模式发展而来的。与代理模式类似,代理模式是一对一的代理,而外观模式是一对多的代理。与装饰者模式不同的是,装饰模式为对象增加功能,而外观模式则是提供一个简化的调用方式。一个系统可以有多个外观类(门面类),每个门面类都只有一个实例,可以使用单例模式来实现;
【门面模式的角色】
1》门面角色facade:门面模式的核心,被客户角色调用,因此它熟悉子系统的功能。它内部根据客户的需求预定了几种功能的组合;
2》子系统角色:实现了子系统的功能。
3》客户角色:调用façade角色来完成要得到的功能
【外观模式的意义】
外观模式属于结构型模式,其意图是为子系统中的一组接口提供一个一致的界面,Façade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用;
在项目设计中,将一个系统划分为若干个子系统有利于降低系统的复杂度,一个常见的设计目标是使子系统之间的通信和相互依赖的关系达到最小,实现该目标的途径之一就是引入一个外观对象,它为子系统中较一般的设施提供了一个单一而简单的界面;
【外观模式的实用性】
1》当需要为一个复杂的子系统提供一个简单的接口的时候,子系统往往因为不断地演化而变得越来越复杂,通常在使用模式的时候都会产生更多、更小的类,这使得子系统更具有可重用性,也更容易对子系统进行定制;
2》客户程序与抽象类的实现部分之间存在很大的依赖性,引入Façade将这个子系统与客户以及其他的子系统进行分离,可以提高子系统的独立性和可移植性;
3》当需要构建一个层次结构的子系统时,使用Façade模式定义子系统中每层的入口点,如果子系统之间是相互依赖的,就可以让它们通过Façade进行通信,从而简化它们之间的依赖关系;
【外观模式的总结】
外观模式的使用场景:
(1)为一个子系统提供一个简单的接口;
(2)提高子系统的独立性;
(3)在层次化结构中,可以使用Façade模式定义系统中每一层的入口;
外观模式可以将一些复杂的类包装成一个简单的封闭接口。外观模式对客户屏蔽了复杂的子系统组件,并为一般用户提供了一个比较简单的程序设计接口。但是,它并没有限制高级用户在需要的时候使用深层次的、较复杂的类;
【外观模式的优点】
(1)对客户屏蔽子系统组件,因而减少了客户处理的对象数目,从而使得子系统使用起来更加的方便;
(2)实现了子系统与客户之间的松散耦合关系,而子系统内部的功能组件往往是紧耦合的。松耦合关系使得子系统的组件变化不会影响到其客户;
(3)如果需要,也不限制使用子系统类;
享元模式即FlyWeight模式,是构造型模式之一,通过与其他的类似对象共享数据来较少内存的占用情况。享元模式运用共享技术有效的支持大量细粒度的对象,也就是说,在一个系统中如果有多个相同的对象,那么只需要共享一份即可,而不必去实例化每一个对象。
在FlyWeight模式中,由于需要生产各种各样的对象,所以在FlyWeight(享元)模式中常出现Factory模式。FlyWeight的内部状态是用来共享的,FlyWeight Factory负责维护一个对象存储池来存放内部状态的对象。为了方便调用,FlyWeightFactory类一般使用Singleton模式实现。FlyWeight模式是一个提高程序效率和性能的模式,会大大的加快程序的运行速度;
【享元模式的结构】
享元模式采用一个共享来避免大量拥有相同内容对象的开销。这种开销最常见、最直观的就是内存的消耗。享元模式能够做到共享的关键是区分内蕴状态(Internal State)和外蕴状态(External State)。
内蕴状态是存储在享元对象内部的,而且是不会随着环境的改变而有所不同的。因此,一个享元可以具有内蕴状态并可以共享;
外蕴状态:外蕴状态是随着环境的改变而改变的、不可共享的。享元对象的外蕴状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象的内部。
外蕴状态不可以影响享元对象的内蕴状态,它们是相互独立的;
【享元模式中的角色】
享元模式中所涉及到的角色有抽象享元角色、具体(单纯)享元角色、复合享元角色、享元工厂角色。以及客户端角色等,具体说明如下:
抽象享元角色(FlyWeight):
此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口或者抽象类。那些需要外蕴状态(External State)的操作可以通过方法的参数传入。抽象享元的接口使得享元变得可能,但是并不强制子类实行共享,因此并非所有的享元对象都是可以共享的;
具体享元角色(ConcreteFlyweight):
实现抽象享元角色所规定的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享。有时候具体享元角色又称为单纯具体享元角色,因为复合享元角色是由单纯具体享元角色通过复合而成的;
复合享元(UnsharableFlyweight):
复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解为多个本身是单纯享元对象的组合。复合享元对象又称为不可共享的享元对象。这个角色一般很少使用;
享元工厂角色(FlyWeightFactory):
负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当的共享。当一个客户端对象请求一个享元对象的时候,享元工厂角色需要检查系统中是否已经有一个符合要求的享元对象,如果已经有了,享元对象就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个新的合适的享元对象;
客户端角色:
本角色还需要自行的存储所有享元对象的外蕴状态;
【享元模式的两种形式】
1》单纯享元模式:
在单纯的享元模式中,所有的享元对象都是可以共享的。单纯享元模式多涉及到的角色如下所示:
抽象享元角色:
具体享元角色:
享元工厂角色:
2》复合享元模式:
在单纯享元模式中,所有的享元对象都是单纯享元对象,也就是说,都是可以直接共享的。还有一种较为复杂的情况,将一些单纯享元使用合成模式加以复合,形成复合享元对象。这样的复合享元对象本身不能共享,但是taeny可以分解成单纯享元对象,而后者则可以共享;
【享元模式的使用场景】
如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时候,就可以考虑使用。还有就是对象的大多数状态可以是外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象;
一般享元的获取都是通过一个工厂类来实现的;
注意:这些共享对象有可能在途中被改变。我们应当根据不同的应用来决定是不是允许共享对象改变,如果不允许,就把它们设计成不可变对象(immutable)。如果允许变,则要考虑是不是创建个新的享元同时保留老享元,或者通知那些使用享元的对象享元已经改变了。
【数据库连接池的应用】
享元模式由于其共享的特性,可以使用在任何池化的操作中,如线程池、数据库连接池等。数据库连接池是享元模式的一个典型应用。
在该应用中,需要一个连接池的工厂类ConnectPool,它是多个链接Connection的聚集。连接池工厂类ConnectPool具有一个数据集合对象pool,它在构造函数中进行初始化。该类提供了一个单例的工厂类,以防止重复创建该工厂的实例,并提供取得连接getConnection()和释放连接freeConnection()函数来分别从共享池中取得和释放一个连接;
【在XML等数据源中的应用】
将公共的属性提取出来当做享元来公用,将节省更过的空间。共享的FlyWeight越多,节省的空间也就越大;
【享元模式的总结】
1》使用FlyWeight的优缺点:
(1)享元模式使得系统更加的复杂。为了是对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化;
(2)享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长;
2》使用FlyWeight模式的情景:
(1)一个应用中使用了大量的对象;
(2)完全由于使用大量的对象,造成很大的存储开销;
(3)对象的大多数状态都可以变为外部状态;
(4)如果删除对象以外的状态,那么可以使用相对较少的共享对象取代很多组对象;
(5)应用程序不依赖于对象标识;
代理模式(Proxy Pattern)是一个使用率很高的模式,为其他的对象提供了一种代理,这样可以控制对这个对象的访问。
代理模式也称为委托模式,它是一项基本的设计技巧。许多其他的模式(例如状态模式、策略模式、访问者模式)本质上是在更为特殊的场合采用了代理模式。
Struts2的Form元素映射就采用了代理模式(准确的讲是动态代理模式)
【代理模式的结构】
(1)Subject(抽象主题角色):
抽象主题类可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求;
(2)RealSubject(具体主题角色):
也被称为委托角色、被代理角色,是业务逻辑的具体执行者;
(3)Proxy(代理主题角色):
也称为委托类、代理类,负责对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色来实现,并且在真实主题角色处理完毕前后做预处理和善后处理工作;
【静态代理/动态代理】
Java主要有两种代理模式:
1》静态代理:
由程序员创建或者工具生成代理类的源码,在编译代理类。所谓静态,也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行之前就已经确定了;
静态代理类的优点:
业务类只需要关注业务逻辑本身,保证了业务类的重用性,这是代理的共有优点;
静态代理类的缺点:
(1)代理对象的一个接口只服务与一种类型的对象,如果需要代理的方法很多,势必需要为每一种方法都进行代理,静态代理在程序规模稍大的时候就无法胜任了。
(2)如果接口增加一个方法,除了所有实现类需要实现这个方法之外,所有代理类也需要实现此方法。增加了代码维护的复杂度;
2》动态代理:
动态代理类的源码是在程序运行期间由JVM根据反射等机制动态地生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定的;
(1)与动态代理紧密关联的API:
1. java.lang.reflect.Proxy:
这是Java动态代理机制生成的所有动态代理类的父类,它提供了一组静态的方法来为一组接口动态地生成代理类及其对象。Proxy类中的静态方法的格式如下所示:
//方法一:该方法用于获取指定代理对象所关联的调用处理器
public static InvocationHandler getInvocationHandler(Object proxy)
//方法二:该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象
public static Class> getProxyClass(ClassLoader loader,
Class>... interfaces)
//方法三:该方法用于判断指定类对象是否是一个动态代理对象
public static boolean isProxyClass(Class> cl) {
return Proxy.class.isAssignableFrom(cl) && proxyClassCache.containsValue(cl);
}
//方法四:该方法用于为指定类装载器、一组接口以及调用处理器生成动态代理类实例
public static Object newProxyInstance(ClassLoader loader,
Class>[] interfaces, InvocationHandler h)
2.java.lang.reflect.InvocationHandler:
这是调用处理接口,它自定义了一个invoke方法,用于集中处理在动态代理类对象的方法的调用,通常在该方法中实现对委托类的代理的访问。每次生成动态代理对象的时候都需要指定一个对应的调用处理器对象。其核心方法InvocationHandler的语法格式如下:
//该方法负责集中处理动态代理类上的所有的方法的调用;
第一个参数是代理类的实例;
第二个参数是被调用的方法对象;
第三个参数是调用参数。调用处理器根据这三个参数进行预处理或者分派到委托类实例上反射执行;
public Object invoke(Object proxy, Method method, Object[] args)
3. java.lang.ClassLoader:
是类装载器类,负责将类的字节码装载到Java虚拟机(JVM)中并为其定义类对象,然后该类才能够被使用。Proxy静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通的类唯一的区别就是其字节码是由JVM在运行时动态生成的,而不是预存在于任何一个.class文件中。每次生成动态代理类对象的时候都需要指定一个类装载器对象。
【实现动态代理的步骤】
生成动态代理类的实例:
(1)实现InvocationHandler接口,创建自己的调用处理器;
(2)给Proxy类提供ClassLoader和代理接口类型数组,创建动态代理类;
(3)以调用处理器类型为参数,利用反射机制得到动态代理类的构造函数;
(4)以调用处理器对象为参数,利用动态代理类的构造函数创建动态代理类的对象;
分步骤实现动态代理的演示代码:
1、InvocationHandlerImpl实现了InvocationHandler接口,并能够实现方法调用从代理类到委托类的分派实例,其内部通常包括指向委托类实例的引用,用于真正执行分派转发过来的方法的调用;
InvocationHandler Handler = new InvocationHandlerImpl(…);
2、通过Proxy为包括Interface接口在内的一组接口动态的创建代理类的类对象
Class clazz = Proxy.getProxyClass(classLoader,new Class[]{Interface.class,…});
3、通过反射从生成的类对象获得构造函数对象
Constructor constructor = clazz.getConstrutor(new Class[]{InvocationHandler.class});
4、通过构造函数对象创建动态代理类实例
Interface Proxy = (Interface)construtor.newInstance(new Object[] {handler});
因为类Proxy的静态方法newProxyInstance对上面步骤中的后面三步做了封装,简化了动态代理对象的获取过程。
InvocationHandler Handler = new InvocationHandlerImpl();
//通过Proxy直接创建动态代理类实例
Interface proxy = (Interface)Proxy.getProxyInstance(classLoader,new Class[]{Interface.class},handler);
【动态生成的代理类本身的特点】
包:如果所代理的接口都是public的,那么它将被定义在顶层包,如果所代理的接口中有非public的接口,那么它将被定义在该接口所在包;
类修饰符:该代理类具有final和public修饰符,意味着它可以被所有的类访问,但是不能被再度继承;
类名:格式是$ProxyN,其中N是一个逐一递增的阿拉伯数字,代表Proxy类第N次生成的动态代理类,注意:如果对同一组接口(包括接口排列顺序相同)试图重复创建动态代理类,它会很聪明的返回先前已经创建号的代理类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率;
类继承关系:类Proxy是它的父类,这个规则适用于所有由Proxy创建的动态代理类。而且该类还实现了其所代理的一组接口,这就是为什么它能够被安全的类型转换到其所代理的某接口的根本原因;
【代理类实例的一些特点】
每一个实例都会关联一个调用处理器对象,可以通过Proxy提供的静态方法getInvocationHandler去获取代理实例的调用处理器对象。在代理类实例上调用其代理接口中所声明的方法时,这些方法最终都会由调用处理器的invoke方法执行。此外值得注意的是:代理类的根类java.lang.Object中有三个方法也同样被分派到调用处理器的invoke方法执行,它们是hashCode、equals、toString;(为了保证代理类与委托类对外的一致性,这三个方法也应该被分派到委托类执行)
当代理类的一组接口有重复声明的方法并且该方法被调用的时候,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或者继承与该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用对象;
【被代理的一组接口的特点】
(1)需要注意不能有重复的接口,以避免动态代理类代码生成时的编译错误;
(2)这些接口对于类装载器必须可见,否则类装载器将无法链接它们,将会导致类定义失败;
(3)需被代理的所有非public的接口必须在同一个包中,否则代理类生成也会失败;
(4)接口的数目不能超过65535,这是JVM设定的限制;
【动态代理的优点】
动态代理与静态代理相比较,最大的好处就是,接口中声明的所有方法都被转移到调用处理器一个集中的方法处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活的处理,而不要像静态代理那样每一个方法进行中转。
使用了动态代理之后,将无需手动的生成“代理角色”,而是由JVM在运行的时候通过指定类加载器、接口数组、调用处理程序这3个参数来动态的生成;
【动态代理的缺点】
诚然,Proxy已经设计得非常优美,但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持interface代理的桎梏,因为它的设计注定了这个遗憾。回想一下那些动态生成的代理类的继承关系图,它们已经注定有一个共同的父类叫Proxy。Java的继承机制注定了这些动态代理类们无法实现对class的动态代理,原因是多继承在Java中本质上就行不通。
有很多条理由,人们可以否定对 class代理的必要性,但是同样有一些理由,相信支持class动态代理会更美好。接口和类的划分,本就不是很明显,只是到了Java中才变得如此的细化。如果只从方法的声明及是否被定义来考量,有一种两者的混合体,它的名字叫抽象类。实现对抽象类的动态代理,相信也有其内在的价值。此外,还有一些历史遗留的类,它们将因为没有实现任何接口而从此与动态代理永世无缘。如此种种,不得不说是一个小小的遗憾。但是,不完美并不等于不伟大,伟大是一种本质,Java动态代理就是佐例。
【剖析代理模式】
网络应用中的代理服务器分为两种:
透明代理:
透明代理就是用户不用设置服务器地址就可以直接访问。也就是说,代理服务器对用户来讲是透明的,看不到,不用知道它的存在;
普通代理:
普通代理则需要用户自己设置代理服务器的IP地址,用户必须知道代理的存在。
设计模式中的普通代理与强制代理也是类似的一种结构:
1》普通代理:
需要知道代理的存在,然后才能访问;
在该模式下,调用者只知道代理而不用知道真实的角色是谁,屏蔽了真实角色的变更对高层模块的影响,真实的主题角色无论如何修改对高层次的模块没有任何影响,只要你实现了接口所对应的方法;
2》强制代理:
强制代理则是调用者直接调用真实角色,而不用关心代理是否存在,其代理的产生是由真实角色决定的;
强制代理在设计模式中比较另类,一般的思维都是通过代理找到真实的角色,但是强制代理却是要“强制”你必须通过真实角色查找找到代理角色,否则你不能访问,不管你是通过代理类还是通过直接new一个主题角色类,都不能访问;
只有通过真实角色指定的代理类才可以访问。也就是说由真实角色管理代理角色,高层模块new了一个真实角色的对象。
强制代理的概念就是要从真实角色查找到代理角色,不允许直接访问真实角色,高层模块只要调用getProxy,就可以访问真实角色的所有方法,它根本就不需要产生一个代理出来,代理的管理已经由真实角色自己来完成;
3》虚拟代理:
虚拟代理是指在需要的时候才初始化主题对象,可以避免被代理对象较多而引起的初始化缓慢的问题,缺点是需要在每个方法中判断主题对象是否被创建,这就是虚拟代理,非常简单;
【代理模式的总结】
代理模式的优点:
(1)职责清晰:
真实的角色就是实现实际的业务逻辑,无须关心其他非本职责的事务,通过后期的代理完成一件事务,附带的好处就是编程更加的简介清晰;
(2)高扩展性:
具体主题角色是随时都会变化的,只要它实现了接口,甭管它如何变化,都逃不了接口的控制,我们的代理类完全可以在不做任何修改的情况下使用;
(3)智能化:
例如Struts是如何将表单元素映射到对象上的;
Spring AOP是一个非常典型的动态代理;
职责链(又称为责任链)模式即Chain of Responsibility模式,是指为了避免请求的发送者和接受者之间的耦合关系,使多个接受对象都有机会处理请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
责任链模式通俗一点说就是,当客户提交一个请求的时候,从第一个对象开始,链中收到请求的对象要么亲自处理它,要么转发给链中的下一个候选者。提交请求的对象并不知道哪个对象会处理它。当然责任链中也可能没有处理该请求的对象,这种情况是允许发生的;
【职责链模式的结构】
职责链模式是一种对象的行为模式
(1)抽象处理者(Handler)角色;
抽象处理者用于定义一个处理请求的接口,如果需要,接口可以定义出一个方法,以返回对下家的引用。
(2)具体处理者(ConcreteHandler)角色:
处理接到请求后,可以选择将请求处理掉,或者将请求传给下家。
【两种责任链模式】
责任链模式有两种:
1》纯的:
一个纯的责任链模式要求一个具体的处理者对象只能在两个行为中选择一个:一个是承担责任;二是把责任推给下家。不允许某个具体处理者对象在承担了一部分责任之后又把责任向下传的情况;
在一个纯的责任链里面,一个请求必须被某一个处理者对象所接受;在一个不纯的责任链里面,一个请求可以最终不被任何的接受端所接受;
2》不纯的:
【责任链模式总结】
责任链模式不指定责任链的拓扑结构,一个链可以是一条线、一个树,也可以是一个环。链的拓扑结构可以是单连通的或者是多连通的;
责任链模式要求在同一时间,命令只可以被传给一个下家(或者被处理掉);而不可以传给多于一个下家。
责任链模式是使用一系列类试图处理一个请求request,这些类之间是一个松散的耦合,唯一共同点就是在它们之间传递request。
【责任链模式的优点】:
因为无法预知来自外界(客户端)的请求是属于哪种类型,每个类如果遇到它不能处理的请求,只要放弃就可以了;
【责任链模式的缺点】:
效率低,因为一个请求的完成可能要遍历到最后才能完成;扩展性差,因为在责任链中,一定要有一个统一的接口Handler
命令模式即Command模式,此模式通过被称为Command的类封装了对目标对象的调用行为以及调用参数。将一个请求封装为一个对象,从而使我们可以使用不同的请求对用户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。
命令模式像很多的设计模式一样通过在请求和处理之间加上了一个中间人的角色,来达到分离耦合的目的;通过对中间人角色的特殊设计来形成不同的模式。
命令模式的关键之处就是把请求封装为对象,也就是命令对象,并定义了统一的执行操作的接口,这个命令对象可以被存储、转发、记录、处理、撤销等,整个命令模式都是围绕着这个对象在进行;
命令模式的过程分为两个阶段:
(1)一个阶段是组装命令对象和接收者对象的过程;
(2)另外一个阶段是触发调用Invoke,来让命令真正执行的过程;
【命令模式的结构】
【命令模式中撤销功能的实现】
方式一:补偿式:又称反操作式,比如被撤销的操作是加的功能,那撤销的实现就变成减的功能;
方式二:存储恢复式:意思是把操作前的状态记录下来,然后要撤销操作的时候,直接恢复回去就可以了;
宏命令:简单来讲,就是包含多个命令的命令,是一个命令的组合;
【Java中的回调机制】
给一个方法传入一个对象,在该方法内使用该对象调用真正的服务方法;
使用命令模式可以实现Java回调,更准确的讲,Invoker充当用户调用的服务实现,而回调的方法只是实现服务中的一个或者几个步骤;
【命令模式的本质】
命令模式的本质就是封装请求,命令模式的关键就是把请求封装成命令对象,然后就可以对这个对象进行一系列的处理了,比如上面讲到的参数化配置、可撤销操作、宏命令、队列请求、日志请求等功能处理;
【命令模式的优点】
解除了请求者与实现者之间的耦合,降低了系统的耦合度。
对请求排队或记录请求日志,支持撤销操作。
可以容易地设计一个组合命令。
新命令可以容易地加入到系统中。
【命令模式的缺点】
因为针对每一个命令都需要设计一个具体命令类,使用命令模式可能会导致系统有过多的具体命令类
【命令模式的使用场景】
1、如果需要抽象出需要执行的动作,并参数化这些对象,可以选用命令模式,把这些需要执行的动作抽象成为命令,然后实现命令的参数化配置;
2、如果需要在不同的时刻指定、排列和执行请求,可以选用命令模式,将这些请求封装成为命令对象,然后实现把请求队列化;
3、如果需要支持取消操作;
4、如果需要支持当前系统崩溃时,能把系统的操作功能重新执行一遍,可以选用命令模式,把这些操作功能的请求封装成为命令对象,然后实现日志命令,就可以在系统恢复回来之后,通过日志获取命令列表,从而重新执行一遍功能;
5、在需要事务的系统中,可以选用命令模式,命令模式提供了对事务进行建模的方法,命令模式的一个别名就是:Transaction;
解释器模式即Interpreter模式,是一种按照规定的语法进行解析的方案;
Interpreter Pattern定义:给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子;
【解释器模式的结构】
(1)AbstractException(抽象解释器):具体的解释任务由各个实现类来完成,具体的解释器分别由TerminalException和NonterminalException完成;
(2)TerminalException(终结符表达式):
实现与文法中的元素相关联的解释操作,一个解释器模式中只有一个终结符表达式,但是有多个实例,对应不同的终结符;
(3)NonterminalException(非终结符表达式):
文法中每条规则对应于一个非终结符表达式。非终结符表达式根据逻辑的复杂程度而增加,原则上每个文法规则都对应一个非终结符表达式;
(4)Context(环境角色):
【解释器模式的实现方法】
模拟Java语言中对布尔表达式进行操作和求值;
在Java语言中终结符是布尔变量true和false。非终结符表达式包含运算符and、or、和not等布尔表达式;
【解释器模式和Composite(组合模式)的区别】
Composite模式通常会为单个对象和群组对象定义一个公共接口。不过Composite模式并不要求支持该方式组织的结构,尽管该模式可以支持这些结构。而解释器模式通常会涉及不同的组合结构(所以说Intercepter通常处于Composite模式之上)。Interpreter模式的意图是可以按照自己定义的组合规则集合来组合可执行对象;
【解释器模式举例】
举例1:四则运算问题:
业务需求:输入一个模型公式(加减四则运算),然后输入模型中的参数,运算出结果。
举例二:编译器问题
一个现代编译器的主要工作流程是:源代码(sourcecode)——》预处理器(preprocessor)——》编译器(compiler)——》汇编程序(assembler)——》目标代码(object code)——》链接器(Linker)——》可执行程序(executables)
【对解释器模式的总结】
1.》解释器模式的优点:
(1)易于改变和扩展文法。
(2)易于实现文法定义;
缺点:复杂的文法难以维护;
解释器模式使用的场景
例如,多个应用服务器,每天产生大量的日志,需要对日志文件进行分析处理,由于各个服务器的日志格式不同,但是数据要素是相同的,按照解释器的说法就是终结符表达式都是相同的,但是非终结符表达式就需要制定了。在这种情况下,可以通过程序来一劳永逸地解决该问题。
为什么是简单?看看非终结表达式,文法规则越多,复杂度越高,而且类间还要进行递归调用(看看我们例子中的堆栈),不是一般地复杂。想想看,多个类之间的调用你需要什么样的耐心和信心去排查问题。因此,解释器模式一般用来解析比较标准的字符集,例如SQL语法分析,不过该部分逐渐被专用工具所取代。
在某些特用的商业环境下也会采用解释器模式,我们刚刚的例子就是一个商业环境,而且现在模型运算的例子非常多,目前很多商业机构已经能够提供出大量的数据进行分析。
解释器模式的注意事项
尽量不要在重要的模块中使用解释器模式,否则维护会是一个很大的问题。在项目中可以使用shell、JRuby、Groovy等脚本语言来代替解释器模式,弥补Java编译型语言的不足。我们在一个银行的分析型项目中就采用JRuby进行运算处理,避免使用解释器模式的四则运算,效率和性能各方面表现良好。
迭代器(Iterator)模式又被称为游标(Cursor)模式。这种模式提供一种方法访问一个容器(Container)对象中的各个元素,而又不暴露该对象的内部细节。
【迭代器模式中的角色】
迭代器角色负责定义访问和遍历元素的接口;
具体迭代器角色需要实现迭代器接口,并需要记录遍历中的当前位置;
容器角色负责提供创建具体迭代器角色的接口;
具体容器角色实现创建具体迭代器角色的接口——这个具体迭代器角色与该容器的结构相关;
【迭代器模式的实现方式】
在Java Collection的应用中,提供的具体迭代器角色是定义在容器角色中的内部类。这样便保护了容器的封装。但是同时容器也提供了遍历算法接口,可以扩展自己的迭代器;
【迭代器模式的缺点】
由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数在成对增加,这在一定的程度上增加了系统的复杂性;
迭代器模式的使用范围如下:
(1)访问一个容器对象的内容而无需暴露它的内部表示;
(2)支持对容器对象的多种遍历;
(3)为遍历不同的容器结构提供一个统一的接口(多态迭代)
迭代器模式支持以不同的方式遍历一个聚合对象:复杂的聚合可以使用多种方式进行遍历。迭代器模式使得改变遍历算法变得很容易,仅需要使用一个不同迭代器的实例代替原先的实例即可。我们也可以自己定义迭代器的子类以支持新的遍历;
迭代器简化了聚合的接口,有了迭代器的遍历接口,聚合本身就不再需要类似的遍历接口了。这样就简化了聚合的接口。在同一个聚合上可以有多个遍历。每个迭代器保持它自己的遍历状态。因此可以同时进行多个遍历;
在迭代器模式中,增加的新的聚合类和迭代器类都很方便,无需修改原有的代码,这满足了“开闭原则的要求”;
中介者模式的定义:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式的相互引用,从而使其耦合松散,而且可以独立的改变它们之间的交互;
简单来讲:将原来两个直接引用或者依赖的对象拆开,在中间加入一个中介对象,使得两头的对象分别与“中介”对象引用或者依赖。当然并不是所有的对象都需要加入“中介”对象。如果对象之间的关系原本一目了然,中介对象的加入便是多余了;
【中介者模式的结构】
1.抽象中介者(Mediator)角色:
抽象中介角色定义统一的接口,用于各同事角色之间的通信;
2.具体中介者(Concrete Mediator)角色:
具体中介者角色通过协调各同事角色,实现协作行为,为此它要指导并引用各个同事角色;
3.同事(Colleague)角色:
每一个同事角色都知道对应的具体中介者角色,而且与其他的同事角色通信的时候,一定需要通过中介者角色协作;
因为中介者的行为和需要使用的数据与具体业务紧密相关,抽象中介者角色提供了一个能方便很多对象使用的接口是不太现实的。所以抽象中介者角色往往是不存在的,或者只是一个标示接口。如果能够提炼出真正带有行为的抽象中介者角色,同事角色对具体中介者角色的选择也是策略的一种应用;
【MVC模型和中介者模式】
MVC模型分为三层:
模型层(Model)
表现层(View)
控制层(Control/Mediator)
控制层是位于表现层和模型层之间的中介者。笼统的说,MVC也算是中介者模式在框架设计中的一个应用;
【门面模式和中介者模式的区别】
门面模式:
门面模式是将原有的复杂逻辑提取到一个统一的接口,简化客户对逻辑的使用。它是被客户感知的,而原有的复杂逻辑则被隐藏了起来。
中介者模式:
中介者模式的加入,并没有改变又有客户的使用习惯,它是隐藏在原有逻辑后面的,使得代码逻辑更加清晰可用;
【中介者模式的最大好处】
将同事角色解耦。这带来了一系列的系统结构改善:提高了原有系统的可读性、简化了原有系统的通信协议——将原有的多对多变为一对多、提高了代码的可复用性;
【中介者模式的使用时机】
一组对象以定义良好但是复杂的方式通信,产生了混乱的依赖关系,也导致对象难以复用;
【两种中介者模式】
1》标准的中介者模式:
标准中介者模式的构成角色:
1.Mediator:
中介者接口。在里面定义各个同事对象之间的交互对象,可以是公共的通信方法,比如changed方法,大家都用,也可以是小范围的交互方法;
2.ConcreteMediator:
具体中介者实现对象。它需要维护各个同事之间的交互关系;
3.Colleague:
同事类的定义,所有具体同事类的父类,通常实现为抽象类,主要负责约束同事对象的类型,并实现一些具体同事类之间的功能;
4.ConcreteColleague:
具体的同事类,实现自己的业务,在需要与同事通信的时候,就与持有的中介者通信,中介者负责与其他的同事进行交互;
中介者角色持有同事的对象,使用逻辑在合适的地方使用合适的同事对象调用相应的方法;
同事角色持有中介者对象,在自己的方法中将自己的方法托管给中介者处理;
2》广义中介者模式:
广义中介者的主要特点如下:
1.通常会去掉同事对象的父类,这样可以让任意的对象,只要需要相互交互,就可以成为同事;
2.同事不定义Mediator接口,把具体的中介者实现成单例;
3.同事对象不再持有中介者对象,而是在具体处理方法里面去创建,或者获取,或者从参数传入需要的同事对象;
【中介者模式使用思路】
抽象同事类:
既然有中介者,那么每个具体同事必然需要与中介者联系,否则即没有必要存在于这个系统中,这里抽象中介者类的构造函数相当于向该系统中注册一个中介者,以取得联系;
具体的同事类:
每个具体同事类通过父类构造函数与中介者取得联系;
每个具体同事必然有自己分内的事,没必要与外界联系;
每个具体同事也需要通过中介者处理与外界交互的逻辑;
抽象中介者:
中介者需要保持有若干个同事的联系方式(可以使用集合实现);
中介者可以动态的与某个同事建立联系;
中介者也可以动态的撤销与某个同事之间的联系;
中介者必须在同事之间处理逻辑、分配任务、促进交流的操作;
具体中介者:
中介者最重要的功能来回奔波于各个同事之间;
备忘录模式的背景:
有时候,有必要记录一个对象的内部状态,为了允许客户取消不确定的操作或者从错误中恢复过来,需要实现检查点和取消机制,而要实现这些机制,需要事先把对象的状态保存在某处,这样才能恢复以前的状态。但对象通常封装了其状态,使得其状态不能被其他对象访问。在不破坏封装的前提条件下,保存对象的状态。备忘录模式就是用来解决这个问题的;
备忘录(Memento)模式又被称为标记(Token)模式。备忘录模式在不破快封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。
由定义可知,备忘录模式是专门用来存放对象历史状态的,这对于很好的实现undo、redo功能有很大的帮助。所以在命令模式中,undo、redo功能可以配合备忘录模式来实现;
【备忘录模式的结构】
备忘录模式的构成角色如下所示:
原发器类(Originator):
创建一个备忘录对象,使用备忘录存储它的内部状态;
负责人类(CareTaker):
负责保存好备忘录对象,不能检查或者操作备忘录的内容;
备忘录类(Memento):
将原发器的内部状态存储起来,原发器根据需要决定备忘录存储原发器的哪些内部状态。防止原发器以外的其他对象访问备忘录。备忘录有两个接口,一个是窄接口,一个是宽接口。CareTaker只能看到窄接口,它只能把备忘录传递给其他对象。相反,Originator看到的是一个宽接口,允许它访问返回到先前状态所需的数据;
【实现双接口】
在Java程序中,为了实现双接口,可以把Memento类设置称为Originator类的内部类,从而将Memento对象封装在Originator内,对外部提供一个标识接口MementoIF给CareTaker以及其他的对象。这样,Originator看到的是Memento的所有接口,而CareTaker看到的仅是标识接口;
【“白箱”备忘录模式的实现】
备忘录角色对任何对象都提供一个接口,即宽接口,通过该接口,备忘录内部的状态就对所有对象公开。因此这个实现又称为“白箱实现”;
白箱实现将发起人角色的状态存储在一个大家看得到的地方,因此是破坏封装性的。
步骤:
(1)实现发起人角色类,发起人角色类利用一个新创建的备忘录对象将自己的内部状态存储起来;
(2)实现备忘录角色,备忘录对象将发起人对象传入的状态存储起来;
(3)实现负责人角色类,负责人角色负责保存备忘录对象,但是从不修改或者从不查看备忘录对象的内容;
【“黑箱”备忘录模式的实现】
备忘录角色对发起人(Originator)角色对象提供一个宽接口,而为其他对象提供了一个窄接口。这样的实现称为“黑箱实现”;
在Java语言中,实现双重接口的办法就是将备忘录角色类设计成为发起人角色类中的内部成员类。将Memento设成Originator类的内部类,从而将Memento对象封装在Originator里面;在外部提供一个标识接口MementoIF给CareTaker以及其他对象。
【多重检查点】
前面给出的白箱和黑箱的示意性实现都是只存储一个状态的简单实现,也可以称为只有一个检查点。常见的系统往往需要存储不止一个状态,而是需要存储多个状态,或者说有多个检查点。
备忘录模式可以将发起人对象的状态存储到备忘录对象里面,备忘录模式可以将发起人对象恢复到备忘录对象所存储的某一个检查点上。
多重检查点备忘录模式的实现过程:
(1)实现发起人角色;
使用集合定义检查点,并且给每个检查点设置一个检查标志;
(2)实现备忘录角色类;
使用集合存储检查点,外界可以使用检查点指数来取出检查点上的状态;
(3)实现负责人角色类;
【自述历史模式】
所谓的自述历史模式(History-On-Self Pattern)实际上就是备忘录模式的一个变种。在备忘录模式中,发起人(Originator)角色、负责人(Caretaker)角色和备忘录(Memento) 角色都是独立的角色。虽然在实现上备忘录类可以成为发起人类的内部成员类,但是备忘录类仍然保持作为一个角色的独立意义。在“自述历史”模式里面,发起人角色自己兼任负责人角色。
此处发起人角色有如下两个责任:
(1)创建一个含有它当前的内部状态的备忘录对象;
(2)使用备忘录对象存储其内部状态;
此处的备忘录角色有如下两个责任:
(1)将发起人(Originator)对象的内部状态存储起来;
(2)备忘录角色可以保护其内容不被任何发起人(Originator)对象之外的任何对象所读取;
【对备忘录模式的总结】
备忘录模式的优点是可以避免暴露一些只应由原发器管理却又必须存储在原发器之外的信息,而且能够在对象需要的时候恢复到先前的状态;
备忘录模式的缺点是可能代价很高。如果原发器在生成备忘录的时候必须复制并存储大量的信息,或者客户非常频繁的创建备忘录和恢复原发器状态,可能会导致非常大的开销;
【备忘录模式的使用场景】
(1)一个类需要保存它的对象的状态(相当于Originator角色);
(2)设计一个类,该类只是用来保存上述对象的状态(相当于Memento)角色;
(3)小的使用Caretaker角色要求Originator返回一个Mementor并加以保存;
(4)undo或者rollback操作的时候,通过Caretaker保存的Memento恢复Originator对象的状态;
观察者模式定义了一个一对多的依赖关系,让一个或者多个观察者对象监察一个主题对象。
这样一个主题对象在状态上的变化能够通知所有的依赖于此对象的那些观察者对象,使这些观察者对象能够自动的更新;
观察者(Observer)模式是对象的行为型模式,又叫做发表——订阅(Publish/Subscribe)模式、模型——视图(Model/View)模式、源——收听者(Source/Listener)模式或者从属者(Dependents)模式;
【观察者模式的结构】
观察者模式中的角色:
(1)抽象主题(Subject)角色:
主题角色把所有的观察者对象的引用保存在一个列表中;每个主题都可以有任何数量的观察者。主题提供一个接口,可以加上或者撤销观察者对象;主题角色又称为抽象被观察者(Observable)角色。
(2)抽象观察者(Observer)角色:
为所有的具体观察者定义一个接口,在得到通知的时候更新自己。抽象观察者角色可以用一个抽象类或者一个接口来实现;在具体的情况下也不排除使用具体类来实现。
(3)具体主题(ConcreteSubject)角色:
具体主题保存对具体观察者对象有用的内部状态,在这种内部状态改变的时候,给其观察者发出一个通知,具体主题角色又称为具体被观察者角色。
(4)具体观察者(ConcreteObserver)角色:
具体观察者角色用于保存一个指向具体主题对象的引用,和一个与主题的状态相符的状态。具体观察者角色实现抽象观察者角色所要求的更新自己的接口,以便使本身的状态与主题的状态自恰。
【Java语言提供对观察者模式的支持】
虽然观察者模式的实现方法可以由设计师自己确定,但是因为从AWT 1.1开始视窗系统的事件模型采用观察者模式,因此观察者模式在Java语言中的地位较为重要。正是因为这个原因,Java语言给出了它自己对观察者模式的支持。因此,在自己的系统中应用观察者模式的时候,不妨利用Java语言所提供的支持;
在Java语言的java.util库中,分别提供了一个Observable类和一个Observer接口,构成了Java语言对观察者模式的支持;
(1)Observer接口
接口Observer只定义了一个update方法。当被观察者对象的状态发生变化的时候,这个方法就会被调用。这个方法的实现应当调用每一个被观察对象的notifyObservers()方法,
从而通知所有的观察对象。
(2)Observable类
被观察者java.util.Observable类的子类。java.util.Observable提供公开的方法支持观察者对象,这些方法中有两个对Observable的子类非常重要:一个是setChanged(),另一个是notifyObservers()。第一个方法setChanged()被调用之后会设置一个内部标记变量,代表被观察者对象的状态发生了变化。第二个是notifyObservers(),这个方法被调用的时候,会调用所有登记过的观察者对象的update()方法,使这些观察者对象可以更新自己;
此外,java.util.Observable类还有其他的一些重要的方法。比如,观察者对象可以调用java.util.Observable类的addObserver()方法,将对象一个一个的加入到一个列表上。当有变化的时候,这个列表可以告诉notifyObservers()方法哪些观察者对象需要通知。由于这个列表是私有的,因此java.util.Observable的子对象并不知道观察者对象一直在观察着它们;
发送通知的次序在这里没有指明。Observerable类所提供的默认实现会按照Observers对象被登记的次序通知它们,但是Observerable类的子类可以改掉这一次序。子类还可以在单独的线程中通知观察者对象;或者在一个公用的线程中按照次序执行。当一个观察者对象刚刚创立的时候,它的观察者集合是空的。两个观察者对象在它们的equals方法返回true的时候,被认为两个相等的对象;
【Java中的DEM事件机制】
在AWT的后续版本中,事件处理模型均为基于观察者模式的委派事件模型(Delegation Event Model,DEM)在DEM模型中,主题(Subject)角色负责发布(publish)事件,而观察者角色向特定的主题订阅(subscribe)它所感兴趣的事件。当一个具体主题产生一个事件的时候,它就会通知所有感兴趣的订阅者;
在使用DEM的术语中,发布者称为事件源(Event Source),而订阅者称为事件聆听者(Event Listener)。
在Java中,事件由类代表,事件的发布是通过同步的调用成员方法而做到的;
(1)Servlet 技术中的DEM机制;
AWT中所使用的DEM事件模型实际上被应用到了所有的Java事件机制上。Servlet技术中的事件处理机制同样也是使用的DEM模型;
(2)SAX2技术中的DEM机制;
DEM事件模型也被应用到SAX2的事件处理机制上;
以一对多的方式进行对“一”方状态的改变而去通知“多”方,观察者模式也称为发布/订阅模式,一方进行发布,而多方进行订阅;
【观察者模式的优点】
(1)观察者模式在被观察者和观察者之间建立一个抽象的耦合。被观察者角色所知道的只是一个具体观察者列表,每一个具体观察者都符合一个抽象观察者的接口。被观察者并不认识任何一个具体观察者,它只知道它们都有一个共同的接口;
(2)观察者模式支持广播通信。被观察者会向所有的登记过的观察者发出通知;
【观察者模式的缺点】
(1)通知所有的观察者可能会花费很多的时间;
(2)如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃;
(3)如果对观察者的通知是通过另外的线程进行异步投递的话,系统必须保证投递是以自恰的方式进行的;
(4)虽然观察者模式可以随时使观察者的对象发生了变化,但是观察者模式没有相应的机制使观察者知道所观察的对象是怎样发生变化的;
注意:观察者模式使用了备忘录模式,暂时将观察者对象存储在被观察者对象中;
状态模式又称状态对象模式(Pattern of Object for Status),状态模式是对象的行为模式。
状态模式允许一个对象在其状态改变的时候,改变它的行为。看起来对象似乎修改了它的类。
状态模式可以有效地替换充满在程序中的if else语句:将不同条件下的行为封装在一个类里面,再给这些类一个统一的父类来约束它们。
【状态模式的结构】
状态模式中的角色:
环境(Context)角色(也称为上下文):定义客户端所感兴趣的接口,并且保留一个具体状态类的实例。这个具体状态类的实例给出此环境对象的现有状态;
抽象状态(State)角色:
定义一个接口,用以封装环境(Context)对象的一个特定的状态所对应的行为;
具体状态(ConcreteState)角色:
每一个具体状态类都实现了环境(Context)的一个状态所对应的行为;
【状态模式的使用场景】
1》一个对象的行为取决于其状态,并且它必须在运行时刻根据状态改变它的行为;
2》一个操作中含有庞大的多分支的条件语句,并且这些分支依赖于该对象的状态;
【状态模式的总结】
所谓对象的状态,通常指的就是对象实例的属性的值;而行为指的就是对象的功能,再具体的说,行为大多可以对应到方法上;
状态模式的功能就是分离状态的行为,通过维护状态的变化,来调用不同状态对应的不同功能;也就是说:状态决定行为;
由于状态是在运行期被改变的,因此行为也会在运行期根据状态的改变而发生变化;
1》行为的平行性:
平行性指的是各个状态的行为所处的层次是一样的,相互独立的,没有关联的,是根据不同的状态来决定到底走平行线的哪一条。行为是不同的,当然对应的实现也是不同的,相互之间是不可替换的;
行为之间的特定也是状态模式和策略模式一个很重要的区别,状态模式的行为是平行性的,不可以相互替换的;而策略模式的行为是平等的,是可以相互替换的;
2》环境和状态处理对象:
在状态模式中,环境(Context)是持有状态的对象,但是环境(Context)自身并不处理跟状态相关的行为,而是把处理状态的功能委托给了状态对应的状态处理类来处理;
在具体的状态处理类中,经常需要获取环境(Context)自身的数据,甚至在必要的的时候会回调环境(Context)的方法,因此,通常将环境(Context)自身当做一个参数传递给具体的状态处理类。
客户端一般只需要与环境(Context)交互。客户端可以用状态对象来配置一个环境,一旦配置完毕,就不再需要和状态对象打交道了。客户端通常不负责运行期间状态的维护,也不负责决定后续到底使用哪一个具体的状态处理对象;
【状态模式的优点和缺点】
优点:避免了为判断状态而产生的巨大的if或者case语句。将对象行为交给状态类维护后,对于上层程序而言,仅需要维护状态之间的转换规则;
缺点:会导致某些系统有过多的具体状态类;
策略模式又被称为算法族模式,就是定义了不同的算法族,并且相互之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
【设计原则和对象】
策略模式的好处是可以动态的改变对象的行为。设计原则是把一个类中经常改变或者将来可能改变的部分提取出来作为一个接口,然后在类中包含这个对象的实例,这样类的实例在运行的时候就可以随意调用实现了这个接口的类的行为;
策略模式属于对象行为型模式,主要针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。通常,策略模式适合于当一个应用程序需要实现一种特定的服务或者功能,而且该程序有多种实现方式时使用;
在策略模式中有如下几个对象:
环境对象:
该类中实现了对抽象策略中定义的接口或者抽象类的引用;
抽象策略对象:
它可由接口或者抽象类来实现;
具体策略对象:
它封装了实现不同功能的不同算法;
利用策略模式构建应用程序,可以根据用户配置等内容,选择不同的算法来实现应用程序的功能;具体的选择由环境对象来完成。采用这种方式可以避免由于使用条件语句而带来的代码混乱,提高应用程序的灵活性和条理性;
【策略模式的结构】
策略模式中有如下三种角色:
1》Strategy:
策略接口,用来约束一系列具体的策略算法。Context使用这个接口来调用具体的策略,实现定义的算法;
2》ConcreteStrategy:
具体的策略实现,也就是具体的算法实现;
3》Context:(通常会持有一个具体的策略对象)
上下文,负责与具体的策略类交互,通常上下文会持有一个真正的策略实现,上下文还可以让具体的策略类来获取上下文的数据,甚至让具体的策略类来回调上下文的方法;
//在具体的策略类中实现相关的方法
Strategy strategy = new ConcreteStrategy(Strategy);
//将具体的策略类传入上下文中
Context context = new Context(strategy);
然后使用Context对象调用使用具体的策略类相关的方法;
【进一步认识策略模式】
策略模式的功能是把具体额算法实现从具体的业务处理里面独立出来,把它们实现成为独立的算法类,从而形成一系列的算法,并让这些算法可以相互转换;
策略模式的重心不是如何来实现算法,而是如何组织,调用这些算法,从而让程序结构更加的灵活、具有更好的维护性和扩展性;
(1)策略模式和if-else语句:
策略模式就是讲if-elseif 语句中的逻辑封装到单独的策略实现类中了,然后通过上下文来与具体的策略类进行交互。
(2)算法的平等性:
策略模式的一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,地位是完全一样的,正是因为这个平等性,才能实现算法之间可以相互转换;
(3)谁来选择具体的策略算法:
在策略模式中,可以在两个地方来进行具体策略的选择。一个是在客户端,在使用上下文的时候,由客户端来选择具体的策略算法,然后把这个策略算法设置给上下文。
另一个地方是客户端不管,由上下文来选择具体的策略算法。
(4)Strategy 的实现方式
Strategy可以使用接口来实现,这也是常见的实现方式,但是如果多个算法之间有公共部分的话,可以把Strategy实现成为抽象类,然后把多个算法的公共功能实现到Strategy里面;
(5)运行时策略的唯一性
【容错恢复机制】
容错机制不但能容忍运行出现错误的情况,还提供出现错误之后的备用方案,也就是恢复机制,来代替正常执行的功能,使程序继续向下运行;
对于这样的功能设计,就可以采用策略模式,把日志记录到数据库和把日志记录到文件当做两种记录日志的策略,然后在运行期间根据需要进行动态转换。
在实际的应用中,需要设计容错恢复的系统一般要求都比较高,应用也会比较复杂,但是基本的思路是差不多的;
【策略模式结合模板方式模式】
在实际应用策略模式的过程中,经常会出现这样一种情况:发现一系列算法的实现上存在公共功能,甚至这一系列算法的实现步骤都是一样的,只是在某些局部步骤上有所不同而已。这个时候,就需要对策略模式进行些许的变化使用了。
对于一系列算法的实现上存在公共功能的情况,策略模式可以有如下几种是实现方式:
(1)在上下文中实现公共功能,让所有具体的策略算法回调这些功能;
(2)把策略的接口改成抽象类,然后在里面实现具体算法的公共功能;
(3)给所有的策略算法定义一个抽象的父类,让这个父类去实现策略的接口,然后在这个父类里面去实现公用的功能;
也就是说,如果这个时候发现“一系列算法的实现步骤都是一样的”,只是在某些局部的步骤上有所不同的情况,那就可以在这个抽象类里卖弄定义算法实现的骨架,然后让具体的策略算法去实现变化的部分。。这样的一个结构,自然就变成了策略模式来结合模板方式模式了,那个抽象类就变成了模板方式模式的模板类;
【策略模式的使用场景】
(1)如果在一个系统里面有许多的类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态的让一个对象在许多行为中选择一种行为;
(2)如果系统需要动态的在几种算法中选择一种,那么这些算法可以包装到一个个的具体算法类里面,而这些算法类都是一个抽象算法类的子类。换言之,这些具体的算法类均有一个统一的接口,由于动态性原则,客户端可以使用任何一个具体算法类,并只持有一个数据类型是抽象算法类的对象;
(3)一个系统的算法使用的数据不可以让客户端知道,策略模式可以避免让客户端涉及到不必要接触到的复杂的和只与算法有关的数据。
(4)如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重条件选择语句来实现,此时,使用策略模式,把这些行为都转移到相应的策略类里面,就可以避免使用难以维护的条件选择语句,并体现出面向对象的设计原则;
【策略模式的优点】
(1)定义了一系列算法,避免多重条件语句;
(2)更好的扩展性;
【策略模式的缺点】
(1)客户必须了解每种策略的不同;
(2)增加了对象数目;
(3)只适合扁平的算法结构;
【策略模式的本质】
策略模式的本质:分离算法,选择实现。
策略模式很好地体现了开——闭原则。策略模式通过把一系列可变的算法进行封装,并定义出合理的使用结构,使得在系统出现新算法的时候,能够很容易的把新的算法加入到已有系统中,而已有的实现不需要做任何修改。
【策略模式和状态模式】
这两个模式从模式结构上看是一样的,但是实现的功能是不一样的。状态模式是根据状态的变化来选择相应的行为,不同的状态对应不同的类,每个状态对应的类实现了该状态对应的功能,在实现功能的同时,还会维护状态数据的变化。这些实现状态对应的功能的类之间是不能相互替换的;
策略类是根据需要或者是客户端的要求来选择相应的实现类,各个实现类是平等的,是可以相互替换的。另外,策略模式可以让客户端来选择需要使用的策略算法,而状态模式一般是由上下文,或者是在状态实现类里面来维护具体的状态数据,通常不由客户端来指定;
【策略模式和享元模式】
这两个模式可以组合使用,策略模式分离并封装出一系列的策略算法对象,这些对象的功能通常比较单一,很多时候就是为了实现某个算法的功能而存在,因此,针对这一系列的、多个细粒度的对象,可以应用享元模式来节省资源,但前提是这些算法对象需要被频繁的使用,如果偶尔使用一次,就没有必要做成享元了;
模板方式模式定义一个操作中算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定的步骤。
【三类模板方式】
AbstractClass叫做抽象模板:它的方法分为如下几类:
(1)基本方法:
基本方法也称为基本操作,是由子类实现的方法,并且在模板方法中被调用;
(2)模板方法:
模板方法可以有一个或者几个,一般是一个具体的方法,也就是一个骨架,实现对基本方法的调度,完成固定的逻辑。为了防止恶意的操作,一般模板方法都加上final关键字,不允许被覆写;
(3)钩子方法:
钩子方法由抽象类声明并加以实现,但是子类可以去扩展,子类可以扩展钩子方法来影响模板方法的逻辑。抽象类的任务是搭建逻辑的框架,通常由经验丰富的人员编写,因为抽象类的好坏直接决定了程序是否稳定;
钩子方法可以做到由子类的一个方法返回值来决定公共部分的执行结果;
模板方式模式就是在模板方法中按照一定的规则和顺序调用基本方法。
【模板方式模式总结】
(1)容易扩展;
一般来讲,抽象类中的模板方法是不易改变的部分,而抽象方法是容易发生变化的部分,因此通过增加实现类,一般可以很容易实现功能的扩展,符合开闭原则;
(2)便于维护;
对于模板方式来说,正是由于它们的主逻辑相同,才使用了模板方法,如果不使用模板方法,则维护起来很不方便;
(3)比较灵活;
因为有了钩子方法,因此,子类的实现也可以影响父类中主逻辑的运行。但是,在灵活性的同时,由于子类影响到了父类,违反了里氏替换原则,也会给程序带来风险。这就是对抽象类的设计有了更高的要求;
在多个子类拥有相同的方法,并且这些方法逻辑相同的时候,可以考虑使用模板方式模式。
在程序的主框架相同,细节不同的场合,也比较适合使用该模式;
访问者模式也称为Visitor模式,使用这种模式以后可以在不修改已有程序结构的前提下,通过添加额外的“访问者”来对已有代码的功能实现提升。
【访问者模式的结构】
定义:
表示一个作用于某对象解耦股中的各元素的操作。它是我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。从定义可以看出,结构对象是使用访问者模式的必须条件,而且这个结构对象必须存在遍历自身各个对象的方法。
角色:
(1)访问者角色(Visitor):为该对象结构中的具体元素角色声明一个访问操作接口。
该操作接口的名字和参数标识了发送访问请求给具体访问者的具体元素角色。这样访问者就可以通过该元素角色的特定接口直接访问它;
(2)具体访问者角色(Concrete Visitor):
实现每个由访问者角色(Visitor)声明的操作。
(3)元素角色(Element):
定义一个Accept操作,它以一个访问者为参数;
(4)具体元素角色(Concrete Element):
实现由元素角色提供的Accept操作;
(5)对象结构角色(Object Structure):
这是使用访问者模式以允许该访问者访问它的元素;可以是一个复合(组合模式)或者是一个集合,如一个列表或者一无序集合;
访问者模式的实现步骤:
(1)定义具有继承关系的数据结构对象群,相当于Element和ConcreteElement角色,并定义accept(Visitor)方法接受Visitor访问;
(2)定义包含上述数据结构对象群的复合结构对象,相当于是ObjectStructure角色;
(3)定义Visitor抽象接口,定义所有访问行为方法,相当于Visitor角色;
(4)定义具体的访问者对象,并实现所有的visit方法,相当于ConcreteVisitor角色;
【使用访问者模式的情况】
(1)一个对象结构包含很多类对象,它们有很多的接口,而我们想对这些对象实施一些依赖其具体类的操作;
(2)需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而我们想避免这些操作“污染”这些对象的类。Visitor使得我们可以将相关的操作集中起来,定义在一个类中。
(3)当对象结构被很多应用共享的时候,Visitor模式让每个应用仅包含需要用到的操作;
(4)定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。
【访问者模式的优点】
(1)访问者模式使得增加新的操作变得容易。
(2)访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个节点类中;
(3)访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。迭代器只能访问属于同一个类型等级结构的成员对象,而不能访问属于不同等级结构的对象;
(4)积累状态,每一个单独的访问者对象都集中了相关的额行为,从而也就可以在访问的过程中将执行操作的状态积累在自己内部,而不是分散到很多的节点对象中;
【访问者模式的缺点】
(1)增加新的节点变得很困难。每增加一个节点都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作;
(2)破坏封装。访问者模式要求访问者对象访问并调用每一个节点对象的操作,这隐含了一个对所有节点对象的要求:它们必须暴露一些自己的操作和内部状态。不然,访问者的访问就变得没有意义。由于访问者对象自己会积累访问操作所需的状态,从而使这些状态不在存储在节点对象中,这也是破坏封装的;