- 创建型模式: 对象实例化的模式,创建型模式用于解耦对象的实例化过程。
- 结构型模式: 把类或对象结合在一起形成一个更大的结构。
- 行为型模式: 类和对象如何交互,及划分责任和算法。
- 模板模式:定义一个算法结构,而将一些步骤延迟到子类实现。
- 迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
- 策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。
- 状态模式:允许一个对象在其对象内部状态改变时改变它的行为。
- 观察者模式:对象间的一对多的依赖关系。
- 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。
- 备忘录模式:在不破坏封装的前提下,保持对象的内部状态。
- 中介者模式:用一个中介对象来封装一系列的对象交互。
- 命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
- 访问者模式:在不改变数据结构的前提下,增加作用于一组对象元素的新功能。
- 责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
设计模式
根据目的分类:
创建型,:单例模式
结构型,:适配器模式(一个接口与其他接口兼容)、桥接(将对象的抽象和其实现分离)、组合(如何构造一个类层次式结构,部分-整体)、代理(通过提 供与对象相同的接口来控制对这个对象的访问)、享元(运用共享技术有效支持大量细粒度对象,解决使用大量对象造成很大存储开销的情况,内蕴,外蕴)、外观(如何用单个对象表示整个子系统)、装饰模式(描述如何动态为对象添加职责)
行为型
命令模式(将请求封装在对象中作为参数传递)、策略模式(将算法封装在对象中,以方便地指定一个对象所使用的算法,灵活地添加对同一问题的不同处理方案。用于多个类只区别于表现行为不同)
备忘录模式(保存和跟踪对象的状态,originator,memento)
状态模式(替换ifelse ,context,state, concrete state)
生成器模式(将一个复杂对象的构建与其表示分离,用于构建复杂对象,对象不同表示)
抽象工厂(为gui定义不同平台的并行类层次结构)
关联的多重度是指一个类的实例能与另一个类的多少个实例相关联。
候选类的选择运用良性依赖原则,不会在实际中造成危害的依赖关系。
候选类的删除运用接口隔离原则(ISP)。不应该强迫客户依赖于他们不用的方法,接口属于客户,不属于它所在的类层次结构。
原型模式:用原型实体指定创建对象的种类,并且通过复制这些原型创建新的对象。
创建者模式
单例模式
单例模式是一种对象创建型模式,使用单例模式,可以保证为一个类只生成唯一的实例对象。也就是说,在整个程序空间中,该类只存在一个实例对象。GoF对单例模式的定义是:保证一个类、只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。
应用场景:
- 在多个线程之间,比如servlet环境,共享同一个资源或者操作同一个对象
- 在整个程序空间使用全局变量,共享资源
- 大规模系统中,为了性能的考虑,需要节省对象的创建时间等等。
优点
- 于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级的对象而言,是很重要的.
- 不需要频繁创建对象,GC压力也减轻了,而在GC中会有STW(stop the world),从这一方面也节约了GC的时间
缺点
简单的单例模式设计开发都比较简单,但是复杂的单例模式需要考虑线程安全等并发问题,引入了部分复杂度。
设计单例模式的时候一般需要考虑几种因素:线程安全;延迟加载 ;代码安全:如防止序列化攻击,防止反射攻击(防止反射进行私有方法调用) ;性能因素
饿汉式 static final field
1 | public class Singleton |
Singleton类被加载的时候就会被初始化,java虚拟机规范虽然没有强制性约束在什么时候开始类加载过程,但是对于类的初始化,虚拟机规范则严格规定了有且只有四种情况必须立即对类进行初始化,遇到new、getStatic、putStatic或invokeStatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。 生成这4条指令最常见的java代码场景是:
- 使用new关键字实例化对象
- 读取一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)
- 设置一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)
- 调用一个类的静态方法
懒汉式(线程不安全)
1 | public class Singleton |
懒汉式(线程安全)
1 | public static synchronized Singleton getInstance() |
双重检验锁 double check lock
1 | public class Singleton |
延迟初始化。
synchronized同步块里面能够保证只创建一个对象。但是通过在synchronized的外面增加一层判断,就可以在对象一经创建以后,不再进入synchronized同步块。这种方案不仅减小了锁的粒度,保证了线程安全,性能方面也得到了大幅提升。
volatile在这里是用来防止指令重排序的。
静态内部类 static nested class
1 | public class Singleton |
基于类初始化的延迟加载解决方案。(既保证了线程的安全,有能够延迟加载,也就是在第一次使用的时候加载)一个线程初始化时,另一个线程会被锁到Class对象的初始化那里,无法进入到
- 分配对象的内存空间
- 设置instance指向内存空间
- 初始化对象
这些步骤里。
这种写法仍然使用 JVM 本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
枚举 Enum
1 | public enum Singleton |
enum没有无参构造器,所以无法用反射方法破坏单例。
1 | public enum t { |
序列化破坏单例模式原理解析及解决方案
反序列化的时候是用反射创建的,所以不一样
解决方法,在类里加个方法, readResolve这个名字是ObjectStream规定好的
1 | private Object readResolve(){ |
反射攻击解决方案及原理分析
通过反射来修改获取、修改构造函数,然后获取方法
1 | Class objectClass = HungrySingleton.class; |
1 | 在类加载的时候就初始化好对象的,两种 |
对于懒汉,这种后初始化对象的,如果反射先进来,就会有两个对象,在构造器里加判断也没有;反射后进来这种方法还有用。
容器单例
1 | public class ContainerSingleton |
这种方式实现的单例是线程不安全的。如果需要线程安全的可以使用HashTable但是HashTable每次存取都会加上同步锁,性能损耗比较严重。或者使用ConcurrentHashMap。
ThreadLocal线程单例
这个单例严格意义上讲并不完全算是单例,它只能算在单个线程中的单例,也就是在同一个线程中的它是单例的。
工厂方法模式
简单工厂
简单工厂并不是一个设计模式,而是一种编程习惯。
logger、calendar等类是这个模式
作为抽象工厂模式的孪生兄弟,工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个,也就是说工厂方法模式让实例化推迟到子类。
工厂方法模式非常符合“开闭原则”,当需要增加一个新的产品时,我们只需要增加一个具体的产品类和与之对应的具体工厂即可,无须修改原有系统。同时在工厂方法模式中用户只需要知道生产产品的具体工厂即可,无须关系产品的创建过程,甚至连具体的产品类名称都不需要知道。虽然他很好的符合了“开闭原则”,但是由于每新增一个新产品时就需要增加两个类,这样势必会导致系统的复杂度增加。
Collection的Iterator方法是工厂方法模式。
抽象工厂模式
所谓抽象工厂模式就是提供一个接口,用于创建相关或者依赖对象的家族,而不需要明确指定具体类。他允许客户端使用抽象的接口来创建一组相关的产品,而不需要关系实际产出的具体产品是什么。这样一来,客户就可以从具体的产品中被解耦。
它的优点是隔离了具体类的生成,使得客户端不需要知道什么被创建了,而缺点就在于新增新的行为会比较麻烦,因为当添加一个新的产品对象时,需要更加需要更改接口及其下所有子类。
建造者模式
对于建造者模式而已,它主要是将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。适用于那些产品对象的内部结构比较复杂。
建造者模式将复杂产品的构建过程封装分解在不同的方法中,使得创建过程非常清晰,能够让我们更加精确的控制复杂产品对象的创建过程,同时它隔离了复杂产品对象的创建和使用,使得相同的创建过程能够创建不同的产品。但是如果某个产品的内部结构过于复杂,将会导致整个系统变得非常庞大,不利于控制,同时若几个产品之间存在较大的差异,则不适用建造者模式,毕竟这个世界上存在相同点大的两个产品并不是很多,所以它的使用范围有限。
原型模式
用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。
原型模式有三种角色:
- 1.客户(Client)角色:该角色提出创建对象的请求。
- 2.抽象原型(Prototype):该角色是一个抽象角色,通常由一个java接口或抽象类实现,给出所有的具体原型类所需要的接口。
- 3.具体原型(Concrete Prototype)角色:该角色是被复制的对象,必须实现抽象原型接口。
java中内置了克隆机制。object类具有一个clone()方法,能够实现对对象的克隆,是一个类支持克隆只需要两步:
1.实现Cloneable接口。
2.覆盖Object的clone()方法,完成对象的克隆操作,通常只需要调用Object的clone()方法即可。为了使外部能够调用此类的clone()方法,可以将访问修饰符改为public。
1 | /** |
优点
- 1.性能优良:原型模式是对内存中二进制流的拷贝,要比直接new一个对象性能好,特别是当一个循环体内产生大量的对象时,原型模式可以更好地体现其优点。
- 2.逃避构造函数的约束。这既是优点也是缺点,直接在内存中拷贝对象,构造函数是不会执行的,因此减少了约束,不过这一点需要在实际应用时进行权衡考虑。
使用场景
- 1.资源优化场景,类初始化时需要消化非常多的资源,这个资源包括数据、硬件资源等。
- 2.性能和安全要求的场景,如果通过new产生一个对象需要非常繁琐的数据准备和访问权限,则可以使用原型模式。
- 3.一个对象多个修改者的场景,一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值,可以考虑使用原型模式拷贝多个对象供调用者使用。
在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现。原型模式通过clone()方法创建一个对象,然后由工厂方法提供给调用者。
结构型模式
适配器模式
- 类适配器(继承实现)
通过多重继承目标接口和被适配者类方式来实现适配。
多重继承,其中继承的目标接口部分达到适配目的,而继承被适配者类的部分达到通过调用被适配者类里的方法来实现目标接口的功能。但是java是不支持多重继承的,但是可以继承类然后继承接口,算是间接的实现了多重继承吧。
- 对象适配器(委让实现)
对象适配器和类适配器使用了不同的方法实现适配,对象适配器使用组合,类适配器使用继承.
在我们的应用程序中我们可能需要将两个不同接口的类来进行通信,在不修改这两个的前提下我们可能会需要某个中间件来完成这个衔接的过程。这个中间件就是适配器。所谓适配器模式就是将一个类的接口,转换成客户期望的另一个接口。它可以让原本两个不兼容的接口能够无缝完成对接。
作为中间件的适配器将目标类和适配者解耦,增加了类的透明性和可复用性。
这个举一个项目的例子:用火鸡冒充鸭子。就是外在是鸭子,其实内在是火鸡,就是用火鸡冒充鸭子。首先通过类对象适配器的方式来实现
1 | public interface Turkey |
1 | 对象适配器 |
1 | 类适配器 |
对象适配器模式其实是通过使用对象组合,以修改的接口去包装被适配者,所以他不仅可以适配某个类,而且可以适配该类的任何子类;而类适配器需要多重的继承。
其实就是改了下外在,但是内在不变,有点像装饰者模式哈,因为就像给火鸡做了一层外包装。但是装饰者模式和适配器模式还是不一样的,一方面装饰者模式是可以继承和嵌套超类的,但是这里的适配器模式是只能继承某一接口,并且适配器模式是接口功能的改变,不能扩展,但是装饰者模式是为了实现某些功能的扩展,举个例子吧,比如usb转换器这个东西,我们要给他添加什么电容保护什么功能的时候用装饰者模式,然后我们要将其转换成type C 的接口就要用适配器模式,也就是让他适配type C的功能模式。
Java I/O 库大量使用了适配器模式,如 ByteArrayInputStream
是一个适配器类,它继承了 InputStream
的接口,并且封装了一个 byte 数组。换言之,它将一个 byte 数组的接口适配成 InputStream 流处理器的接口。
在 OutputStream
类型中,所有的原始流处理器都是适配器类。ByteArrayOutputStream
继承了 OutputStream
类型,同时持有一个对 byte 数组的引用。它一个 byte 数组的接口适配成 OutputString 类型的接口,因此也是一个对象形式的适配器模式的应用。
FileOutputStream
继承了 OutputStream
类型,同时持有一个对 FileDiscriptor
对象的引用。这是一个将 FileDiscriptor
接口适配成 OutputStream
接口形式的对象型适配器模式。
Reader
类型的原始流处理器都是适配器模式的应用。StringReader
是一个适配器类,StringReader
类继承了 Reader
类型,持有一个对 String 对象的引用。它将 String 的接口适配成 Reader
类型的接口。
外观模式
- 门面角色:外观模式的核心。它被客户角色调用,它熟悉子系统的功能。内部根据客户角色的需求预定了几种功能的组合。
- 子系统角色:实现了子系统的功能。它对客户角色和Facade时未知的。它内部可以有系统内的相互交互,也可以由供外界调用的接口。
- 客户角色:通过调用Facede来完成要实现的功能。
组合模式
装饰模式
装饰模式介绍
装饰模式又名包装(Wrapper)模式。装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。就增加功能来说,Decorator模式比生成子类更为灵活。
- 抽象构件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。
- 具体构件(ConcreteComponent)角色:定义一个将要接收附加责任的类。
- 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。
- 具体装饰(ConcreteDecorator)角色:负责给构件对象“贴上”附加的责任。
1 | // 抽象接口,用来规范将要被附加一些操作的对象 |
装饰者模式与Java I/O
由上图可知在Java中应用程序通过输入流(InputStream)的Read方法从源地址处读取字节,然后通过输出流(OutputStream)的Write方法将流写入到目的地址。流的来源主要有三种
下面的图可以看出Java中的装饰者类和被装饰者类以及它们之间的关系,这里只列出了InputStream中的关系。
代理模式
为另一个对象提供一个替身或占位符以控制对这个对象的访问。
- Proxy 中有一个 RealSubject 对象,我们拿不到 RealSubject 对象,只能拿到 Proxy 对象;
- Proxy 和 RealSubject 都实现了 Subject 接口,它们有相同的方法;
- 我们通过 Proxy 对象调用 RealSubject 对象的方法,不过在调用前,Proxy 会先检查一下这个调用合不合理,不合理它就不调用 RealSubject 对象的方法。
静态代理
电影是电影公司委托给影院进行播放的,但是影院可以在播放电影的时候,产生一些自己的经济收益,比如卖爆米花、可乐等,然后在影片开始结束时播放一些广告。
1 | public interface Movie |
代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。
上面介绍的是静态代理的内容,为什么叫做静态呢?因为它的类型是事先预定好的,比如上面代码中的 Cinema 这个类。下面要介绍的内容就是动态代理。
动态代理
上一节代码中 Cinema 类是代理,我们需要手动编写代码让 Cinema 实现 Movie 接口,而在动态代理中,我们可以让程序在运行的时候自动在内存中创建一个实现 Movie 接口的代理,而不需要去定义 Cinema 这个类。这就是它被称为动态的原因。
1 | public interface Subject |
这在 AOP 面向切面编程领域经常见。
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
- Spring IOC 容器创建Bean(目标类对象);
- Bean创建完成后,Bean后处理器(BeanPostProcessor)根据具体的切面逻辑及Bean本身使用Java动态代理技术生成代理对象;
- 应用程序使用上述生成的代理对象替代原对象来完成业务逻辑,从而达到增强处理的目的。
主要功能
日志记录,性能统计,安全控制,事务处理,异常处理等等。
总结
- 代理分为静态代理和动态代理两种。
- 静态代理,代理类需要自己编写代码写成。
- 动态代理,代理类通过 Proxy.newInstance() 方法生成。
- 不管是静态代理还是动态代理,代理与被代理者都要实现两样接口,它们的实质是面向接口编程。
- 静态代理和动态代理的区别是在于要不要开发者自己定义 Proxy 类。
- 动态代理通过 Proxy 动态生成 proxy class,但是它也指定了一个 InvocationHandler 的实现类。
- 代理模式本质上的目的是为了增强现有代码的功能。
享元模式
系统底层开发,解决性能问题。
使用
- 数据库连接池
- java基础类库中大量使用了享元模式,如String、Integer、Boolean、Character等类都通过享元模式提供了内部的优化机制。
享元模式(Flyweight Pattern)是池技术的重要实现方式,它可以降低大量重复的、细粒度的类在内存中的开销。英文原话是:Use sharing to support large numbers of fine-grained objects efficiently.意思是:使用共享对象可有效地支持大量的细粒度的对象。
享元模式是以共享的方式高效地支持大量细粒度对象。享元对象能做到共享的关键是区分内部状态(Internal State)和外部状态(External State)。
- 内部状态是存储在享元对象内部的、可以共享的信息,并且不会随环境的改变而改变。
- 外部状态是随环境的改变而改变且不可以共享的状态。享元对象的外部状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。
享元角色分为四种角色:
- 抽象享元(Flyweight)角色:该角色对享元类进行抽象,需要外部状态的操作可以通过参数的形式将外部状态传入。
- 具体享元(ConcreteFlyweight)角色:该角色实现抽象享元定义的业务。注意享元对象的内部状态必须与环境无关,从而使得享元对象可以在系统内共享。
- 享元工厂(FlyweightFactory)角色:该角色就是构造一个池容器,负责创建和管理享元角色,并提供从池容器中获得享元对象的方法,保证享元对象可以被系统适当地共享。当一个客户端对象请求一个享元对象时,享元工厂角色会去检查系统中是否已经有一个符合要求的享元对象。如果已经有了,则享元工厂提供这个已有的享元对象;否则创建一个合适的享元对象。
- 客户端(Client)角色:该角色需要自行存储所有享元对象的外部状态。
使用享元模式的典型场景如下:
- 系统中有大量相似的对象,这些对象耗费大量的内存。
- 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,即对象没有特定身份。
- 需要缓冲池的场景。
享元模式的优缺点
享元模式的优点在于能大幅减少内存中对象的数量,降低程序内存的占用率,提高性能。但是,付出的代价也很高:
- 享元模式增加了系统的复杂性,需要分出外部状态和内部状态,而且内部状态具有固话特性,不能随外部状态改变而改变,这使得程序的逻辑复杂化。
- 享元模式将享元对象的状态外部化,而读取外部状态使得运行时间变长。
1 | Integer a = Integer.valueOf(100); |
1 | /** |
桥接模式
- 抽象和实现的分离,是桥梁模式的主要特点,桥梁模式是为了解决继承的缺点而提出的设计模式。在该模式下,实现可以不受抽象的约束,不用绑定在一个固定的抽象层次上。
- 实现对客户透明。客户端不用关心实现的细节,它已经由抽象层通过聚合关系完成了封装。
- 提高代码的灵活性和扩展性。
使用组合而不是继承,来连接两个类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71实现化角色
public interface Account {
Account openAccount();
void showAccountType();
}
具体实现化角色
public class DepositAccount implements Account
{
public Account openAccount() {
System.out.println("打开定期账号");
return new DepositAccount();
}
public void showAccountType() {
System.out.println("这是一个定期账号");
}
}
public class SavingAccount implements Account
{
public Account openAccount() {
System.out.println("打开活期账号");
return new SavingAccount();
}
public void showAccountType() {
System.out.println("这是一个活期账号");
}
}
抽象化角色
public abstract class Bank
{
protected Account account;
public Bank(Account account){
this.account = account;
}
abstract Account openAccount();
}
修正抽象化角色
public class ABCBank extends Bank
{
public ABCBank(Account account) {
super(account);调用父类的构造方法
}
Account openAccount() {
System.out.println("打开中国农业银行账号");
account.openAccount();
return account;
}
}
public class Test
{
public static void main(String[] args)
{
Bank icbcBank = new ICBCBank(new DepositAccount());
Account icbcAccount = icbcBank.openAccount();
icbcAccount.showAccountType();
Bank icbcBank2 = new ICBCBank(new SavingAccount());
Account icbcAccount2 = icbcBank2.openAccount();
icbcAccount2.showAccountType();
Bank abcBank = new ABCBank(new SavingAccount());
Account abcAccount = abcBank.openAccount();
abcAccount.showAccountType();
}
}
行为型模式
模板模式
迭代器模式
提供一种方法,访问一个容器对象中的各个元素,而又不暴露该对象的内部细节。
迭代器是为容器服务的,容器是指用来容纳其他对象的对象,例如:Collection集合类型、Set类型等。迭代器模式便于遍历和访问容器中的元素。
角色:
- 抽象迭代器(Iterator)角色:该角色负责定义访问和遍历元素的接口。
- 具体迭代器(Concrete Iterator)角色:该角色实现Iterator接口,完成容器元素的遍历。
- 抽象聚集(Aggregate)角色:该角色提供创建迭代器角色的接口。
- 具体聚集(Concrete Aggregate)角色:该角色实现抽象聚集接口,创建出容纳迭代器的对象。
1 | /*** 抽象迭代器*/ |
- 优点
- 迭代器模式简化了访问容器元素的操作,具备统一的遍历接口。
- 封装遍历算法,使算法独立于聚集角色。客户无需知道聚集对象的类型,即使聚集对象的类型发生变化,也不会影响遍历过程。
- 缺点
- 迭代器模式给使用者一种序列化的错觉,而产生错误。
- 迭代器的元素都是Object类型,没有类型特征(JDK1.5之后引入泛型可以解决此问题)。
策略模式
- Strategy: 定义所有支持的算法的公共接口抽象类.
- ConcreteStrategy: 封装了具体的算法或行为,继承于Strategy
- Context: 用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用。
Strategy模式有下面的一些优点:
- 相关算法系列 Strategy类层次为Context定义了一系列的可供重用的算法或行为。 继承有助于析取出这些算法中的公共功能。
- 提供了可以替换继承关系的办法: 继承提供了另一种支持多种算法或行为的方法。你可以直接生成一个Context类的子类,从而给它以不同的行为。但这会将行为硬行编制到 Context中,而将算法的实现与Context的实现混合起来,从而使Context难以理解、难以维护和难以扩展,而且还不能动态地改变算法。最后你得到一堆相关的类 , 它们之间的唯一差别是它们所使用的算法或行为。 将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。
- 消除了一些if else条件语句 :Strategy模式提供了用条件语句选择所需的行为以外的另一种选择。当不同的行为堆砌在一个类中时 ,很难避免使用条件语句来选择合适的行为。将行为封装在一个个独立的Strategy类中消除了这些条件语句。含有许多条件语句的代码通常意味着需要使用Strategy模式。
- 实现的选择 Strategy模式可以提供相同行为的不同实现。客户可以根据不同时间 /空间权衡取舍要求从不同策略中进行选择。
Strategy模式缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类: 本模式有一个潜在的缺点,就是一个客户要选择一个合适的Strategy就必须知道这些Strategy到底有何不同。此时可能不得不向客户暴露具体的实现问题。因此仅当这些不同行为变体与客户相关的行为时 , 才需要使用Strategy模式。
- Strategy和Context之间的通信开销 :无论各个ConcreteStrategy实现的算法是简单还是复杂, 它们都共享Strategy定义的接口。因此很可能某些 ConcreteStrategy不会都用到所有通过这个接口传递给它们的信息;简单的 ConcreteStrategy可能不使用其中的任何信息!这就意味着有时Context会创建和初始化一些永远不会用到的参数。如果存在这样问题 , 那么将需要在Strategy和Context之间更进行紧密的耦合。
- 策略模式将造成产生很多策略类:可以通过使用享元模式在一定程度上减少对象的数量。 增加了对象的数目 Strategy增加了一个应用中的对象的数目。有时你可以将 Strategy实现为可供各Context共享的无状态的对象来减少这一开销。任何其余的状态都由 Context维护。Context在每一次对Strategy对象的请求中都将这个状态传递过去。共享的 Strategy不应在各次调用之间维护状态。
从项目“模拟鸭子游戏开始”:
首先定义一个超类DUCK:
1 | public abstract class Duck |
现在我们有一个新的需求;
(1)添加会飞的鸭子
我们想的就是在DUCK这个类里面添加一个方法
1 | public abstract class Duck { |
然后这样的话就导致了所有的鸭子都是会飞的,那要是有些鸭子不会飞呢,
然后我们想的是在子类里面重写这个方法,例如
1 | public class GreenHeadDuck extends Duck { |
这样的话所有的不会飞的鸭子全部要改动,这样的话工作量很大,上面的设计思想就是我们经常会想到的方法,也就是用面向对象的方式去设计。
继承的问题:对类的局部改动,尤其超类的局部改动,会影响其他部分。影响会有溢出效应
而且超类挖的一个坑,每个子类都要来填,增加工作量,复杂度O(N^2)。不是好的设计方式
需要新的设计方式,应对项目的扩展性,降低复杂度:
1)分析项目变化与不变部分,提取变化部分,抽象成接口+实现;
2)鸭子哪些功能是会根据新需求变化的?叫声、飞行…
所以设计两个接口一个是飞行,一个是叫声
1 | public interface FlyBehavior { |
重新设计DUCK这个类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40public abstract class Duck
{
FlyBehavior mFlyBehavior;
QuackBehavior mQuackBehavior;
public Duck() {}
public void Fly()
{
mFlyBehavior.fly();
}
public void Quack()
{
mQuackBehavior.quack();
}
public abstract void display();
public void SetQuackBehavoir(QuackBehavior qb)
{
mQuackBehavior = qb;
}
public void SetFlyBehavoir(FlyBehavior fb)
{
mFlyBehavior = fb;
}
public void swim()
{
System.out.println("~~im swim~~");
}
}
重写DUCK的子类GreenHeadDuck :
public class GreenHeadDuck extends Duck
{
public GreenHeadDuck()
{
mFlyBehavior = new GoodFlyBehavior(); //实例化的时候必须要传入的参数,因为在具体的父类中会用到,并且在用的时候用的是父类中的方法,而且你不必去计较底层是怎么实现的
mQuackBehavior = new GaGaQuackBehavior();
}
@Override
public void display() {...}
}
状态模式
观察者模式
实例
Observer模式是行为模式之一,它的作用是当一个对象的状态发生变化时,能够自动通知其他关联对象,自动刷新对象状态。这样的话就不会错过该对象感兴趣的事情。对象甚至可以在运行时决定是否需要继续被通知。
Observer模式提供给关联对象一种同步通信的手段,使某个对象与依赖它的其他对象之间保持状态同步。
- Subject(被观察者)
被观察的对象。当需要被观察的状态发生变化时,需要通知队列中所有观察者对象。Subject需要维持(添加,删除,通知)一个观察者对象的队列列表。 - ConcreteSubject
被观察者的具体实现。包含一些基本的属性状态及其他操作。 - Observer(观察者)
接口或抽象类。当Subject的状态发生变化时,Observer对象将通过一个callback函数得到通知。 - ConcreteObserver
观察者的具体实现。得到通知后将完成一些具体的业务逻辑处理。
首先想到的是设计两个类,一个是天气的数据,一个就是公告板也就是对天气的数据进行操作的类
1 | public class CurrentConditions |
这样设计带来的问题是什么呢?
1)其他第三方公司接入气象站获取数据的问题
2)无法在运行时动态的添加第三方
也就是说我们想要再添加新的公告板的时候,同时需要更改WeatherData类,这样的话工作量变大,不符合设计要求。
根据上面观察者模式,我们重新设计我们的方案,也就是留出subject和observer两个接口,由WeatherDada和CurrentConditions分别继承实现,这样的话我再来新的公告板的话只需要扩展observer这个接口就可以了。
1 | public interface Observer |
这个时候我想要添加一个气象预报的公告板,只需要继承Observer接口然后在WeatherData中注册就可以了
1 | public class ForcastConditions implements Observer |
通过上面的例子我们可以看出通过观察者模式,我们可以轻松的扩展程序,扩展后只需要让观察者通知被观察者发生的改变即可。
其实观察者模式有两种的实现方式,就是推和拉的模式,推的模式其实就是有主题去通知观察者那些状态发生了改变,拉的方式就是有观察者自己去获取自己需要的状态的改变,对于不需要的不需要获取。
java内置的观察者模式
Java内置的观察者:
- Observable
- Observer
Observer模式的典型应用:
- 侦听事件驱动程序设计中的外部事件
- 侦听/监视某个对象的状态变化
- 发布者/订阅者(publisher/subscriber)模型中,当一个外部事件(新的产品,消息的出现等等)被触发时,通知邮件列表中的订阅者
解释器模式
备忘录模式
中介者模式
命令模式
访问者模式
责任链模式
多个对象都有处理请求的机会,从而避免了请求的发送者和接收者之间的耦合关系。将这些对象串成一条链,并沿着这条链一直传递该请求,直到有对象处理它为止。
角色
- 抽象处理者(Handler)角色:该角色对请求进行抽象,并定义一个方法来设定和返回对下一个处理者的引用。
- 具体处理者(Concrete Handler):该角色接到请求后,可以选择将请求处理掉,或者将请求传给下一个处理者。由于具体处理者持有对下一个处理者的引用,因此,如果需要,处理者可以访问下一个处理者。
优点
- 责任链模式将请求和处理分开,请求者不知道是谁处理的,处理者可以不用知道请求的全貌。
- 提高系统的灵活性。
缺点
- 降低程序的性能。每个请求都是从链头遍历到链尾,当链比较长的时候,性能会大幅下降。
- 不易于调试。由于该模式采用了类似递归的方式,调试的时候逻辑比较复杂。
1 | public class Course { |
源码应用:
servlet中的filterChain。
参考: