1、设计模式的七大原则
👻编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的
挑战,设计模式是为了让程序(软件),具有更好:
1)代码的重用性
2)可读性
3)可扩展性(即:当需要增加新的功能时,非常方便,也称为可维护性)
4)可靠性(即:新增加的功能后,对原有的功能没有影响)
5)使程序呈现高内聚,低耦合的特性
1.1 单一职责
1.1.1 基本介绍
对类来说的,即一个类应该只负责-项职责。如类A负责两个不同职责:职责1,职责2。当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为Al, A2
1.1.2 应用案例
public class test { public static void main(String[] args) { veichle v1 = new veichle(); v1.run("汽车"); v1.run("飞机"); } } class veichle{ public void run(String veichle){ System.out.println(veichle + "在公路上跑"); } } //这里run方法就违背单一原则 //1.应该根据交通方法不同,分解成不同类 class veichleCar{ public void run(String veichle){ System.out.println(veichle + "在公路上跑"); } }class veichleAir{ public void run(String veichle){ System.out.println(veichle + "在天上飞"); } } /*2.或者将run方法分为几种,这里虽然没有在类这个级别上遵守单一-职责原则,但是在方法级别上, 仍然是遵守单- - 职责*/ class veichle{ public void runCar(String veichle){ System.out.println(veichle + "在公路上跑"); } public void runAir(String veichle){ System.out.println(veichle + "在天上飞"); } }
1.1.3 小结
1) 降低类的复杂度,一个类只负责一项职责。
2)提高类的可读性,可维护性
3)降低变更引起的风险
4)通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足量少,可以在方法级别上保持单一原则
1.2 接口隔离原则
1.2.1 基本介绍
客户端不应该依赖它不需要的接口,即-一个类对另-一个类的依赖应该建立在最小的接口上。
1.2.2 应用案例
1) 类A通过接口Interfacel 依赖类B( A.depend(new B() ),类C通过接口Interfacel 依赖类D (C.depend(new D() ),如果接口Interface1 对于类A和类C来说不是最小接口,那么类B和类D必须去实现他们不需要的方法。
2) 按隔离原则应当这样处理:
将接口Interface1 拆分为独立的几个接口(这里我们拆分成3个接口),类A和类C分别与他们需要的接口建立依 赖关系。也就是采用接口隔离原则。
3) 处理方法:
1.3 依赖倒转原则
1.3.1 基本介绍
依赖倒转原则(Dependence Inversion Principle,DIP)是指:
1)高层模块不应该依赖低层模块,二者都应该依赖其抽象。
2)抽象不应该依赖细节,细节应该依赖抽象。
3)依赖倒转(倒置)的中心思想是面向接口编程。
4)依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类。
5)使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
1.3.2 应用案例
现在我们先不考虑依赖倒置原则,看一下如下的设计:
司机类和奔驰车类都属于细节,并没有实现或继承抽象,它们是对象级别的耦合。司机有一个drive()方法,用来开车,奔驰车有一个run()方法,用来表示车辆运行,并且奔驰车类依赖于司机类,用户模块表示高层模块,负责调用司机类和奔驰车类。
public class Driver { //司机的主要职责就是驾驶汽车 public void drive(Benz benz){ benz.run(); } } public class Benz { //汽车肯定会跑 public void run(){ System.out.println("奔驰汽车开始运行..."); } } //高层模块 public class Client { public static void main(String[] args) { Driver xiaoLi = new Driver(); Benz benz = new Benz(); //小李开奔驰车 xiaoLi.drive(benz); } }
这样的设计乍一看好像也没有问题,小李只管开着他的奔驰车就好。但是假如有一天他不想开奔驰了,想换一辆宝马车玩玩怎么办呢?我们当然可以新建一个宝马车类,也给它弄一个run()方法,但问题是,这辆车有是有了,但是小李却不能开啊。因为司机类里面并没有宝马车的依赖,所以小李空看着宝马车在那儿躺着,自己却没有钥匙,你说郁不郁闷呢?
public class BMW { //宝马车当然也可以开动了 public void run(){ System.out.println("宝马汽车开始运行..."); } }
上面的设计没有使用依赖倒置原则,我们已经郁闷的发现,模块与模块之间耦合度太高,生产力太低,只要需求一变就需要大面积重构,说明这样的设计是不合理。现在我们引入依赖倒置原则,重新设计的类图如下:
//将司机模块抽象为一个接口 public interface IDriver { //是司机就应该会驾驶汽车 public void drive(ICar car); } public class Driver implements IDriver{ //司机的主要职责就是驾驶汽车 public void drive(ICar car){ car.run(); } } //将汽车模块抽象为一个接口:可以是奔驰汽车,也可以是宝马汽车 public interface ICar { //是汽车就应该能跑 public void run(); } public class Benz implements ICar{ //汽车肯定会跑 public void run(){ System.out.println("奔驰汽车开始运行..."); } } public class BMW implements ICar{ //宝马车当然也可以开动了 public void run(){ System.out.println("宝马汽车开始运行..."); } } //高层模块 public class Client { public static void main(String[] args) { IDriver xiaoLi = new Driver(); ICar benz = new Benz(); //小李开奔驰车 xiaoLi.drive(benz); } }
在新增低层模块时,只修改了高层模块(业务场景类),对其他低层模块(Driver类)不需要做任何修改,可以把"变更"的风险降低到最低。在Java中,只要定义变量就必然有类型,并且可以有两种类型:表面类型和实际类型,表面类型是在定义时赋予的类型,实际类型是对象的类型。就如上面的例子中,小李的表面类型是IDriver,实际类型是Driver。
抽象是对实现的约束,是对依赖者的一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的就是保证所有的细节不脱离契约的范畴,确保约束双方按照规定好的契约(抽象)共同发展,只要抽象这条线还在,细节就脱离不了这个圈圈。就好比一场篮球比赛,已经定好了规则,大家如果按照规则来打球,那么会很愉快。但是假如大家脱离了规则,那么也许比赛就无法顺利进行。
1.3.3 DIP的几种写法
- 接口声明依赖对象: 在接口的方法中声明依赖对象,就如上面的例子。
- 构造函数传递依赖对象: 在类中通过构造函数声明依赖对象(好比Spring中的构造器注入),采用构造器注入。
//将司机模块抽象为一个接口 public interface IDriver { public void drive(); } public class Driver implements IDriver{ private ICar car; //注入 public void Driver(ICar car){ this.car = car; } public void drive(ICar car){ this.car.run(); } }
- Setter方法传递依赖对象: 在抽象中设置Setter方法声明依赖对象(Spring中的方法注入)
public interface IDriver{ //注入依赖 public void setCar(ICar car); public void drive(); } public class Driver implements IDriver{ private ICar car; public void setCar(ICar car){ this.car = car; } public void drive(){ this.car.run(); } }
总结:依赖倒置原则的核心就是面向抽象(抽象类或者接口)编程
1.4 里氏替换原则
对继承性的思考和说明
1.4.1 继承
继承作为面向对象三大特性之一,它具有如下特点:
- 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性
- 提高代码的重用性
- 子类可以形似父类
- 提高代码的可扩展性
- 提高产品或项目的开放性
同时也具有如下缺点:
- 继承具有侵入性,只要子类继承了父类,那么子类必须拥有父类的所有属性和方法
- 降低了代码的灵活性
- 增强了耦合性。当父类中发生方法,属性的修改时需要考虑子类是否修改,而且在缺乏规范的情况下,还可能发生大段的代码重构
为了最大减小化继承的缺点(这就是为什么说在开发时多用组合,少用继承),故引入里氏替换原则。
1.4.2 基本介绍
里式替换原则(LiskovSubstitution Principle, LSP):
- 第一种定义:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。
- 第二种定义:子类型必须能替换掉它们的基类型
第二种相对容易理解:通俗的说,只要是父类出现的地方子类都可以出现,并且可以用子类进行替换。但是反过来就不行,有子类出现的地方,父类未必可以替换。
1.4.3 LSP的深层含义
- 子类必须完全实现父类的方法, 但不能覆盖(重写)父类的非抽象方法:这个规则相对来说是很好理解的,我们定义了一个接口或抽象类,我们必须在子类中完全实现所有的抽象方法,其实这时我们已经使用了里式替换原则
public abstract class A{ public abstract void run(); public abstract void fly(); public void walk(){ .... } } class B extends A{ @Override public void run(){...} @Override public void fly(){...} } public calss test{ public static void main(String[] args){ A a = new B(); a.run(); a.fly(); a.walk(); } }
- 子类可以增加自己特有的方法
- 当子类的方法重载父类的方法时,子类方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
public class Father { public Collection doSomething(HashMap map) { System.out.println("父类被执行..."); return map.values(); } } public class Son extends Father { // 放大输入参数类型 public Collection doSomething(Map map) { System.out.println("子类被执行..."); return map.values(); } } public class Test { public static void invoker() { // 父类存在的地方,子类就应该能够存在 // Father f = new Father(); Son son = new Son(); HashMap map = new HashMap(); son.doSomething(map); } public static void main(String[] args){ invoker(); } } 两个输出结果都是:父类被执行... //假如将父类和子类的参数类型调换 则 f.doSomething(map) 输出结果为:父类被执行 son.doSomething(map) 输出结果为:子类被执行
解释如下:在上面的例子中,子类中的doSomething(Map map)和父类中的doSomething(HashMap map)两个方法构成重载(并不是重写,因为参数列表不同,子类继承父类那么相应的父类方法就存在于子类的生命周期中,所以构成重载),而子类方法的形参范围比父类方法的形参范围要大。其实我们可以想一想,子类方法的形参范围比父类方法的形参范围要大,则子类代替父类传递参数到调用者中,子类的方法将永远不会被执行,这其实和里式交换原则是想符合的,父类的空间必须是子类的子区间,那么子类才能替换父类。而假如父类方法的形参范围大于子类方法的形参范围,子类方法在没有重写父类方法的前提下被执行了,这会引起业务逻辑的混乱,因为在实际应用中父类一般是抽象类,子类是实现类,你传递了一个这样的实现类就会“歪曲”父类的意图,引起一堆意想不到的逻辑混乱,所以子类中方法的前置条件必须与超类中被覆写的方法的前置条相同或更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更加严格: 如父类要求返回List,那么子类就应该返回List的实现ArrayList,父类是采用泛型,那么子类则不能采用泛型,而是具体的返回。
1.4.4 小结
- 其实通俗说来,里式替换原则就是:子类可以扩展父类的功能,但不能改变父类原有的功能
- 当继承不能满足里式替换原则时应该进行重构:
-
- 把冲突的派生类与基类的公共部分提取出来作为一个抽象基类,然后分别继承这个类。
- 改变继承关系:从父子关系变为委托关系
- 在类中调用其他类时务必要使用父类或接口, 如果不能使用父类或接口, 则说明类的设计已经违背了LSP原则
- 如果子类不能完整地实现父类的方法, 或者父类的某些方法在子类中已经发生“畸变”, 则建议断开父子继承关系, 采用依赖、 聚集、 组合等关系代替继承
1.4.5 扩展:多态和LSP是否矛盾?
多态前提就是要子类继承父类,并且要重写父类的方法,明显这LSP原则冲突。
给出如下解释:
-
-
- 里式替换原则是针对继承而言的,如果继承是为了实现代码的重用,也 就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过添加新的方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时就可以使用子类对象将父类对象替换掉。
- 如果继承的目的是为了多态,而多态的前提就是子类重写父类的方法,为了符合LSP,我们应该将父类重新定义为抽象类,并定义抽象方法,让子类重新定义这些方法。由于父类是抽象类,所以父类不能被实例化,也就不存在可实例化的父类对象在程序里,就不存在子类替换父类时逻辑不一致的可能。
-
1.3 开闭原则
1.3.1 基本介绍
开闭原则是Java中最基础的设计原则,它指导我们如何建立一个稳定的,灵活的系统。
- 定义:一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。
- 为什么使用开闭原则: 在程序的生命周期内,因为变化,升级和维护等原因需要对程序原有的代码进行修改时,可能会给代码引入错误,增加项目开发测试的复杂度,也可能会使我们不得不对整个功能进行重构,而且还要对原有的代码进行测试。
1.3.2 开闭原则——我就是爸爸
简单总结五大原则就是:单一职责原则告诉我们实现类要职责单一;里式替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向抽象编程;接口隔离原则告诉我们设计接口要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则告诉我们:要对修改关闭,对扩展开放。其实只要我们想一想,前面的五大原则一直反复强调的,几乎每一个原则都在强调的宗旨是什么:解耦,单一,高内聚——这不就是开闭原则的精神纲领
1.4 迪米特法则
1.4.1 基本介绍
迪米特法则(Law of Demeter, LoD)主要强调一下两点:
- 从被依赖者的角度:只暴露应该暴露的方法或属性,即编写相关的类时确定方法和属性的权限
- 从依赖者的角度来看,只依赖应该依赖的对象
1.4.2 实例演示
先举例演示第一点:当我们按下计算机的按钮的时候,计算机会指行一系列操作:保存当前任务,关闭相关服务,接着关闭显示屏,最后关闭电源,这些操作完成则计算机才算关闭。如下是代码示例:
//计算机类 public class Computer{ public void saveCurrentTask(){ //do something } public void closeService(){ //do something } public void closeScreen(){ //do something } public void closePower(){ //do something } public void close(){ saveCurrentTask(); closeService(); closeScreen(); closePower(); } } //人 public class Person{ private Computer c; ... public void clickCloseButton(){ //现在你要开始关闭计算机了,正常来说你只需要调用close()方法即可, //但是你发现Computer所有的方法都是公开的,该怎么关闭呢?于是你写下了以下关闭的流程: c.saveCurrentTask(); c.closePower(); c.close(); //亦或是以下的操作 c.closePower(); //还可能是以下的操作 c.close(); c.closePower(); } }
观察上面的代码我们发现了什么问题:对于人来说,我期待的结果只是按下关闭电钮然后计算机“啪”的给我关了,而不是需要我去小心的去保存当前正在执行的任务等等。在上面的代码中,c是一个完全暴露的对象,它的方法是完全公开的,对于Person来说,手里面就如同多出了好几把钥匙,至于具体用哪一把他不知道,所以只能一把一把的去试一遍,显然这样的设计是不对的。
根据迪米特法则的第一点:从被依赖者的角度,只暴露应该暴露的方法。在本例中,应该暴露的方法就是close(),关于计算机的其他操作不是依赖者应该关注的问题,应该对依赖者关闭,重新设计如下:
//计算机类 public class Computer{ private void saveCurrentTask(){ //do something } private void closeService(){ //do something } private void closeScreen(){ //do something } private void closePower(){ //do something } public void close(){ saveCurrentTask(); closeService(); closeScreen(); closePower(); } } //人 public class Person{ private Computer c; ... public void clickCloseButton(){ c.close(); } }
现在举例演示第二点:在我们生活中会有这样的情况,比如张三去找李四帮忙做一件事,对于李四来说这件事也很棘手,李四也做不了,但是李四有一个好哥们王五却能完成这件事,所以李四就把这件事交给王五去办(在本例中,张三和王五是不认识的)。现在我们暂定张三为A,李四为B,王五为C,代码示例如下:
//张三找李四办事 public class A { public String name; public A(String name) { this.name = name; } public B getB(String name) { return new B(name); } public void work() { B b = getB("李四"); C c = b.getC("王五"); c.work(); } } //李四办不了于是去找王五 public class B { private String name; public B(String name) { this.name = name; } public C getC(String name) { return new C(name); } } //对于王五来说so easy,办得妥妥的 public class C { public String name; public C(String name) { this.name = name; } public void work() { System.out.println(name + "把这件事做好了"); } } public class Client { public static void main(String[] args) { A a = new A("张三"); a.work(); } } 结果:王五把事情做好了
上面的设计输出答案是正确的,王五确实把事情办妥了。但是我们仔细看业务逻辑确发现这样做事不对的。张三和王五互相不认识,那为什么代表张三的A类中会有代表李四的C类呢?这样明显是违背了迪米特法则的。现在我们对上面的代码进行重构,根据迪米特法则的第二点:从依赖者的角度来看,只依赖应该依赖的对象。在本例中,张三只认识李四,那么只能依赖李四。重构后代码如下:
//张三认识李四,只依赖李四 public class A { public String name; public A(String name) { this.name = name; } public B getB(String name) { return new B(name); } public void work() { B b = getB("李四"); b.work(); } } //李四依赖王五 public class B { private String name; public B(String name) { this.name = name; } public C getC(String name) { return new C(name); } public void work(){ C c = getC("王五"); c.work(); } } //王五把事情办得妥妥的 public class C { public String name; public C(String name) { this.name = name; } public void work() { System.out.println(name + "把这件事做好了"); } } public class Client { public static void main(String[] args) { A a = new A("张三"); a.work(); } } 结果:王五把事情做好了
总结:迪米特法则的目的是让类之间解耦,降低耦合度,提高类的复用性。但是设计原则并非有利无弊,使用迪米特法则会产生大量的中转类或跳转类,导致系统复杂度提高。在实际的项目中,需要适度的考虑这个原则,不能因为套用原则而反而使项目设计变得复杂。
2. 设计模式
2.1 单例模式
2.1.1 基本介绍
所谓类的单例设计模式,就是采取-定的方法保证在整个的软件系统中,对某个类只能存在-个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
2.1.2 饿汉式——线程安全的单例模式
饿汉式:在程序启动或单例模式类被加载的时候,单例模式实例就已经被创建。
public class Singleton1 { private static Singleton1 instance = new Singleton1(); private Singleton1(){} public static Singleto1 getInstnance() { return instance; } } public class test { public static void main(String[] args){ Singleton s1 = Singleton1.getInstnance(); Singleton s2 = Singleton1.getInstnance(); System.out.println(s1 == s2); //true } }
优点:在类装载的时候就完成实例化。避免了线程同步问题,线程安全;
缺点:类加载的时候已经实例化,浪费空间;
2.1.3 懒汉式
懒汉式:当程序第一次访问单例模式实例时才进行创建。
(1)懒汉式V1
public class LazySingleton1() { private static LazySingleton1 instance; private LazySingleton1(){} public static LazySingleton1 getInstance() { if (instance == null) { instance = new LazySingleton1(); } return instance; } }
线程不安全,故此给方法加锁得到如下方式:
public static synchronized LazySingleton1 getInstance() { if (instance == null) { instance = new LazySingleton1(); } return instance; }
由于将synchronized关键字加在方法上会造成线程阻塞(同步),在性能上会大打折扣。
推荐:所以使用双重校验锁(在保证线程安全的同时提高了性能)得到如下方式:
public class LazySingleton1() { private static volatile LazySingleton1 instance; private LazySingleton1() {}; public static LazySingleton1 getInstance() { if (instance == null) { synchronized (LazySingleton1.class) { if (instance == null) { instance = new LazySingleton1(); } } } return instance; } } class LazySingleton{ private static volatile LazySingleton instance; private LazySingleton(){}; public LazySingleton getInstance(){ if(instance == null){ synchronized(LazySingleton.class){ if(instance == null){ instance = new LazySingleton(); } } } return instance; } }
(1)为什么 getInstance() 方法内需要使用两个 if (singleton == null) 进行判断呢?
假设高并发下,线程A、B 都通过了第一个 if 条件。若A先抢到锁,new 了一个对象,释放锁,然后线程B再抢到锁,此时如果不做第二个 if 判断,B线程将会再 new 一个对象。使用两个 if 判断,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗。
(2)volatile 关键字的作用?
volatile 的作用主要是禁止指令重排序。假设在不使用 volatile 的情况下,两个线程A、B,都是第一次调用该单例方法,线程A先执行 singleton = new Singleton(),但由于构造方法不是一个原子操作,编译后会生成多条字节码指令,由于 JAVA的 指令重排序,可能会先执行 singleton 的赋值操作,该操作实际只是在内存中开辟一片存储对象的区域后直接返回内存的引用,之后 singleton 便不为空了,但是实际的初始化操作却还没有执行。如果此时线程B进入,就会拿到一个不为空的但是没有完成初始化的singleton 对象,所以需要加入volatile关键字,禁止指令重排序优化,从而安全的实现单例。
(2)懒汉式V2(常用)
回到第一版的双重校验锁。其实不管性能如何优越,还是使用了synchronized关键字,那么对性能始终是有影响的(有兴趣的话可以了解一下synchronized的内存模型与底层原理)。所以,下面给出了改良版本——使用静态内部类的方式。
public class LazySingleton2 { private LazySingleton2() { } //静态内部类 static class SingletonHolder { private static final lazySingleton2 instance = new LazySingleton2(); } public static LazySingleton2 getInstance() { return SingletonHolder.instance; } } 2 class MyThread extends Thread{ @Override public void run() { System.out.println(LazySingleton2.getInstance().hashCode()); } } class Run{ public static void main(String[] args) { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); MyThread t3 = new MyThread(); t1.start(); t2.start(); t3.start(); } } 结果: 1385166630 1385166630 1385166630
- 由于对象实例化是在内部类加载的时候构建的,因此是线程安全的(在方法中创建对象才存在并发问题,静态内部类随着方法的调用而被加载,只加载一次,不存在线程安全问题)。
- 在getInstance()方法中没有使用synchronized关键字,因此没有造成多余的性能损耗。当LazySingleton2类加载的时候,其静态内部类SingletonHolder并没有被加载,因此instance对象没有被构建。
- 而我们在调用LazySingleton2.getInstance()方法时,内部类SingletonHolder被加载,此时单例对象才被构建。因此,这种写法节约空间,达到了懒加载的目的。
(3)懒汉式V3
使用静态内部类的懒加载方式虽然具有很多优良特性,但是在反射的作用下,单例结构还是会被破坏:
故此诞生第三版懒汉式:
public class LazySingleton3 { private static boolean initialized = false; private LazySingleton3() { synchronized (LazySingleton3.class) { if (initialized == false) { initialized = !initialized; } else { throw new RuntimeException("单例已经被破坏!"); } } } static class SingletonHolder { private static final LazySingleton3 instance = new LazySingleton3(); } public static LazySingleton3 getInstance() { return SingletonHolder.instance; } } 经过测试后输出为: java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.yuangh.concurrent4.demo3.LazySingleton2Test.main(LazySingleton2Test.java:22) Caused by: java.lang.RuntimeException: 单例已经被破坏! at com.yuangh.concurrent4.demo3.LazySingleton3.<init>(LazySingleton3.java:16) ... 5 more Exception in thread "main" java.lang.NullPointerException at com.yuangh.concurrent4.demo3.LazySingleton2Test.main(LazySingleton2Test.java:28) Instance1's hashcode:1836019240
这就保证了反射无法破坏其单例性。
(4)懒汉式V4
在分布式系统中,有些情况下需要在单例中实现Serializable接口。这样就可以在文件系统中存储它的状态并且在稍后的某一时间点取出。
错误版本:
public class LazySingleton3 implements Serializable{ private static boolean initialized = false; private LazySingleton3() { synchronized (LazySingleton3.class) { if (initialized == false) { initialized = !initialized; } else { throw new RuntimeException("单例已经被破坏!"); } } } static class SingletonHolder { private static final LazySingleton3 instance = new LazySingleton3(); } public static LazySingleton3 getInstance() { return SingletonHolder.instance; } } public class LazySingleton3Test { public static void main(String[] args) { try { //将对象序列化到文件 LazySingleton3 instance1 = LazySingleton3.getInstance(); ObjectOutput out = null; out = new ObjectOutputStream(new FileOutputStream("filename.ser")); out.writeObject(instance1); out.close(); //从文件反序列化到对象 ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser")); LazySingleton3 instance2 = (LazySingleton3) in.readObject(); in.close(); System.out.println("instance1's hashcode:" + instance1.hashCode()); System.out.println("instance2's hashcode:" + instance2.hashCode()); } catch (Exception e) { e.printStackTrace(); } } } 得到结果为: instance1's hashcode:856419764 instance2's hashcode:1480010240
显然,出现了两个实例类,说明破坏了单例模式。因此重写readResolve() 方法,使得readResolve()代替了从流中读取对象。这就确保了在序列化和反序列化的过程中没人可以创建新的实例。下面是懒汉式第四个版本:
public class LazySingleton4 implements Serializable { private static boolean initialized = false; private LazySingleton4() { synchronized (LazySingleton3.class) { if (initialized == false) { initialized = !initialized; } else { throw new RuntimeException("单例已经被破坏!"); } } } static class SingletonHolder { private static final LazySingleton4 instance = new LazySingleton4(); } public static LazySingleton4 getInstance() { return SingletonHolder.instance; } private Object readResolve() { return getInstance(); } } 测试后输出结果为: instance1's hashcode:856419764 instance2's hashcode:856419764
2.2 工厂模式
2.2.1 什么是工厂模式
工厂模式将目的将创建对象的具体过程屏蔽隔离起来,从而达到更高的灵活性,工厂模式可以分为三类:
- 简单工厂模式(Simple Factory)
- 工厂方法模式(Factory Method)
- 抽象工厂模式(Abstract Factory)
2.2.2 简单工厂模式
简单工厂模式的核心是定义一个创建对象的接口,将对象的创建和本身的业务逻辑分离,降低系统的耦合度,使得两个修改起来相对容易些,当以后实现改变时,只需要修改工厂类即可。
工厂模式的核心思想是将对象的创建过程封装在一个工厂类中。这个工厂类负责创建对象,并将其返回给调用者。这样,调用者就不需要知道对象的创建过程,只需要知道如何使用这个对象即可。
如果不使用工厂,用户将自己创建宝马车,具体UML图和代码如下:
public class BMW320 { public BMW320(){ System.out.println("制造-->BMW320"); } } public class BMW523 { public BMW523(){ System.out.println("制造-->BMW523"); } } public class Customer { public static void main(String[] args) { BMW320 bmw320 = new BMW320(); BMW523 bmw523 = new BMW523(); } }
用户需要知道怎么创建一款车,这样子客户和车就紧密耦合在一起了,为了降低耦合,就出现了简单工厂模式,把创建宝马的操作细节都放到了工厂里,而客户直接使用工厂的创建方法,传入想要的宝马车型号就行了,而不必去知道创建的细节。
简单工厂模式的UML图:
- 工厂类角色: 该模式的核心,用来创建产品,含有一定的商业逻辑和判断逻辑
- 抽象产品角色:它一般是具体产品继承的父类或者实现的接口。
- 具体产品角色:工厂类所创建的对象就是此角色的实例。在java中由一个具体类实现。
//产品类 abstract class BMW { public BMW(){} } public class BMW320 extends BMW { public BMW320() { System.out.println("制造-->BMW320"); } } public class BMW523 extends BMW{ public BMW523(){ System.out.println("制造-->BMW523"); } } //工厂类 public class Factory { public BMW createBMW(int type) { switch (type) { case 320: return new BMW320(); case 523: return new BMW523(); default: break; } return null; } } //用户类 public class Customer { public static void main(String[] args) { Factory factory = new Factory(); BMW bmw320 = factory.createBMW(320); BMW bmw523 = factory.createBMW(523); } }
优缺点:
简单工厂模式提供专门的工厂类用于创建对象,实现了对象创建和使用的职责分离,客户端不需知道所创建的具体产品类的类名以及创建过程,只需知道具体产品类所对应的参数即可,通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
但缺点在于不符合“开闭原则”,每次添加新产品就需要修改工厂类。在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展维护,并且工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
2.2.3 工厂方法模式
工厂方法模式将工厂抽象化,并定义一个创建对象的接口。每增加新产品,只需增加该产品以及对应的具体实现工厂类,由具体工厂类决定要实例化的产品是哪个,将对象的创建与实例化延迟到子类,这样工厂的设计就符合“开闭原则”了,扩展时不必去修改原来的代码。在使用时,用于只需知道产品对应的具体工厂,关注具体的创建过程,甚至不需要知道具体产品类的类名,当我们选择哪个具体工厂时,就已经决定了实际创建的产品是哪个了。
但缺点在于,每增加一个产品都需要增加一个具体产品类和实现工厂类,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。
1)具体UML图
- 抽象工厂 AbstractFactory: 工厂方法模式的核心,是具体工厂角色必须实现的接口或者必须继承的父类,在 Java 中它由抽象类或者接口来实现。
- 具体工厂 Factory:被应用程序调用以创建具体产品的对象,含有和具体业务逻辑有关的代码
- 抽象产品 AbstractProduct:是具体产品继承的父类或实现的接口,在 Java 中一般有抽象类或者接口来实现。
- 具体产品 Product:具体工厂角色所创建的对象就是此角色的实例。
2)代码实现
//产品类 abstract class BMW { public BMW(){} } public class BMW320 extends BMW { public BMW320() { System.out.println("制造-->BMW320"); } } public class BMW523 extends BMW{ public BMW523(){ System.out.println("制造-->BMW523"); } } //工厂类 interface FactoryBMW { BMW createBMW(); } public class FactoryBMW320 implements FactoryBMW{ @Override public BMW320 createBMW() { return new BMW320(); } } public class FactoryBMW523 implements FactoryBMW { @Override public BMW523 createBMW() { return new BMW523(); } } //客户类 public class Customer { public static void main(String[] args) { FactoryBMW320 factoryBMW320 = new FactoryBMW320(); BMW320 bmw320 = factoryBMW320.createBMW(); FactoryBMW523 factoryBMW523 = new FactoryBMW523(); BMW523 bmw523 = factoryBMW523.createBMW(); } }
2.2.4 抽象工厂模式
在工厂方法模式中,我们使用一个工厂创建一个产品,一个具体工厂对应一个具体产品,但有时候我们需要一个工厂能够提供多个产品对象,而不是单一的对象,这个时候我们就需要使用抽象工厂模式。
在介绍抽象工厂模式前,我们先厘清两个概念:
- 产品等级结构:产品等级结构指的是产品的继承结构,例如一个空调抽象类,它有海尔空调、格力空调、美的空调等一系列的子类,那么这个空调抽象类和他的子类就构成了一个产品等级结构。
- 产品族:产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品。比如,海尔工厂生产海尔空调、海尔冰箱,那么海尔空调则位于空调产品族中。
1)什么是抽象工厂模式:
抽象工厂模式主要用于创建相关对象的家族。当一个产品族中需要被设计在一起工作时,通过抽象工厂模式,能够保证客户端始终只使用同一个产品族中的对象;并且通过隔离具体类的生成,使得客户端不需要明确指定具体生成类;所有的具体工厂都实现了抽象工厂中定义的公共接口,因此只需要改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
但该模式的缺点在于添加新的行为时比较麻烦,如果需要添加一个新产品族对象时,需要更改接口及其下所有子类,这必然会带来很大的麻烦。
抽象工厂模式是最复杂的工厂模式。在抽象工厂模式中,我们定义一个抽象工厂类,它包含一组抽象的工厂方法。每个工厂方法用于创建一组相关的产品对象。每个具体的工厂类都实现了抽象工厂类,并负责创建对应的产品对象。
- 抽象工厂 AbstractFactory:定义了一个接口,这个接口包含了一组方法用来生产产品,所有的具体工厂都必须实现此接口。
- 具体工厂 ConcreteFactory:用于生产不同产品族,要创建一个产品,用户只需使用其中一个工厂进行获取,完全不需要实例化任何产品对象。
- 抽象产品 AbstractProduct:这是一个产品家族,每一个具体工厂都能够生产一整组产品。
- 具体产品 Product
2)代码案例:
通过抽象工厂模式,我们可以实现以下的效果:比如宝马320系列使用空调型号A和发动机型号A,而宝马230系列使用空调型号B和发动机型号B,在为320系列生产相关配件时,就无需制定配件的型号,它会自动根据车型生产对应的配件型号A。
也就是说,当每个抽象产品都有多于一个的具体子类的时候(空调有型号A和B两种,发动机也有型号A和B两种),工厂角色怎么知道实例化哪一个子类呢?抽象工厂模式提供两个具体工厂角色(宝马320系列工厂和宝马230系列工厂),分别对应于这两个具体产品角色,每一个具体工厂角色只负责某一个产品角色的实例化,每一个具体工厂类只负责创建抽象产品的某一个具体子类的实例。
//发动机以及型号 public interface Engine {} public class EngineA implements Engine{ public EngineA(){ System.out.println("制造-->EngineA"); } } public class EngineB implements Engine{ public EngineB(){ System.out.println("制造-->EngineB"); } } //空调以及型号 public interface Aircondition {} public class AirconditionA implements Aircondition{ public AirconditionA(){ System.out.println("制造-->AirconditionA"); } } public class AirconditionB implements Aircondition{ public AirconditionB(){ System.out.println("制造-->AirconditionB"); } } //创建工厂的接口 public interface AbstractFactory { //制造发动机 public Engine createEngine(); //制造空调 public Aircondition createAircondition(); } //为宝马320系列生产配件 public class FactoryBMW320 implements AbstractFactory{ @Override public Engine createEngine() { return new EngineA(); } @Override public Aircondition createAircondition() { return new AirconditionA(); } } //宝马523系列 public class FactoryBMW523 implements AbstractFactory { @Override public Engine createEngine() { return new EngineB(); } @Override public Aircondition createAircondition() { return new AirconditionB(); } } public class Customer { public static void main(String[] args){ //生产宝马320系列配件 FactoryBMW320 factoryBMW320 = new FactoryBMW320(); factoryBMW320.createEngine(); factoryBMW320.createAircondition(); //生产宝马523系列配件 FactoryBMW523 factoryBMW523 = new FactoryBMW523(); factoryBMW523.createEngine(); factoryBMW523.createAircondition(); } }
3)小结
工厂方法模式与抽象工厂模式的区别在于:
(1)工厂方法只有一个抽象产品类和一个抽象工厂类,但可以派生出多个具体产品类和具体工厂类,每个具体工厂类只能创建一个具体产品类的实例。
(2)抽象工厂模式拥有多个抽象产品类(产品族)和一个抽象工厂类,每个抽象产品类可以派生出多个具体产品类;抽象工厂类也可以派生出多个具体工厂类,同时每个具体工厂类可以创建多个具体产品类的实例。
2.3 原型模式
2.3.1 问题引入(克隆羊)
要求编写程序,创建和Tom🐏十个完全相同的🐏。
public class Client { public static void main(String[] args) { Sheep sheep = new Sheep("tom", 1,"白色"); Sheep sheep3 = new Sheep(sheep. getName(), sheep .getAge(), sheep.getColr(); Sheep sheep2 = new Sheep(sheep. getName(), sheep getAge(), sheep getColor()); Sheep sheep4 = new Sheep( sheep.getName(), sheep. getAge(), sheep. getColor()); //.............
传统方法创建虽然简单易懂,但是性能较差,倘若数据量剧增,传统创建方法是远远不够的。故此引入原型模式。
2.3.2 什么是原型模式
原型模式主要用于对象的创建,使用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。UML图如下:
原型模式的核心是就是原型类 Prototype,Prototype 类需要具备以下两个条件:
- (1)实现 Cloneable 接口:在 Java 中 Cloneable 接口的作用就是在运行时通知虚拟机可以安全地在实现了 Cloneable 接口的类上使用 clone() 方法,只有在实现了 Cloneable 的类才可以被拷贝,否则在运行时会抛出 CloneNotSupportedException 异常。
- (2)重写 Object 类中的 clone() 方法:Java 中所有类的父类都是 Object,Object 中有一个clone() 方法用于返回对象的拷贝,但是其作用域 protected,一般的类无法调用,因此,Prototype 类需要将 clone() 方法的作用域修改为 public。
原型模式是一种比较简单的模式,也非常容易理解,实现一个接口,重写一个方法即完成了原型模式。在实际应用中,原型模式很少单独出现。经常与其他模式混用,他的原型类Prototype也常用抽象类来替代。
2.3.3 优点和适用场景
(1)原型模式比 new 方式创建对象的性能要好的多,因为 Object 类的 clone() 方法是一个本地方法,直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显;
(2)简化对象的创建;
所以原型模式适合在重复地创建相似对象的场景使用,比如在一个循环体内创建对象,假如对象创建过程比较复杂或者循环次数很多的话,使用原型模式不但可以简化创建过程,而且也可以提供系统的整体性能。
2.3.4 注意事项
(1)使用原型模式复制对象不会调用类的构造函数,对象是通过调用 Object 类的 clone() 方法来完成的,它直接在内存中复制数据。不但构造函数不会执行,甚至连访问权限都对原型模式无效。单例模式中,需要将构造函数的访问权限设置为 private,但是 clone() 方法直接无视构造方法的权限,所以单例模式与原型模式是冲突的,在使用时需要注意。
(2)深拷贝与浅拷贝。Object 类的 clone() 方法只会拷贝对象中的基本的数据类型(8种基本数据类型 byte,char,short,int,long,float,double,boolean 和对应的封装类),对于数组、容器对象、引用对象等都不会拷贝,这就是浅拷贝。如果要实现深拷贝,必须将原型模式中的数组、容器对象、引用对象等另行拷贝。
- 浅拷贝:只克隆对象中的基本数据类型,而不会克隆数组、容器、引用对象等。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。如果变量为String字符串,则拷贝其引用地址,但是在修改的时候,它会从字符串池中重新生成一个新的字符串,原有的字符串对象保持不变。
- 深拷贝:把要克隆的对象所引用的对象都克隆了一遍。
2.3.5 实现代码
public abstract class Prototype implements Cloneable { protected ArrayList<String> list = new ArrayList<String>(); @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } public abstract void show(); } public class ShallowClone extends Prototype { @Override public Prototype clone(){ Prototype prototype = null; try { prototype = (Prototype)super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return prototype; } @Override public void show(){ System.out.println("浅克隆"); } } //由于ArrayList不是基本类型,所以成员变量list,不会被拷贝,需要我们自己实现深拷贝, //幸运的是Java提供的大部分的容器类都实现了Cloneable接口。所以实现深拷贝并不是特别困难。 public class DeepClone extends Prototype { @SuppressWarnings("unchecked") @Override public Prototype clone() { Prototype prototype = null; try { prototype = (Prototype)super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } prototype.list = (ArrayList<String>) this.list.clone(); return prototype; } @Override public void show(){ System.out.println("深克隆"); } } public class Client { public static void main(String[] args) { ShallowClone cp = new ShallowClone(); ShallowClone clonecp = (ShallowClone) cp.clone(); clonecp.show(); System.out.println(clonecp.list == cp.list); DeepClone cp2 = new DeepClone(); DeepClone clonecp2 = (DeepClone) cp2.clone(); clonecp2.show(); System.out.println(clonecp2.list == cp2.list); } }
2.4 建造者模式
2.4.1 什么是建造者模式
建造者模式将复杂产品的创建步骤分解在在不同的方法中,使得创建过程更加清晰,从而更精确控制复杂对象的产生过程;通过隔离复杂对象的构建与使用,也就是将产品的创建与产品本身分离开来,使得同样的构建过程可以创建不同的对象;并且每个具体建造者都相互独立,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。
2.4.2 UML图
- Product产品类: 该类通常实现了模板方法模式,有模板方法和基本方法。
- Builder抽象建造者: 该类用于规范产品的组建,一般由子类实现。
- ConcreteBuilder具体建造者类: 实现抽象类中定义的所有方法,并且返回一个组建好的对象。
- Director导演类: 负责安排已有模板的顺序,然后告诉Builder开始建造。
2.4.3 代码实现
KFC里面一般都有好几种可供客户选择的套餐,它可以根据客户所点的套餐,然后在后面做这些套餐,返回给客户的事一个完整的、美好的套餐。下面我们将会模拟这个过程,我们约定套餐主要包含汉堡、薯条、可乐、鸡腿等等组成部分,使用不同的组成部分就可以构建出不同的套餐。
public class Meal { private String food; private String drink; public String getFood() { return food; } public void setFood(String food) { this.food = food; } public String getDrink() { return drink; } public void setDrink(String drink) { this.drink = drink; } }
public abstract class MealBuilder { Meal meal = new Meal(); public abstract void buildFood(); public abstract void buildDrink(); public Meal getMeal(){ return meal; } }
public class MealA extends MealBuilder{ public void buildDrink() { meal.setDrink("一杯可乐"); } public void buildFood() { meal.setFood("一盒薯条"); } } public class MealB extends MealBuilder{ public void buildDrink() { meal.setDrink("一杯柠檬果汁"); } public void buildFood() { meal.setFood("三个鸡翅"); } }
public class KFCWaiter { private MealBuilder mealBuilder; public void setMealBuilder(MealBuilder mealBuilder) { this.mealBuilder = mealBuilder; } public Meal construct(){ //准备食物 mealBuilder.buildFood(); //准备饮料 mealBuilder.buildDrink(); //准备完毕,返回一个完整的套餐给客户 return mealBuilder.getMeal(); } }
public class Client { public static void main(String[] args) { //服务员 KFCWaiter waiter = new KFCWaiter(); //套餐A MealA a = new MealA(); //服务员准备套餐A waiter.setMealBuilder(a); //获得套餐 Meal mealA = waiter.construct(); System.out.print("套餐A的组成部分:"); System.out.println(mealA.getFood()+"---"+mealA.getDrink()); } } //套餐A的组成部分:一盒薯条---一杯可乐
2.4.4 与工厂模式的区别
两者都是创建型模式,并且最终都是得到一个产品,但两者的区别在于:
1、抽象工厂模式实现对产品族的创建,产品族指的是不同分类维度的产品组合,用抽象工厂模式不需要关心具体构建过程,只关心产品由什么工厂生产即可。而建造者模式则更关心的是对象的构建过程,要求按照指定的蓝图建造产品,主要目的是通过组装零配件而产生一个新产品。
2、在抽象工厂模式中使用“工厂”来描述构建者,而在建造者模式中使用“车间”来描述构建者。
(1)抽象工厂模式就好比是一个一个的工厂,宝马车工厂生产宝马SUV和宝马VAN,奔驰车工厂生产奔驰车SUV和奔驰VAN,它是从一个更高层次去看对象的构建,具体到工厂内部还有很多的车间,如制造引擎的车间、装配引擎的车间等,但这些都是隐藏在工厂内部的细节,对外不公布。也就是对领导者来说,他只要关心一个工厂到底是生产什么产品的,不用关心具体怎么生产。
(2)建造者模式就不同了,它是由车间组成,不同的车间完成不同的创建和装配任务,一个完整的汽车生产过程需要引擎制造车间、引擎装配车间的配合才能完成,它们配合的基础就是设计蓝图,而这个蓝图是掌握在车间主任(Director类)手中,它给建造车间什么蓝图就能生产什么产品,建造者模式更关心建造过程。虽然从外界看来一个车间还是生产车辆,但是这个车间的转型是非常快的,只要重新设计一个蓝图,即可产生不同的产品,这有赖于建造者模式的功劳。
3、相对来说,抽象工厂模式比建造者模式的粒度要大,它关注产品整体,而建造者模式关注构建过程,所以建造者模式可以很容易地构建出一个崭新的产品,只要指挥类 Director 能够提供具体的工艺流程。也正因为如此,两者的应用场景截然不同,如果希望屏蔽对象的创建过程,只提供一个封装良好的对象,则可以选择抽象工厂方法模式。而建造者模式可以用在构件的装配方面,如通过装配不同的组件或者相同组件的不同顺序,可以产生出一个新的对象,它可以产生一个非常灵活的架构,方便地扩展和维护系统。
2.5 适配器
2.5.1 什么是适配器
适配器模式主要用于将一个类的接口转化成客户端希望的目标类格式,使得原本不兼容的类可以在一起工作,将目标类和适配者类解耦;同时也符合“开闭原则”,可以在不修改原代码的基础上增加新的适配器类;将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性,但是缺点在于更换适配器的实现过程比较复杂。
所以,适配器模式比较适合以下场景:
- (1)系统需要使用现有的类,而这些类的接口不符合系统的接口。
- (2)使用第三方组件,组件接口定义和自己定义的不同,不希望修改自己的接口,但是要使用第三方组件接口的功能。
2.5.2 适配器实现的三种方式
1)类适配器
- 目标接口(Target):客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
- 需要适配的类(Adaptee):需要适配的类或适配者类。
- 适配器(Adapter):通过包装一个需要适配的对象,把原接口转换成目标接口。
某公司做了一个人力资源管理项目,共分为三大模块:人员信息管理,薪酬管理,职位管理。其中,人员信息管理的对象是所有员工的所有信息(指在职的员工,离职退休的员工不考虑):
//人员信息管理模块接口(包含员工的基本信息) public interface IUserInfo { //获得用户姓名 public String getUserName(); //获得家庭地址 public String getHomeAddress(); //手机号码 public String getMobileNumber(); //办公电话 public String getOfficeTelNumber(); //职位 public String getJobPosition(); //获得家庭电话 public String getHomeTelNumber(); }
上面代码是信息管理模块的接口设计,具体实现类没有给出。现在遇到了一个问题,公司需要从劳动服务公司引进一部分员工解决公司劳动力不足问题,就需要将他们的基本信息比如:人员信息,工资情况,福利情况(与本公司的接口设计不同)等同步到本公司的人力资源管理系统中来(人力资源部门要求我们的系统同步劳动服务公司这部分员工的信息),但是经过调研发现,劳动服务公司的人员对象和本公司系统的对象不相同,劳动服务公司人员信息管理:
//接口设计 public interface IOuterUser { //基本信息, 比如名称、 性别、 手机号码等 public Map<String, String> getUserBaseInfo(); //工作区域信息 public Map<String, String> getUserOfficeInfo(); //用户的家庭信息 public Map<String, String> getUserHomeInfo(); } //接口的实现类 public class OuterUser implements IOuterUser { /* * 用户的基本信息 */ public Map<String, String> getUserBaseInfo() { HashMap<String, String> baseInfoMap = new HashMap<>(); baseInfoMap.put("userName", "这个员工叫混世魔王..."); baseInfoMap.put("mobileNumber", "这个员工电话是..."); return baseInfoMap; } /* 员工的家庭信息 */ public Map getUserHomeInfo() { HashMap<String, String> homeInfo = new HashMap<>(); homeInfo.put("homeTelNumbner", "员工的家庭电话是..."); homeInfo.put("homeAddress", "员工的家庭地址是..."); return homeInfo; } /* 员工的工作信息, 比如, 职位等 */ public Map<String, String> getUserOfficeInfo() { HashMap officeInfo = new HashMap(); officeInfo.put("jobPosition","这个人的职位是BOSS..."); officeInfo.put("officeTelNumber", "员工的办公电话是..."); return officeInfo; } }
分析如上设计我们发现:劳动服务公司将人员信息分为了三部分:基本信息,办公信息和个人家庭信息,并且都放到HashMap中。现在的问题是,本公司的人员信息管理系统如何和劳服公司的系统进行交互呢?这时可以进行这样的转化,先拿到对方的数据对象,然后转化为我们自己的数据对象,中间加一层数据转换处理:
//适配器类OuterUserInfo的实现如下 public class OuterUserInfo extends OuterUser implements IUserInfo{ private Map<String, String> baseInfo = super.getUserBaseInfo();//员工的基本信息 private Map<String, String> homeInfo = super.getUserHomeInfo(); //员工的家庭信息 private Map<String, String> officeInfo = super.getUserOfficeInfo(); //员工的工作信息 /* * 家庭地址 */ public String getHomeAddress() { String homeAddress = this.homeInfo.get("homeAddress"); System.out.println(homeAddress); return homeAddress; } /* *家庭电话号码 */ public String getHomeTelNumber() { String homeTelNumber = this.homeInfo.get("homeTelNumber"); System.out.println(homeTelNumber); return homeTelNumber; } /* *职位信息 */ public String getJobPosition() { String jobPosition = this.officeInfo.get("jobPosition"); System.out.println(jobPosition); return jobPosition; } /* *手机号码 */ public String getMobileNumber() { String mobileNumber = this.baseInfo.get("mobileNumber"); System.out.println(mobileNumber); return mobileNumber; } /* *办公电话 */ public String getOfficeTelNumber() { String officeTelNumber = this.officeInfo.get("officeTelNumbe"); System.out.println(officeTelNumber); return officeTelNumber; } /* *员工的名称 */ public String getUserName() { String userName = this.baseInfo.get("userName"); System.out.println(userName); return userName; } } //主类如下 public class Client { public static void main(String[] args) { //1.没有与外系统共享时 IUserInfo girl = new UserInfo(); girl.getMobileNumber(); //2.与外系统共享 IUserInfo girl2 = new OuterUserInfo(); girl2.getMobileNumber(); } }
2)对象适配器
我们还是使用上面的案例,我们想一想,假如劳动服务公司给的员工信息接口是分开的,比如基本信息一个接口,家庭信息一个接口等有多个接口的情况,我们还能像上面那样做吗?当然不行,因为Java是不支持多继承的,我们可以使用委托(也就是一种关联关系)来达到目的,这就是适配器模式的另一种方式:对象适配器模式。
public interface IOuterUserBaseInfo { //基本信息, 比如名称、 性别、 手机号码等 public Map<String, String> getUserBaseInfo(); } public interface IOuterUserHomeInfo { //用户的家庭信息 public Map<String, String> getUserHomeInfo(); } public interface IOuterUserOfficeInfo { //工作区域信息 public Map<String, String> getUserOfficeInfo(); } public class OuterUserBaseInfo implements IOuterUserBaseInfo { /* * 用户的基本信息 */ public Map<String, String> getUserBaseInfo() { HashMap<String, String> baseInfoMap = new HashMap<>(); baseInfoMap.put("userName", "这个员工叫混世魔王..."); baseInfoMap.put("mobileNumber", "这个员工电话是..."); return baseInfoMap; } public class OuterUserHomeInfo implements IOuterUserHomeInfo { /* * 员工的家庭信息 */ public Map<String, String> getUserHomeInfo() { HashMap<>String, String> homeInfo = new HashMap<>(); homeInfo.put("homeTelNumbner", "员工的家庭电话是..."); homeInfo.put("homeAddress", "员工的家庭地址是..."); return homeInfo; } } public class OuterUserOfficeInfo implements IOuterUserOfficeInfo { /* * 员工的工作信息, 比如, 职位等 */ public Map<String, String> getUserOfficeInfo() { HashMap<String, String> officeInfo = new HashMap<>(); officeInfo.put("jobPosition","这个人的职位是BOSS..."); officeInfo.put("officeTelNumber", "员工的办公电话是..."); return officeInfo; } } }
public class OuterUserInfo implements IUserInfo { //源目标对象 private IOuterUserBaseInfo baseInfo = null; //员工的基本信息 private IOuterUserHomeInfo homeInfo = null; //员工的家庭信息 private IOuterUserOfficeInfo officeInfo = null; //工作信息 //数据处理 private Map<String, String> baseMap = null; private Map<String, String> homeMap = null; private Map<String, String> officeMap = null; //构造函数传递对象 public OuterUserInfo(IOuterUserBaseInfo baseInfo,IOuterUserHomeInfo homeInfo, IOuterUserOfficeInfo officeInfo){ this.baseInfo = baseInfo; this.homeInfo = homeInfo; this.officeInfo = officeInfo; //数据处理 this.baseMap = this.baseInfo.getUserBaseInfo(); this.homeMap = this.homeInfo.getUserHomeInfo(); this.officeMap = this.officeInfo.getUserOfficeInfo(); } //家庭地址 public String getHomeAddress() { String homeAddress = this.homeMap.get("homeAddress"); System.out.println(homeAddress); return homeAddress; } //家庭电话号码 public String getHomeTelNumber() { String homeTelNumber = this.homeMap.get("homeTelNumber"); System.out.println(homeTelNumber); return homeTelNumber; } //职位信息 public String getJobPosition() { String jobPosition = this.officeMap.get("jobPosition"); System.out.println(jobPosition); return jobPosition; } //手机号码 public String getMobileNumber() { String mobileNumber = this.baseMap.get("mobileNumber"); System.out.println(mobileNumber); return mobileNumber; } //办公电话 public String getOfficeTelNumber() { String officeTelNumber= this.officeMap.get("officeTelNumber" System.out.println(officeTelNumber); return officeTelNumber; } //员工的名称 public String getUserName() { String userName = this.baseMap.get("userName"); System.out.println(userName); return userName; } } //主类如下: public class Client { public static void main(String[] args) { //外系统的人员信息 IOuterUserBaseInfo baseInfo = new OuterUserBaseInfo(); IOuterUserHomeInfo homeInfo = new OuterUserHomeInfo(); IOuterUserOfficeInfo officeInfo = new OuterUserOfficeInfo(); //传递三个对象 IUserInfo girl = new OuterUserInfo(baseInfo,homeInfo,officeInfo) girl.getMobileNumber(); }
测试结果与上面的一致。从类图中我们也知道需要修改的只不过就是 Adapter 类的内部结构,即 Adapter 自身必须先拥有一个被适配类的对象,再把具体的特殊功能委托给这个对象来实现。使用对象适配器模式,可以使得 Adapter 类(适配类)根据传入的 Adaptee 对象达到适配多个不同被适配类的功能,当然,此时我们可以为多个被适配类提取出一个接口或抽象类。这样看起来的话,似乎对象适配器模式更加灵活一点。
3)接口适配器
有时我们写的一个接口中有多个抽象方法,当我们写该接口的实现类时,必须实现该接口的所有方法,这明显有时比较浪费,因为并不是所有的方法都是我们需要的,有时只需要某一些,此处为了解决这个问题,我们引入了接口的适配器模式,借助于一个抽象类,该抽象类实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和该抽象类取得联系,所以我们写一个类,继承该抽象类,重写我们需要的方法就行。看一下类图:
public interface Sourceable { public void method1(); public void method2(); } //实现该接口的抽象类 public abstract class Wrapper2 implements Sourceable{ public void method1(){} public void method2(){} } public class SourceSub1 extends Wrapper2 { public void method1(){ System.out.println("the sourceable interface's first Sub1!"); } } public class SourceSub2 extends Wrapper2 { public void method1(){ System.out.println("the sourceable interface's second Sub2!"); } } //测试 public class WrapperTest { public static void main(String[] args) { Sourceable source1 = new SourceSub1(); Sourceable source2 = new SourceSub2(); source1.method1(); source1.method2(); source2.method1(); source2.method2(); } }
2.6 桥接模式
2.6.1 定义
桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。
2.6.2 问题引入
这里的抽象与实现是什么意思呢?先来看一个例子:
假如你有一个几何形状Shape类,从它能扩展出两个子类: 圆形Circle和 方形Square 。 你希望对这样的类层次结构进行扩展以使其包含颜色,所以你打算创建名为红色Red和蓝色Blue的形状子类。 但是, 由于你已有两个子类, 所以总共需要创建四个类才能覆盖所有组合, 例如 蓝色圆形BlueCircle和 红色方形RedSquare 。
在层次结构中新增形状和颜色将导致代码复杂程度指数增长。 例如添加三角形状, 你需要新增两个子类, 也就是每种颜色一个; 此后新增一种新颜色需要新增三个子类, 即每种形状一个。 照这样下去,所有组合类的数量将以几何级数增长,情况会越来越糟糕。
解决方案——桥接模式:通过将继承改为组合的方式来解决这个问题。 具体来说, 就是抽取其中一个维度并使之成为独立的类层次, 这样就可以在初始类中引用这个新层次的对象, 从而使得一个类不必拥有所有的状态和行为。
根据该方法, 我们可以将颜色相关的代码抽取到拥有 红色和 蓝色两个子类的颜色类中, 然后在 形状类中添加一个指向某一颜色对象的引用成员变量。 现在, 形状类可以将所有与颜色相关的工作委派给连入的颜色对象。 这样的引用就成为了 形状和 颜色之间的桥梁。 此后, 新增颜色将不再需要修改形状的类层次, 反之亦然。
2.6.3 示例代码
/** * Created on 2020/3/18 * Package com.design_pattern.bridge * * @author dsy */ public interface ColorAPI { public void paint(); }
/** * Created on 2020/3/18 * Package com.design_pattern.bridge * * @author dsy */ public class BlueColorAPI implements ColorAPI { @Override public void paint() { System.out.println("画上蓝色"); } }
/** * Created on 2020/3/18 * Package com.design_pattern.bridge * * @author dsy */ public class RedColorAPI implements ColorAPI { @Override public void paint() { System.out.println("画上红色"); } }
/** * Created on 2020/3/18 * Package com.design_pattern.bridge * * @author dsy */ public abstract class Shape { protected ColorAPI colorAPI; //添加一个颜色的成员变量以调用ColorAPI 的方法来实现给不同的形状上色 public void setDrawAPI(ColorAPI colorAPI) { //注入颜色成员变量 this.colorAPI= colorAPI; } public abstract void draw(); }
/** * Created on 2020/3/18 * Package com.design_pattern.bridge * * @author dsy */ public class Circle extends Shape { @Override public void draw() { System.out.print("我是圆形"); colorAPI.paint(); } }
/** * Created on 2020/3/18 * Package com.design_pattern.bridge * * @author dsy */ public class Rectangle extends Shape { @Override public void draw() { System.out.print("我是长方形"); colorAPI.paint(); } }
/** * Created on 2020/3/18 * Package com.design_pattern.bridge * * @author dsy */ public class Client { public static void main(String[] args) { //创建一个圆形 Shape shape = new Circle(); //给圆形蓝色的颜料 shape.setDrawAPI(new BlueColorAPI()); //上色 shape.draw(); //创建一个长方形 Shape shape1 = new Rectangle(); //给长方形红色的颜料 shape1.setDrawAPI(new RedColorAPI()); //上色 shape1.draw(); } }
假如现在客户让我们增了一个三角形,我们只需要新增一个三角形类就可以了,而无需把每一种颜色都增加一个,我们在客户端调用时只需按照需求来挑选即可
优缺点:
优点:
- 实现抽象和实现的分离
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统
- 桥接模式有时类似于多继承方案,但是多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差,而且多继承结构中类的个数非常庞大,桥接模式是比多继承方案更好的解决方法
缺点:
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
2.7 装饰者模式
2.7.1 定义
当需要对类的功能进行拓展时,一般可以使用继承,但如果需要拓展的功能种类很繁多,那势必会生成很多子类,增加系统的复杂性,并且使用继承实现功能拓展时,我们必须能够预见这些拓展功能,也就是这些功能在编译时就需要确定了。那么有什么更好的方式实现功能的拓展吗?答案就是装饰器模式。
装饰器模式可以动态给对象添加一些额外的职责从而实现功能的拓展,在运行时选择不同的装饰器,从而实现不同的行为;比使用继承更加灵活,通过对不同的装饰类进行排列组合,创造出很多不同行为,得到功能更为强大的对象;符合“开闭原则”,被装饰类与装饰类独立变化,用户可以根据需要增加新的装饰类和被装饰类,在使用时再对其进行组合,原有代码无须改变。
2.7.2 实例代码
现在需要一个汉堡,主体是鸡腿堡,可以选择添加生菜、酱、辣椒等等许多其他的配料,这种情况下就可以使用装饰者模式。
public abstract class Humburger { protected String name ; public String getName(){ return name; } public abstract double getPrice(); }
public class ChickenBurger extends Humburger { public ChickenBurger(){ name = "鸡腿堡"; } @Override public double getPrice() { return 10; } }
public abstract class Condiment extends Humburger { public abstract String getMenul(); }
public class Lettuce extends Condiment { Humburger humburger; public Lettuce(Humburger humburger){ this.humburger = humburger; } @Override public String getMenul() { return humburger.getName()+" 加生菜"; } @Override public double getPrice() { return humburger.getPrice()+1.5; } }
public class Chilli extends Condiment { Humburger humburger; public Chilli(Humburger humburger){ this.humburger = humburger; } @Override public String getMenul() { return humburger.getName()+" 加辣椒"; } @Override public double getPrice() { return humburger.getPrice(); //辣椒是免费的哦 } }
测试:
package decorator; public class Test { public static void main(String[] args) { Humburger humburger = new ChickenBurger(); System.out.println(humburger.getName()+" 价钱:"+humburger.getPrice()); //鸡腿堡 价钱:10.0 Lettuce lettuce = new Lettuce(humburger); System.out.println(lettuce.getMenul()+" 价钱:"+lettuce.getPrice()); //鸡腿堡 加生菜 价钱:11.5 Chilli chilli = new Chilli(humburger); System.out.println(chilli.getMenul()+" 价钱:"+chilli.getPrice()); //鸡腿堡 加辣椒 价钱:10.0 Chilli chilli2 = new Chilli(lettuce); System.out.println(chilli2.getMenul()+" 价钱:"+chilli2.getPrice()); //鸡腿堡 加生菜 加辣椒 价钱:11.5 } }
2.7.3 小结
Decorator的优点:
- 装饰类和被装饰类可以独立发展,而不会相互耦合
- 装饰模式是继承关系的一个替代方案
- 装饰模式可以动态的扩展一个实现类的功能
Decorator的缺点:
- 装饰类太多会增加复杂性
- 会产生很多小对象,增加系统的复杂性
- 虽然比继承更加灵活,当时也意味着比继承更加容易出错。在出现错误后,由于多层装饰,排错很困难,需要逐级排查,较为繁琐
Decorator的应用场景:
- 需要扩张一个类的功能或给一个类增加附加功能
- 需要动态的给一个对象增加功能,这些功能可以再动态的撤销
- 需要为一批兄弟类改装或加装功能
2.8 组合模式
2.8.1 定义
组合模式将叶子对象和容器对象进行递归组合,形成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性,能够像处理叶子对象一样来处理组合对象,无需进行区分,从而使用户程序能够与复杂元素的内部结构进行解耦。
组合模式最关键的地方是叶子对象和组合对象实现了相同的抽象构建类,它既可表示叶子对象,也可表示容器对象,客户仅仅需要针对这个抽象构建类进行编程,这就是组合模式能够将叶子节点和对象节点进行一致处理的原因。
通过组合模式,可以清晰地定义复杂对象的层次结构,叶子对象可以被组合成更复杂的容器对象,而容器对象又可以被组合,这样不断递归从而形成复杂的树形结构;同时在组合模式中加入新的对象构建也更容易,客户端不必因为加入了新的对象构件而更改原有代码。
2.8.2 UML结构图
- Component :抽象构建类,组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。
- Leaf:叶子对象。叶子结点没有子结点。
- Composite:容器对象,定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关操作,如增加(add)和删除(remove)等(相当于叶子节点的管理者)。
2.8.3 实例代码
在文件系统中,可能存在很多种格式的文件,如果图片,文本文件、视频文件等等,这些不同的格式文件的浏览方式都不同,同时对文件夹的浏览就是对文件夹中文件的浏览,但是对于客户而言都是浏览文件,两者之间不存在什么差别,现在只用组合模式来模拟浏览文件。UML结构图:
public abstract class File { String name; public File(String name){ this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } public abstract void display(); }
public class Folder extends File{ private List<File> files; public Folder(String name){ super(name); files = new ArrayList<File>(); } /** * 浏览文件夹中的文件 */ public void display() { for(File file : files){ file.display(); } } /** * @desc 向文件夹中添加文件 * @param file * @return void */ public void add(File file){ files.add(file); } /** * @desc 从文件夹中删除文件 * @param file * @return void */ public void remove(File file){ files.remove(file); } }
public class TextFile extends File{ public TextFile(String name) { super(name); } public void display() { System.out.println("这是文本文件,文件名:" + super.getName()); } } public class ImagerFile extends File{ public ImagerFile(String name) { super(name); } public void display() { System.out.println("这是图像文件,文件名:" + super.getName()); } } public class VideoFile extends File{ public VideoFile(String name) { super(name); } public void display() { System.out.println("这是影像文件,文件名:" + super.getName()); } }
public class Client { public static void main(String[] args) { /** * 我们先建立一个这样的文件系统 * 总文件 * * a.txt b.jpg c文件夹 * c_1.text c_1.rmvb c_1.jpg */ //总文件夹 Folder zwjj = new Folder("总文件夹"); //向总文件夹中放入三个文件:1.txt、2.jpg、1文件夹 TextFile aText= new TextFile("a.txt"); ImagerFile bImager = new ImagerFile("b.jpg"); Folder cFolder = new Folder("C文件夹"); zwjj.add(aText); zwjj.add(bImager); zwjj.add(cFolder); //向C文件夹中添加文件:c_1.txt、c_1.rmvb、c_1.jpg TextFile cText = new TextFile("c_1.txt"); ImagerFile cImage = new ImagerFile("c_1.jpg"); VideoFile cVideo = new VideoFile("c_1.rmvb"); cFolder.add(cText); cFolder.add(cImage); cFolder.add(cVideo); //遍历C文件夹 cFolder.display(); //将c_1.txt删除 cFolder.remove(cText); System.out.println("-----------------------"); cFolder.display(); } }
运行结果:
2.8.4 适用场景
- 希望客户端可以忽略组合对象与单个对象的差异时
- 处理一个树型结构时
- 文件、文件夹的管理
2.9 外观模式
2.9.1 定义
外观模式通过对客户端提供一个统一的接口,用于访问子系统中的一群接口。使用外观模式有以下几点好处:
(1)更加易用:使得子系统更加易用,客户端不再需要了解子系统内部的实现,也不需要跟众多子系统内部的模块进行交互,只需要跟外观类交互就可以了;
(2)松散耦合:将客户端与子系统解耦,让子系统内部的模块能更容易扩展和维护。
(3)更好的划分访问层次:通过合理使用 Facade,可以更好地划分访问的层次,有些方法是对系统外的,有些方法是系统内部使用的。把需要暴露给外部的功能集中到门面中,这样既方便客户端使用,也很好地隐藏了内部的细节。
2.9.2 UML图
- Facade 门面角色:客户端可以调用这个角色的方法,该角色知晓相关子系统的功能。在正常情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去。
- SubSystem 子系统角色:可以同时有一个或者多个子系统。每个子系统都不是一个单独的类,而是一个类的集合(如上面的子系统就是由 SubSystemA、SubSystemB、SubSystemC、SubSystemD 几个类组合而成)。每个子系统都可以被客户端直接调用,或者被门面角色调用。子系统并不知道门面的存在,对于子系统而言,门面角色仅仅是另外一个客户端而已。
2.9.3 实例代码
我们以一个计算机的启动过程为例:
public class CPU { public void startup(){ System.out.println("cpu startup!"); } public void shutdown(){ System.out.println("cpu shutdown!"); } } public class Memory { public void startup(){ System.out.println("memory startup!"); } public void shutdown(){ System.out.println("memory shutdown!"); } } public class Disk { public void startup(){ System.out.println("disk startup!"); } public void shutdown(){ System.out.println("disk shutdown!"); } }
//Facade类 public class Computer { private CPU cpu; private Memory memory; private Disk disk; public Computer(){ cpu = new CPU(); memory = new Memory(); disk = new Disk(); } public void startup(){ System.out.println("start the computer!"); cpu.startup(); memory.startup(); disk.startup(); System.out.println("start computer finished!"); } public void shutdown(){ System.out.println("begin to close the computer!"); cpu.shutdown(); memory.shutdown(); disk.shutdown(); System.out.println("computer closed!"); } }
public class User { public static void main(String[] args) { Computer computer = new Computer(); computer.startup(); computer.shutdown(); } }
start the computer!
cpu startup!
memory startup!
disk startup!
start computer finished!
begin to close the computer!
cpu shutdown!
memory shutdown!
disk shutdown!
computer closed!
Facade类其实相当于CPU、Disk、Memory模块的外观界面,有了这个Facade类,那么客户端就不需要亲自调用子系统中的CPU、Disk、Memory模块了,也不需要知道系统内部的实现细节,甚至都不需要知道CPU、Disk、Memory模块的存在,客户端只需要跟Facade类交互就好了,从而更好地实现了客户端和子系统中CPU、Disk、Memory模块的解耦,让客户端更容易地使用系统。
2.9.4 其他问题
1、一个系统可以有几个外观类?
在外观模式中,通常只需要一个外观类,并且此外观类只有一个实例,也就是单例类。但这并不意味着在整个系统里只有一个外观类,而仅仅是说对每一个子系统只有一个外观类。或者说,如果一个系统有好几个子系统的话,每一个子系统都有一个外观类,整个系统可以有数个外观类。
2、能否为子系统增加新行为?
外观模式的用意是为子系统提供一个集中化和简化的沟通管道,而不能向子系统加入新的行为。比如医院中的接待员并不是医护人员,接待员并不能为病人提供医疗服务。
2.10 享元模式
2.10.1 定义
享元模式通过共享技术有效地支持细粒度、状态变化小的对象复用,当系统中存在有多个相同的对象,那么只共享一份,不必每个都去实例化一个对象,极大地减少系统中对象的数量。比如说一个文本系统,每个字母定一个对象,那么大小写字母一共就是52个,那么就要定义52个对象。如果有一个1M的文本,那么字母是何其的多,如果每个字母都定义一个对象那么内存早就爆了。那么如果要是每个字母都共享一个对象,那么就大大节约了资源。
在享元模式中可以共享的相同内容称为 内部状态(Intrinsic State),而那些需要外部环境来设置的不能共享的内容称为 外部状态(Extrinsic State),其中外部状态和内部状态是相互独立的,外部状态的变化不会引起内部状态的变化。由于区分了内部状态和外部状态,因此可以通过设置不同的外部状态使得相同的对象可以具有一些不同的特征,而相同的内部状态是可以共享的。也就是说,享元模式的本质是分离与共享 : 分离变与不变,并且共享不变。把一个对象的状态分成内部状态和外部状态,内部状态即是不变的,外部状态是变化的;然后通过共享不变的部分,达到减少对象数量并节约内存的目的。
2.10.2 UML图
- (1)Flyweight:抽象享元类,所有具体享元类的超类或者接口,通过这个接口,Flyweight 可以接受并作用于外部专题;
- (2)ConcreteFlyweight:具体享元类。指定内部状态,为内部状态增加存储空间。
- (3)UnsharedConcreteFlyweight:非共享具体享元类。指出那些不需要共享的Flyweight子类。
- (4)FlyweightFactory:享元工厂类,用来创建并管理Flyweight对象,它主要用来确保合理地共享 Flyweight。
享元模式的核心是享元工厂类,享元工厂类维护了一个对象存储池,当客户端需要对象时,首先从享元池中获取,如果享元池中存在对象实例则直接返回,如果享元池中不存在,则创建一个新的享元对象实例返回给用户,并在享元池中保存该新增对象,这点有些单例的意思。
工厂类通常会使用集合类型来保存对象,如 HashMap、Hashtable、Vector 等等,在 Java 中,数据库连接池、线程池等都是用享元模式的应用。
2.10.3 实例代码
场景:假如我们有一个绘图的应用程序,通过它我们可以出绘制各种各样的形状、颜色的图形,那么这里形状和颜色就是内部状态了,通过享元模式我们就可以实现该属性的共享了。如下:
public abstract class Shape { public abstract void draw(); }
public class Circle extends Shape{ private String color; public Circle(String color){ this.color = color; } public void draw() { System.out.println("画了一个" + color +"的圆形"); } }
//核心类 public class FlyweightFactory{ static Map<String, Shape> shapes = new HashMap<String, Shape>(); public static Shape getShape(String key){ Shape shape = shapes.get(key); //如果shape==null,表示不存在,则新建,并且保持到共享池中 if(shape == null){ shape = new Circle(key); shapes.put(key, shape); } return shape; } public static int getSum(){ return shapes.size(); } }
在这里定义了一个HashMap 用来存储各个对象,用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。
public class Client { public static void main(String[] args) { Shape shape1 = FlyweightFactory.getShape("红色"); shape1.draw(); Shape shape2 = FlyweightFactory.getShape("灰色"); shape2.draw(); Shape shape3 = FlyweightFactory.getShape("绿色"); shape3.draw(); Shape shape4 = FlyweightFactory.getShape("红色"); shape4.draw(); Shape shape5 = FlyweightFactory.getShape("灰色"); shape5.draw(); Shape shape6 = FlyweightFactory.getShape("灰色"); shape6.draw(); System.out.println("一共绘制了"+FlyweightFactory.getSum()+"中颜色的圆形"); } }
运行结果:
在java实际运用中,String类型就是使用了享元模式,String 对象是 final 类型,对象一旦创建就不可改变。而 Java 的字符串常量都是存在字符串常量池中的,JVM 会确保一个字符串常量在常量池中只有一个拷贝。
String a = "hello"; String b = "hello"; if(a == b) System.out.println("OK"); else System.out.println("Error"); // 结果返回OK,以看出if条件比较的是两a和b的地址,也可以说是内存空间。
2.10.4 小结
1、享元模式的优点:
(1)极大减少系统中对象的个数;
(2)由于使用了外部状态,外部状态相对独立,不会影响到内部状态,所以享元模式使得享元对象能够在不同的环境被共享。
2、享元模式的缺点:
(1)由于享元模式需要区分外部状态和内部状态,使得应用程序在某种程度上来说更加复杂化了。
(2)为了使对象可以共享,需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
3、适用场景:
(1)如果系统中存在大量的相同或者相似的对象,由于这类对象的大量使用,会造成系统内存的耗费,可以使用享元模式来减少系统中对象的数量。
(2)对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
2.11 代理模式
2.11.1 定义
代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。简单的说就是,我们在访问实际对象时,是通过代理对象来访问的,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。
2.11.2 静态代理
静态代理:由程序员创建或特定工具自动生成源代码,也就是在编译时就已经将接口、被代理类、代理类等确定下来。在程序运行之前,代理类的.class文件就已经生成。
根据上面代理模式的类图,来写一个简单的静态代理的例子:假如一个班的同学要向老师交班费,但是都是通过班长把自己的钱转交给老师。这里,班长代理学生上交班费,班长就是学生的代理。
示例代码:
/** * 创建Person接口 */ public interface Person { //上交班费 void giveMoney(); }
public class Student implements Person { private String name; public Student(String name) { this.name = name; } @Override public void giveMoney() { System.out.println(name + "上交班费50元"); } }
/** * 学生代理类,也实现了Person接口,保存一个学生实体,这样既可以代理学生产生行为 * @author Gonjan * */ public class StudentsProxy implements Person{ //被代理的学生 Student stu; public StudentsProxy(Person stu) { // 只代理学生对象 if(stu.getClass() == Student.class) { this.stu = (Student)stu; } } //代理上交班费,调用被代理学生的上交班费行为 public void giveMoney() { stu.giveMoney(); } }
public class StaticProxyTest { public static void main(String[] args) { //被代理的学生张三,他的班费上交有代理对象monitor(班长)完成 Person zhangsan = new Student("张三"); //生成代理对象,并将张三传给代理对象 Person monitor = new StudentsProxy(zhangsan); //班长代理上交班费 monitor.giveMoney(); } }
这里并没有直接通过张三(被代理对象)来执行上交班费的行为,而是通过班长(代理对象)来代理执行了,这就是代理模式。
同时我们也可以利用代理模式,在代理工程当中加入其他用途,例如:
public class StudentsProxy implements Person{ //被代理的学生 Student stu; public StudentsProxy(Person stu) { // 只代理学生对象 if(stu.getClass() == Student.class) { this.stu = (Student)stu; } } //代理上交班费,调用被代理学生的上交班费行为 public void giveMoney() { System.out.println("张三最近学习有进步!"); stu.giveMoney(); } }
2.11.3 动态代理
代理类在程序运行时创建的代理方式被成为动态代理。
我们上面静态代理的例子中,代理类(studentProxy)是自己定义好的,在程序运行之前就已经编译完成。然而动态代理,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。 比如说,想要在每个代理的方法前都加上一个处理方法:
这里只有一个giveMoney方法,就写一次beforeMethod方法,但是如果除了giveMonney还有很多其他的方法,那就需要写很多次beforeMethod方法,麻烦。所以建议使用动态代理实现。
示例代码:
interface Human { String getBelief(); void eat(String food); }
class SuperMan implements Human { @Override public String getBelief() { return "i believe I can fly" } @Override public String eat(String food) { System.out.println("我喜欢吃" + food); } }
/** 想实现动态代理,需要解决的问题: 问题一:如何根据加载到内存的被代理类,动态创建一个代理类及其对象。 问题二:当通过代理类的对象调用方法时,如何动态的去调用被代理类中的同名方法。 */ class ProxyFactory{ //调用此方法,返回一个代理类对象,解决问题一 public static Object getProxyInstance(Object obj){ //obj 被代理类对象 MyInvocationHandler hander = new MyInvocationHandler(); handler.bind(obj); //如下为参数的具体作用 Proxy,newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),hander) } } class MyInvocationHandler implements InvocationHandler{ private Object obj; //需要使用被代理类的对象进行赋值 public void bind(Object obj){ this.obj = obj; } //当我们通过代理类的对象,调用方法时,就会自动转为如下方法:invoke(); //将被代理类执行的方法a的功能就声明在invoke()中 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //method,即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法 //obj:被代理的对象 //最后返回方法执行后的结果 return method.invoke(obj,args); } }
- obj.getClass().getClassLoader():一个classloader对象,定义了由哪个classloader对象对生成的代理类进行加载
- obj.getClass().getInterfaces():一个interface对象数组,表示我们将要给我们的代理对象提供一组什么样的接口,如果我们提供了这样一个接口对象数组,那么也就是声明了代理类实现了这些接口,代理类就可以调用接口中声明的所有方法。
- hander:一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用。
public class client{ public static void main(String args[]){ SuperMan superMan = new SuperMan(); Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan); //当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法 String believef = proxyInstance.getBelief(); System.out.println(believef); proxyInstance.eat("四川麻辣烫"); } }
动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。是因为所有被代理执行的方法,都是通过在InvocationHandler中的invoke方法调用的,所以我们只要在invoke方法中统一处理,就可以对所有被代理的方法进行相同的操作了。
动态代理的过程,代理对象和被代理对象的关系不像静态代理那样一目了然,清晰明了。因为动态代理的过程中,我们并没有实际看到代理类,也没有很清晰地的看到代理类的具体样子,而且动态代理中被代理对象和代理对象是通过InvocationHandler来完成的代理过程的,其中具体是怎样操作的,为什么代理对象执行的方法都会通过InvocationHandler中的invoke方法来执行。
2.11.4 JDK与CGLIB的区别
JDK和CGLIB动态代理总结:
- JDK动态代理只能对实现了接口的类生成代理,而不能针对类 ,使用的是 Java反射技术实现,生成类的过程比较高效。
- CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法 ,使用asm字节码框架实现,相关执行的过程比较高效,生成类的过程可以利用缓存弥补,因为是继承,所以该类或方法最好不要声明成final
- JDK代理是不需要第三方库支持,只需要JDK环境就可以进行代理,使用条件:实现InvocationHandler + 使用Proxy.newProxyInstance产生代理对象 + 被代理的对象必须要实现接口
- CGLib必须依赖于CGLib的类库,但是它需要类来实现任何接口代理的是指定的类生成一个子类,覆盖其中的方法,是一种继承但是针对接口编程的环境下推荐使用JDK的代理;
2.12 模板方法模式
2.12.1 定义
在父类定义一个操作中的算法框架,而将一些步骤延迟到子类中。使得子类可以玩不改变一个算法的结构即可重新定义该算法的某些特定步骤。
2.12.2 UML图
- 抽象类(AbstractClass):实现了模板方法,定义了算法的骨架。
- 具体类(ConcreteClass):实现抽象类中的抽象方法,已完成完整的算法。
注意:因为模板方法是一种定义好的框架,一般是不允许恶意修改的,所以通常会在模板方法上加final关键字,防止被重写。
抽象模板中的基本方法尽量设计为protected类型,符合迪米特法则。不需要暴露的细节尽量设计为私有。实现类如非必要,不要扩大父类中的访问权限
2.12.3 实例代码
举个例子,以准备去学校所要做的工作(prepareGotoSchool)为例,假设需要分三步:穿衣服(dressUp),吃早饭(eatBreakfast),带上东西(takeThings)。因此准备工作顺序只需要在父类中定义实现,不必在每个子类中重写。子类中学生和老师要做得具体事情肯定有所区别,所以只需要考虑具体所做的某事,而不需要考虑顺序。
//抽象类定义整个流程骨架 public abstract class AbstractPerson{ //模板方法,使用final修改,防止子类改变算法的实现步骤 public final void prepareGotoSchool(){ dressUp(); eatBreakfast(); takeThings(); } //以下是不同子类根据自身特性完成的具体步骤 protected abstract void dressUp(); protected abstract void eatBreakfast(); protected abstract void takeThings(); } //具体实现类 public class Student extends AbstractPerson{ @Override protected void dressUp() { System.out.println(“穿校服"); } @Override protected void eatBreakfast() { System.out.println(“吃妈妈做好的早饭"); } @Override protected void takeThings() { System.out.println(“背书包,带上家庭作业和红领巾"); } } public class Teacher extends AbstractPerson{ @Override protected void dressUp() { System.out.println(“穿工作服"); } @Override protected void eatBreakfast() { System.out.println(“做早饭,照顾孩子吃早饭"); } @Override protected void takeThings() { System.out.println(“带上昨晚准备的考卷"); } } public class Client { public static void main(String[] args) { Student student = new Student() student.prepareGotoSchool(); Teacher teacher = new Teacher() teacher.prepareGotoSchool(); } }
2.12.4 模板方法模式的应用——钩子函数(hook)
直接通过举例来说明吧。就拿测试宝马车的例子来说,其实按不按喇叭是一个随机操作,你可以按也可以不按,但是由于我们把它写死在了模板方法中,那么不管你愿不愿意,喇叭都会响,这是一种很不爽的事情,也不利于功能的扩展,所以钩子函数就诞生了,通过钩子函数我们就可以约束模板的行为。下面看一下代码:
//父类BaomaModel public abstract class BaomaModel { //发动汽车 protected abstract void start(); //停止汽车 protected abstract void stop(); //鸣笛 protected abstract void alarm(); //启动引擎 protected abstract void engineBoom(); //钩子函数(hook):用来控制汽车是否鸣笛 protected boolean isAlarm() { return true; } /** * 定义一个模板方法,使子类按照定义的顺序执行 */ protected void run() { //发动汽车 this.start(); //启动引擎 this.engineBoom(); //是否鸣笛 if (isAlarm()) { this.alarm(); } //停止汽车 this.stop(); } } //子类BaomaX5 public class BaomaX5 extends BaomaModel { private boolean alarmFlag = true; //默认喇叭可以响 public void setAlarmFlag(boolean alarmFlag) { this.alarmFlag = alarmFlag; } protected void start() { System.out.println("我是X5,我要发动了。。。"); } protected void stop() { System.out.println("我要停止了。。。"); } protected void alarm() { System.out.println("我要鸣笛,前面的家伙绕道。。。"); } protected void engineBoom() { System.out.println("我要发动引擎了。。。"); } @Override protected boolean isAlarm() { return this.alarmFlag; } } //子类BaomaX6 public class BaomaX6 extends BaomaModel { protected void start() { System.out.println("我是X6,我要发动了。。。"); } protected void stop() { System.out.println("我要停止了。。。"); } protected void alarm() { System.out.println("我要鸣笛,前面的家伙绕道。。。"); } protected void engineBoom() { System.out.println("我要发动引擎了。。。"); } @Override protected boolean isAlarm() { return false; } }
public class Client { public static void main(String[] args) { BaomaModel x5 = new BaomaX5(); BaomaModel x6 = new BaomaX6(); x5.run(); System.out.println("=============================="); x6.run(); } } //结果: 我是X5,我要发动了。。。 我要发动引擎了。。。 我要鸣笛,前面的家伙绕道。。。 我要停止了。。。 ============================== 我是X6,我要发动了。。。 我要发动引擎了。。。 我要停止了。。。
在抽象类中定义了一个实现方法isAlarm()方法,子类可以重写该方法并决定是否鸣笛,其返回值会影响模板方法的执行结果,该方法就叫钩子方法(钩子函数)。这样的设计是非常优雅与有用的,值得我们细细揣摩。
2.12.5 小结
优点:
- 封装不变部分,扩展可变部分
- 提取公共部分代码,便于维护
- 行为由父类控制,子类实现
使用场景:
- 多个子类有公有的方法,并且逻辑基本相同
- 重要,复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现
2.13 命令模式
在开发中,我们可能需要向某些对象发送一些请求,但我们不知道请求的具体接收者是谁,也不知道被请求的操作是哪个,只知道在系统运行中指定具体的请求接收者即可,打个比方,电视遥控器,我们只需知道按哪个按钮能够打开电视、关闭电视和换台即可,并不需要知道是怎么开电视、关电视和换台的,对于这种情况,我们可以采用命令模式来进行设计。
2.13.1 定义
命令模式的本质是将请求封装成对象,将发出命令与执行命令的责任分开,命令的发送者和接收者完全解耦,发送者只需知道如何发送命令,不需要关心命令是如何实现的,甚至是否执行成功都不需要理会。命令模式的关键在于引入了抽象命令接口,发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。另外命令可以像强对象一样可以被存储和传递,所以可支持撤销的操作
使用命令模式的优势在于降低了系统的耦合度,而且新命令可以很方便添加到系统中,也容易设计一个组合命令。但缺点在于会导致某些系统有过多的具体命令类,因为针对每一个命令都需要设计一个具体命令类。所以命令模式适用于以下场景:
- (1)需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- (2)系统需要在不同的时间指定请求、将请求排队和执行请求。
- (3)系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- (4)系统需要将一组操作组合在一起,即支持宏命令。
2.13.2 UML图
- Receiver:接收者,执行命令的对象,任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
- Command:抽象命令类,声明需要执行的方法。
- ConcreteCommand:具体命令类,通常会持有接收者,并调用接收者的功能完成命令要执行的操作。
- Invoker:调用者,通常会持有命令对象,可以持有多个命令对象,是客户端真正触发命令并要求命令执行相应操作的地方,就是相当于使用命令对象的入口。
- Client:客户类,创建具体的命令对象,并且设置命令对象的接收者。注意这里不是指常规意义上的客户端,把这个 Client 称为装配者会合适,主要用于组装命令对象和接收者。
2.13.3 实例代码
这里以电视机为例。电视是请求的接受者,遥控器是请求的发送者,遥控器上有一些按钮,不同的按钮对应着不同的操作。在这里遥控器需要执行三个命令:打开电视机、关闭电视机、换台。
/** * Command命令接口,为所有的命令声明一个接口。所有的命令都应该实现它 */ public interface Command { public void execute(); }
public class Television { public void open(){ System.out.println("打开电视机......"); } public void close(){ System.out.println("关闭电视机......"); } public void changeChannel(){ System.out.println("切换电视频道......"); } }
public class Television { public void open(){ System.out.println("打开电视机......"); } public void close(){ System.out.println("关闭电视机......"); } public void changeChannel(){ System.out.println("切换电视频道......"); } }
public class Controller { private Command openTVCommand; private Command closeTVCommand; private Command changeChannelCommand; public Controller(Command openTvCommand,Command closeTvCommand,Command changeChannelCommand){ this.openTVCommand = openTvCommand; this.closeTVCommand = closeTvCommand; this.changeChannelCommand = changeChannelCommand; } /** * 打开电视剧 */ public void open(){ openTVCommand.execute(); } /** * 关闭电视机 */ public void close(){ closeTVCommand.execute(); } /** * 换频道 */ public void change(){ changeChannelCommand.execute(); } }
public class OpenTvCommand implements Command{ private Television tv; public OpenTvCommand(Television tv){ this.tv = tv; } public void execute() { tv.open(); } } public class ChangeChannelCommand implements Command{ private Television tv; public ChangeChannelCommand(Television tv){ this.tv = tv; } public void execute() { tv.changeChannel(); } } public class CloseTvCommand implements Command{ private Television tv; public CloseTvCommand(Television tv){ this.tv = tv; } public void execute() { tv.close(); } }
public class Client { public static void main(String a[]) { Television tv = new Television(); //定义按钮命令 Command openCommand = new OpenTvCommand(tv); Command closeCommand = new CloseTvCommand(tv); Command changeCommand = new ChangeChannelCommand(tv); //定义一个遥控器 Controller control = new Controller(openCommand,closeCommand,changeCommand); //用户使用遥控器,下达命令 control.open(); //打开电视机 control.change(); //换频道 control.close(); //关闭电视机 } }
运行结果:
打开电视机......
切换电视机频道......
关闭电视机......
2.13.4 小结
Command的优点:
- 类间解耦: 调用角色与接收角色之间没有任何依赖关系,调用者实现功能时只需要调用Command抽象类的execute方法,不需要了解到底是哪个接收者执行
- 可扩展性: Command的子类可以非常容易的扩展,而调用者Invoker和高层次的模块Client不产生严重的代码耦合
Command的缺点:
- 命令太多会导致类膨胀问题,增加了系统复杂度。
2.14 访问者模式
2.14.1 案例引入
去医院看病时,医生会给你一个处方单要你去拿药,拿药我们可以分为两步走:
- (1)去柜台交钱,划价人员会根据处方单上的药进行划价,交钱。
- (2)去药房拿药,药房工作者同样根据处方单给你相对应的药。
里我们就划价和拿药两个步骤进行讨论,这里有三个类,处方单(药)、划价人员、药房工作者。同时划价人员和药房工作者都各自有一个动作:划价、拿药。这里进行最初步的设计如下:
public class Charge { public void action(){ public void action(){ if("A药".equals(medicine)){ //A的价格 } if("B药".equals(medicine)){ //B的价格 } if("C药".equals(medicine)){ //C的价格 } if("D药".equals(medicine)){ //D的价格 } if("E药".equals(medicine)){ //E的价格 } ............ } } }
public class WorkerOfPharmacy { public void action(){ if("A药".equals(medicine)){ //给你A药 } if("B药".equals(medicine)){ //给你B药 } if("C药".equals(medicine)){ //给你C药 } if("D药".equals(medicine)){ //给你D药 } if("E药".equals(medicine)){ //给你E药 } ............ } }
这样的代码写法,在药品种类少的情况没什么问题,但也存在这么多的 if…else,而且我们可以想象医院里的药是那么多,而且随时都会增加的,增加了药就要改变划价人员和药房工作者的代码,这是我们最不希望改变的。那么有没有办法来解决呢?有,访问者模式提供一中比较好的解决方案。
在实际开发过程中,我们对同个对象可能存在不同的操作方式,如处方单,划价人员要根据它来划价,药房工作者要根据它来给药。而且可能会随时增加新的操作,如医院增加新的药物,但是这里有两个元素是保持不变的,或者说很少变:划价人员和药房工作中,变的只不过是他们的操作。所以我们想如果能够将他们的操作抽象化就好了,这里访问者模式就是一个值得考虑的解决方案了。
2.14.2 实例代码
以上面在医院付费、取药为实例。在这个实例中划价员和药房工作者作为访问者,药品作为访问元素、处方单作为对象结构,所以整个UML结构图如下:
public abstract class Visitor { protected String name; public void setName(String name) { this.name = name; } public abstract void visitorWork(MedicineA a); public abstract void visitorWork(MedicineB b); }
public class Charger extends Visitor{ public void visitorWork(MedicineA a) { System.out.println("划价员:" + name +"给药" + a.getName() +"划价:" + a.getPrice()); } public void visitorWork(MedicineB b) { System.out.println("划价员:" + name +"给药" + b.getName() +"划价:" + b.getPrice()); } }
public class WorkerOfPharmacy extends Visitor{ public void visitorWork(MedicineA a) { System.out.println("药房工作者:" + name + "拿药 :" + a.getName()); } public void visitorWork(MedicineB b) { System.out.println("药房工作者:" + name + "拿药 :" + b.getName()); } }
public abstract class Medicine { protected String name; protected double price; public Medicine (String name,double price){ this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public abstract void accept(Visitor visitor); }
public class MedicineA extends Medicine{ public MedicineA(String name, double price) { super(name, price); } public void accept(Visitor visitor) { visitor.visitorWork(this); } }
public class MedicineB extends Medicine{ public MedicineB(String name, double price) { super(name, price); } public void accept(Visitor visitor) { visitor.visitorWork(this); } }
public class Presciption { List<Medicine> list = new ArrayList<Medicine>(); public void accept(Visitor visitor){ Iterator<Medicine> iterator = list.iterator(); while (iterator.hasNext()) { iterator.next().accept(visitor); } } public void addMedicine(Medicine medicine){ list.add(medicine); } public void removeMedicien(Medicine medicine){ list.remove(medicine); } }
public class Client { public static void main(String[] args) { Medicine a = new MedicineA("板蓝根", 11.0); Medicine b = new MedicineB("感康", 14.3); Presciption presciption = new Presciption(); presciption.addMedicine(a); presciption.addMedicine(b); Visitor charger = new Charger(); charger.setName("张三"); Visitor workerOfPharmacy = new WorkerOfPharmacy(); workerOfPharmacy.setName("李四"); presciption.accept(charger); System.out.println("-------------------------------------"); presciption.accept(workerOfPharmacy); } }
运行结果:
2.15 迭代器模式
2.15.1 定义
实际开发中,我们针对不同的需求,可能需要以不同的方式来遍历整个整合对象,但我们不希望在集合容器的抽象接口层中充斥着各种不同的遍历操作,这时候我们就需要一种能完成下面功能的迭代器:
- (1)遍历一个集合对象
- (2)不需要了解聚合对象的内部结构
- (3)提供多种不同的遍历方式
迭代器模式提供一种访问集合中的各个元素,而不暴露其内部表示的方法。将在元素之间游走的职责交给迭代器,而不是集合对象,从而简化集合容器的实现,让集合容器专注于在它所应该专注的事情上,更加符合单一职责原则。
2.15.2 UML图
- Iterator:抽象迭代器,提供了在集合容器元素之间游走的方法。
- ConcreteIterator:具体迭代器,能够对具体的集合容器进行遍历,每种集合容器都应该对应一个具体的迭代器
- Aggregate:抽象容器类
- ConcreteAggregate:具体容器类,实现 creatorIterator() 方法,返回该聚合对象的迭代器。
2.15.3 实例代码
现有一个书架BookShelf,有一堆书Book,我想把书放到书架上,并按照书的名字按顺序显示出来。
Aggregate接口:表示集合的接口,相当于Java集合中Collection,List等集合接口
/** * 表示集合的接口 */ public interface Aggregate { /** * 该方法用于生成一个遍历结合的迭代器 * * 在遍历集合中的元素时吗,调用iterator方法生成一个实现了Iterator接口的类的实例 * @return */ public abstract Iterator iterator(); }
Iterator接口:表示用于遍历集合的接口
/** * 用于遍历集合的接口:遍历集合中的元素,其作用相当于循环语句中的循环变量 */ public interface Iterator { /** * 判断是否存在下一个元素 * @return */ public abstract boolean hasNext(); /** * 返回集合中的一个元素 * @return */ public abstract Object next(); }
BookShelf类:实现了Aggregate接口,表示一个书籍的集合,可以看成Java集合框架中的实现类
/** * 表示书架类,可以看成是一个书的集合,所以需要实现Aggregate接口 */ public class BookShelf<T> implements Aggregate { private Book[] books;// 可见性设置为private,避免不小心篡改 private int last;//标识最后一本书的索引 public BookShelf(int maxsize) { this.books = new Book[maxsize];// 生成BookShelf实例的时候指定了books的大小 } //根据索引得到书籍 public Book getBookAt(int index) { return books[index]; } //向书架上添加书籍 public void appendBook(Book book) { this.books[last] = book; last++; } //返回书籍的数量 public int getLength() { return last; } @Override public Iterator iterator() { return new BookShelfIterator<T>(this); } }
BookShelfIterator类:Iterator的实现类
/** * 遍历书籍的迭代器实现(Iterator)类 * @param <T> */ public class BookShelfIterator<T> implements Iterator { private BookShelf<T> bookShelf;//表示BookShelfIterator要遍历的书架 private int index;//表示迭代器当前指向的书的索引 //初始化 public BookShelfIterator(BookShelf<T> bookShelf) { this.bookShelf = bookShelf; this.index = 0; } @Override public boolean hasNext() { <!-- if (index < bookShelf.getLength()) {//如果当前索引小于书架书籍的数量,表名下一本书还存在,继续遍历 return true; } else { return false; } --> return index != bookShelf.getLength; } /** * next()方法有两个作用: * >返回当前索引对应的书籍实例 * >并将index指向下一个元素 >>>> 这和for循环中的i++很像 */ @Override public Object next() { Book book = bookShelf.getBookAt(index);//将对应索引的书一一返回 index++; return book; } }
Book类:书籍类(实体类)
public class Book { private String name; public Book(String name) { super(); this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Book [name=" + name + "]"; } }
下面是Main方法:
public class Main { public static void main(String[] args) { BookShelf<Book> bookShelf = new BookShelf<>(4); bookShelf.appendBook(new Book("雪国")); bookShelf.appendBook(new Book("山中古音")); bookShelf.appendBook(new Book("天堂")); bookShelf.appendBook(new Book("一个人的路")); Iterator iterator = bookShelf.iterator(); while(iterator.hasNext()) { System.out.println(iterator.next()); } } } /** * 运行结果: * Book [name=雪国] * Book [name=山中古音] * Book [name=天堂] * Book [name=一个人的路] */
2.15.4 集合框架中的迭代器模式(源码分析)
我就使用List接口进行解析,List定义了如下方法 Iterator<E> iterator(); Iterator接口: public interface Iterator<E> { boolean hasNext(); E next(); default void remove() { throw new UnsupportedOperationException("remove"); } //该方法与函数式接口有关(lambda表达式) default void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); while (hasNext()) action.accept(next()); } } 以List的一个实现类ArrayList来看看如何实现的 public Iterator<E> iterator() { return new Itr(); } //Itr是AbstractList的一个成员内部类(其实ArrayList也有一个Itr的成员内部类 只不过这是优化版本) private class Itr implements Iterator<E> { int cursor; //表示下一个要访问的元素的索引 int lastRet = -1; //表示上一个元素的索引 int expectedModCount = modCount;//表示对ArrayList修改次数的期望值,它的初始值为modCount(modCount是AbstractList类中的一个成员变量) public boolean hasNext() { return cursor != size(); } public E next() { checkForComodification(); try { int i = cursor;//下一个元素的索引 E next = get(i);//通过该索引得到实例 lastRet = i;//将该索引赋值给表示上一个元素索引的变量 cursor = i + 1;//指向下一个元素 return next;//返回当前实例 } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { AbstractList.this.remove(lastRet); if (lastRet < cursor) cursor--; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
2.15.5 小结
看了上面介绍的迭代器模式,我们或许头有点晕,既然同是迭代,为什么我们不去使用更为普通的迭代方式,如for循环呢,非要用这种复杂的设计模式干什么,其实回到前面迭代器模式的定义:迭代器模式提供一种顺序访问一个聚合对象中各个元素的方法,而又不暴露该对象的内部实现。
如同上面举的那个例子,这里只是用了Iterator的hasNext和next方法,并没有调用BookShelf的方法,也就是说,while循环并不依赖于BookShelf的实现。不管BookShelf如何变化,只要BookShelf的iterator方法能返回正确的Iterator实例,即使不对while循环做任何修改,都可以正常工作。设计模式的作用就是帮助我们编写可复用的类。而可复用的含义就是将类视为“组件”,当一个组件方法发生变化时,不需要对其他组件进行修改或者只需要进行很小的修改就可以了,大大提高了代码的重用率。
2.16 观察者模式
2.16.1 定义
观察者模式(又被称为发布-订阅(Publish/Subscribe)模式,属于行为型模式的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。
2.16.2 UML图
在观察者模式中有如下角色:
- Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
- ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
- Observer:抽象观察者,是观察者者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
- ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。
2.16.3 实例代码
观察者模式这种发布-订阅的形式我们可以拿微信公众号来举例,假设微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了程序猿这个公众号,当这个公众号更新时就会通知这些订阅的微信用户。好了我们来看看用代码如何实现:
public interface Observer { public void update(String message); }
public class WeixinUser implements Observer { // 微信用户名 private String name; public WeixinUser(String name) { this.name = name; } @Override public void update(String message) { System.out.println(name + "-" + message); } }
public interface Subject { /** * 增加订阅者 * @param observer */ public void attach(Observer observer); /** * 删除订阅者 * @param observer */ public void detach(Observer observer); /** * 通知订阅者更新消息 */ public void notify(String message); }
//微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法: public class SubscriptionSubject implements Subject { //储存订阅公众号的微信用户 private List<Observer> weixinUserlist = new ArrayList<Observer>(); @Override public void attach(Observer observer) { weixinUserlist.add(observer); } @Override public void detach(Observer observer) { weixinUserlist.remove(observer); } @Override public void notify(String message) { for (Observer observer : weixinUserlist) { observer.update(message); } } }
public class Client { public static void main(String[] args) { SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject(); //创建微信用户 WeixinUser user1=new WeixinUser("杨影枫"); WeixinUser user2=new WeixinUser("月眉儿"); WeixinUser user3=new WeixinUser("紫轩"); //订阅公众号 mSubscriptionSubject.attach(user1); mSubscriptionSubject.attach(user2); mSubscriptionSubject.attach(user3); //公众号更新发出消息给订阅的微信用户 mSubscriptionSubject.notify("刘望舒的专栏更新了"); } }
结果:
杨影枫-刘望舒的专栏更新了
月眉儿-刘望舒的专栏更新了
紫轩-刘望舒的专栏更新了
2.16.4 观察者模式在JDK和Spring源码中的典型应用示例
(1)在JDK中的典型应用:Java JDK中的java.util.Observable类就是一个观察者模式的主题类,源码如下:
@Deprecated(since="9") public class Observable { private boolean changed = false; //用来保存观察者对象的容器 private Vector<Observer> obs; //初始化 public Observable() { obs = new Vector<>(); } //添加一个观察者 public synchronized void addObserver(Observer o) { if (o == null) throw new NullPointerException(); if (!obs.contains(o)) { obs.addElement(o); } } //删除一个观察者 public synchronized void deleteObserver(Observer o) { obs.removeElement(o); } //通知观察者 public void notifyObservers() { notifyObservers(null); } //通知观察者 public void notifyObservers(Object arg) { /* * a temporary array buffer, used as a snapshot of the state of * current Observers. */ Object[] arrLocal; synchronized (this) { /* We don't want the Observer doing callbacks into * arbitrary code while holding its own Monitor. * The code where we extract each Observable from * the Vector and store the state of the Observer * needs synchronization, but notifying observers * does not (should not). The worst result of any * potential race-condition here is that: * 1) a newly-added Observer will miss a * notification in progress * 2) a recently unregistered Observer will be * wrongly notified when it doesn't care */ if (!changed) return; arrLocal = obs.toArray(); clearChanged(); } for (int i = arrLocal.length-1; i>=0; i--) ((Observer)arrLocal[i]).update(this, arg); } //删除所有观察者 public synchronized void deleteObservers() { obs.removeAllElements(); } //指示对象改变 protected synchronized void setChanged() { changed = true; } //指示对象不再改变 protected synchronized void clearChanged() { changed = false; } //测试对象是否改变 public synchronized boolean hasChanged() { return changed; } //统计观察者数 public synchronized int countObservers() { return obs.size(); } }
(2)Spring框架中的典型应用:org.springframework.context.ApplicationListener类,源码如下:
@FunctionalInterface public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { /** * Handle an application event. * @param event the event to respond to */ //该方法类似于示例中的update方法 void onApplicationEvent(E event); }
该监听器的主题对象是ApplicationEventMulticaster类的子类,ApplicationEventMulticaster实现了管理观察者和发布时间的接口,Spring应用上下文ApplicationContext可以使用ApplicationEventMulticaster作为实际发布时间的委托。
2.16.5 小结
Observer的优点:
- 观察者和被观察者之间是抽象耦合:增加观察和被观察变得容易
- 建立了一套触发机制:被观察者一个对象的改变会引起多个对象作出回应
Observer的缺点:
- 开发效率和运行效率问题:牵一发动全身,一个被观察者,多个观察者,开发调试会很困难
Observer的应用场景:
- 关联行为场景
- 事件多级触发场景
- 跨系统的消息交换场景
在项目中使用Observer:
- 观察者被观察者之间的消息沟通:在实际项目中,观察者的Update()方法接受两个参数,一个是被观察者,一个是DTO(数据传输对象:纯洁的JavaBean,由被观察者生成,由观察者消费)
观察者响应方式:假如有一个观察者多个被观察者那如何考虑性能:
- 采用多线程技术,这就是异步架构
- 采用缓存技术,这就是同步架构
2.17 中介者模式
2.17.1 定义
中介者模式通过中介者对象来封装一系列的对象交互,将对象间复杂的关系网状结构变成结构简单的以中介者为核心的星形结构,对象间一对多的关联转变为一对一的关联,简化对象间的关系,便于理解;各个对象之间的关系被解耦,每个对象不再和它关联的对象直接发生相互作用,而是通过中介者对象来与关联的对象进行通讯,使得对象可以相对独立地使用,提高了对象的可复用和系统的可扩展性。
在中介者模式中,中介者类处于核心地位,它封装了系统中所有对象类之间的关系,除了简化对象间的关系,还可以对对象间的交互进行进一步的控制。
2.17.2 UML图
- Mediator:抽象中介者,定义了同事对象到中介者对象之间的接口
- ConcreteMediator:具体中介者,实现抽象中介者的方法,它需要知道所有的具体同事类,同时需要从具体的同事类那里接收信息,并且向其他具体的同事类发送信息
- Colleague:抽象同事类
- ConcreteColleague:具体同事类,每个具体同事类都只需要知道自己的行为即可,但是他们都需要认识中介者,每个同事类的行为都分为两种:
-
- 第一种:自己本身的行为,比如改变对象本身的状态,处理自己的行为等,这种行为叫做自发行为,与其他的同时类或中介者没有任何的依赖
- 第二种:必须依赖中介者才能完成的行为,叫做依赖方法
从UML结构类图中可以看出,在系统中,中介者类主要承担中转和协调两个方面的责任:
- 结构上起到中转作用:通过中介者类对关系的封装,使得对象类不再需要显示的引用其他对象,只需要通过中介者就可以与其他对象类进行的通信。
- 行为上起到协调作用:通过中介者类对关系的封装,对象类可以在不知道其他对象的情况下通过中介者与其他对象完成通信。在这个过程中对象类是不需要告诉中介者该如何做,中介者可以根据自身的逻辑来进行协调,对同事的请求进一步处理,将同事成员之间的关系行为进行分离和封装。
2.17.3 示例代码
我们先来看这样一种场景:有一家宝马4s店,下面有三个部门:销售部门,采购部门,库存管理部门,这三个部门的职责分别是:
- 采购部门: 负责宝马汽车的采购。与销售部门沟通销售情况:销售的好多进几辆车,销售不好就少进几辆车;与库存管理部门沟通库存情况:扩大库存或者减少库存
- 库存管理部门: 负责库存管理。与采购部门沟通采购情况:库存太多少采购,库存太少多采购;与销售部门沟通销售情况:库存积压,倾销处理。
- 销售部门: 负责销售汽车。与采购部门和库存管理部门进行沟通:根据销售情况决定是否增加库存。
未使用中介者模式:
//采购管理 public class Purchase { //采购宝马汽车 public void buyBaomaCar(int number) { //访问库存 Stock stock = new Stock(); //访问销售 Sale sale = new Sale(); //电脑的销售情况 int saleStatus = sale.getSaleStatus(); if (saleStatus > 80) { //销售情况良好 System.out.println("采购宝马汽车:" + number + "台"); //库存增加 stock.increase(number); } else { //销售情况不好 //折半采购 int buyNumber = number / 2; System.out.println("采购宝马汽车:" + buyNumber + "台"); } } //不再采购宝马汽车 public void refuseBuyBaomaCar() { System.out.println("不再采购宝马汽车。。。"); } } //库存管理 public class Stock { //原始库存是100辆宝马车 private static int BAOMACAR_NUMBER = 100; //库存增加 public void increase(int number) { BAOMACAR_NUMBER = BAOMACAR_NUMBER + number; System.out.println("库存数量为:" + BAOMACAR_NUMBER); } //库存降低 public void decrease(int number) { BAOMACAR_NUMBER = BAOMACAR_NUMBER - number; System.out.println("库存数量为:" + BAOMACAR_NUMBER); } //获得库存数量 public int getStockNumber() { return BAOMACAR_NUMBER; } //存货太多,通知采购人员不要采购,销售人员尽快销售 public void clearStock() { Purchase purchase = new Purchase(); Sale sale = new Sale(); System.out.println("清理存货数量为:" + BAOMACAR_NUMBER); //要求销售人员折价销售 sale.offSale(); //要求采购人员不要采购 purchase.refuseBuyBaomaCar(); } } //销售管理 public class Sale { //销售宝马汽车 public void sellBaomaCar(int number) { //访问库存 Stock stock = new Stock(); //访问采购人员 Purchase purchase = new Purchase(); //库存不够销售,通知采购人员采购宝马汽车 if (stock.getStockNumber() < number) { purchase.buyBaomaCar(number); } System.out.println("销售宝马汽车:" + number + "台"); stock.decrease(number); } //反馈销售情况 public int getSaleStatus() { Random random = new Random(System.currentTimeMillis()); int saleStatus = random.nextInt(100); System.out.println("宝马汽车的销售情况为:" + saleStatus); return saleStatus; } //折价处理 public void offSale() { //清理所有库存 Stock stock = new Stock(); System.out.println("半价销售宝马汽车" + stock.getStockNumber() + "台"); } } //在一个场景中运行得: public class Client { public static void main(String[] args) { System.out.println("======采购人员采购宝马汽车======"); Purchase purchase = new Purchase(); purchase.buyBaomaCar(100); System.out.println("======销售人员销售宝马汽车======="); Sale sale = new Sale(); sale.sellBaomaCar(12); System.out.println("======库房管理人员管理库存======="); Stock stock = new Stock(); stock.clearStock(); } } //结果如下: ======采购人员采购宝马汽车====== 宝马汽车的销售情况为:22 采购宝马汽车:50台 ======销售人员销售宝马汽车======= 销售宝马汽车:12台 库存数量为:88 ======库房管理人员管理库存======= 清理存货数量为:88 半价销售宝马汽车88台 不再采购宝马汽车。。。
虽然我们顺利的实现了功能,可是这样的设计有没有问题?肯定是有很大的问题的,前面已经说过,耦合度太高,每个类之间依赖太深,可扩展性太低。假如再加一个部门,比如说是监管部门,监管部门需要获取每个部门的情况,每个部门又要获得监管部门的反馈信息,那样所有的类都需要修改,类图之间出现蜘蛛网状结构,这种设计是很不合理的,故此使用中介者模式:
//抽象的中介者 public abstract class AbstractMediator { protected Purchase purchase; protected Sale sale; protected Stock stock; //构造函数 public AbstractMediator() { purchase = new Purchase(this); sale = new Sale(this); stock = new Stock(this); } //事件方法:处理多个对象之间的关系 public abstract void execute(String str, Object...objects); } //具体中介者 public class Mediator extends AbstractMediator { //事件方法 public void execute(String str, Object...objects) { if (str.equals("purchase.buy")) { //采购宝马汽车 this.buyBaomaCar((Integer)objects[0]); } else if (str.equals("sale.sell")) { //销售宝马汽车 this.sellBaomaCar((Integer)objects[0]); } else if (str.equals("sale.offsell")) { //折价销售 this.offSell(); } else if (str.equals("stock.clear")) { //清仓处理 this.clearStock(); } } //采购宝马汽车 private void buyBaomaCar(int number) { int saleStatus = super.sale.getSaleStatus(); if (saleStatus > 80) { //销售情况良好 System.out.println("采购宝马汽车:" + number + "台"); //库存增加 super.stock.increase(number); } else { //销售情况不好 //折半采购 int buyNumber = number / 2; System.out.println("采购宝马汽车:" + buyNumber + "台"); } } //销售宝马汽车 private void sellBaomaCar(int number) { //库存不够销售,通知采购人员采购宝马汽车 if (super.stock.getStockNumber() < number) { super.purchase.buyBaomaCar(number); } super.stock.decrease(number); } //折价销售 private void offSell() { System.out.println("半价销售宝马汽车" + stock.getStockNumber() + "台"); } //清仓处理 private void clearStock() { //要求清仓销售 super.sale.offSale(); //要求采购人员不要采购 super.purchase.refuseBuyBaomaCar(); } } //抽象的同事类 public abstract class AbstractColleague { protected AbstractMediator mediator; public AbstractColleague(AbstractMediator mediator) { this.mediator = mediator; } } //采购管理 public class Purchase extends AbstractColleague{ //构造函数 public Purchase(AbstractMediator mediator) { super(mediator); } //采购宝马汽车 public void buyBaomaCar(int number) { super.mediator.execute("purchase.buy", number); } //不再采购宝马汽车 public void refuseBuyBaomaCar() { System.out.println("不再采购宝马汽车。。。"); } } //库存管理 public class Stock extends AbstractColleague { //原始库存是100辆宝马车 private static int BAOMACAR_NUMBER = 100; public Stock(AbstractMediator mediator) { super(mediator); } //库存增加 public void increase(int number) { BAOMACAR_NUMBER = BAOMACAR_NUMBER + number; System.out.println("库存数量为:" + BAOMACAR_NUMBER); } //库存降低 public void decrease(int number) { BAOMACAR_NUMBER = BAOMACAR_NUMBER - number; System.out.println("库存数量为:" + BAOMACAR_NUMBER); } //获得库存数量 public int getStockNumber() { return BAOMACAR_NUMBER; } //存货太多,通知采购人员不要采购,销售人员尽快销售 public void clearStock() { System.out.println("清理存货数量为:" + BAOMACAR_NUMBER); super.mediator.execute("stock.clear"); } } //销售管理 public class Sale extends AbstractColleague{ public Sale(AbstractMediator mediator) { super(mediator); } //销售宝马汽车 public void sellBaomaCar(int number) { super.mediator.execute("sale.sell", number); System.out.println("销售宝马汽车:" + number + "台"); } //反馈销售情况 public int getSaleStatus() { Random random = new Random(System.currentTimeMillis()); int saleStatus = random.nextInt(100); System.out.println("宝马汽车的销售情况为:" + saleStatus); return saleStatus; } //折价处理 public void offSale() { super.mediator.execute("sale.offsell"); } } //在一个场景中运行得: public class Client { public static void main(String[] args) { AbstractMediator mediator = new Mediator(); System.out.println("======采购人员采购电脑====="); Purchase purchase = new Purchase(mediator); purchase.buyBaomaCar(100); System.out.println("======销售人员销售电脑======"); Sale sale = new Sale(mediator); sale.sellBaomaCar(1); System.out.println("======库房管理人员管理库存======"); Stock stock = new Stock(mediator); stock.clearStock(); } } //结果如下: ======采购人员采购电脑===== 宝马汽车的销售情况为:27 采购宝马汽车:50台 ======销售人员销售电脑====== 库存数量为:99 销售宝马汽车:1台 ======库房管理人员管理库存====== 清理存货数量为:99 半价销售宝马汽车99台 不再采购宝马汽车。。。
加入一个中介者,将与自己无关的活动交给中介者进行处理,设计结构更加清晰,而且类与类之间的耦合性大大减少,可扩展性大大增强。
2.17.4 小结
Mediator的优缺点:
- 优点: 减少类之间的依赖,把一对多的依赖变为一对一的依赖,降低类之间的耦合
- 缺点: 中介者会膨胀得很大,逻辑变得复杂,很难调试维护
Mediator的应用:
- 中介者模式适用于对象之间紧密耦合的情况,紧密耦合的标准是:在类图之间出现可蜘蛛网状结构。
2.18 备忘录模式
2.18.1 定义
备忘录模式提供了一种恢复状态的机制,在不破坏封装的前提下,捕获对象的内部状态,并保存在该对象之外,保证该对象能够恢复到某个历史状态;备忘录模式将保存的细节封装在备忘录中,除了创建它的创建者之外其他对象都不能访问它,并且实现了即使要改变保存的细节也不影响客户端。但是备忘录模式都是多状态和多备份的,会早用较多的内存,消耗资源。
2.18.2 UML图
- Originator:原发器,负责创建一个备忘录,用于记录当前对象的内部状态,也可以使用它来利用备忘录恢复内部状态,同时原发器还可以根据需要决定 Memento 存储 Originator 的哪些内部状态。
- Memento:备忘录,用于存储 Originator 的内部状态,并且可以防止 Originator 以外的对象访问Memento。在备忘录 Memento 中有两个接口,其中 Caretaker 只能看到备忘录中的窄接口,它只能将备忘录传递给其他对象。Originator可以看到宽接口,允许它访问返回到先前状态的所有数据。
- Caretaker: 负责人,对备忘录 Memento 进行管理,保存和提供备忘录,但不能对备忘录的内容进行操作和访问,只能够将备忘录传递给其他对象。
备忘录模式的核心就是备忘录 Memento,在备忘录中存储的就是原发器 Originator 的部分或者所有的状态信息,而这些状态信息是不能够被其他对象所访问的,也就是说我们是不能使用备忘录之外的对象来存储这些状态信息,如果暴漏了内部状态信息就违反了封装的原则,故备忘录除了原发器外其他对象都不可以访问。所以为了实现备忘录模式的封装,我们需要对备忘录的访问做些控制:
(1)对原发器:可以访问备忘录里的所有信息。
(2)对负责人:不可以访问备忘录里面的数据,但是他可以保存备忘录并且可以将备忘录传递给其他对象。
(3)其他对象:不可访问也不可以保存,它只负责接收从负责人那里传递过来的备忘录同时恢复原发器的状态。
2.18.3 案例引入
在电影中,月光宝盒是一种可以穿越时空的宝物,持有月关宝盒的人可以去到任意的时间,也可以迅速的回到原来的时间。当然,在现实中,起码在现在的科技水平下,人类的科技是无法做到时空穿梭的程度的,但是作为程序员,我们却可以通过编程来体验这种任意穿梭时间的乐趣:
- TimeMachine:这就是前面说的月关宝盒类了,这个类会显示当前月关宝盒所在的年代,同时可以创建一个备份保存这个年代,以便以后可以返回
- Memento:备忘录类,这是一个Javabean,就是单纯的为TimeMachine保存一个年代信息
- Caretaker:备忘录的管理类,通过这个类去管理备忘录类,高层模块不必要直接和备忘录接触,降低耦合
// 时间机器——月关宝盒 public class TimeMachine { // 年代 private String time = ""; // 改变年代 public void changeTime() { this.time = "公元1970-1-1 00:00:00,我是月光宝盒,今天是我的生日!"; } public void setTime(String time) { this.time = time; } public String getTime() { return time; } // 保留一个备份 public Memento createMemento() { return new Memento(this.time); } // 恢复一个备份 public void restoreMemento(Memento memento) { this.setTime(memento.getTime()); } } // 备忘录类 public class Memento { // 年代 private String time = ""; // 通过构造函数来传递年代 public Memento(String time) { this.time = time; } public String getTime() { return time; } public void setTime(String time) { this.time = time; } } // 备忘录管理员 public class Caretaker { // 备忘录对象 private Memento memento; public Memento getMemento() { return memento; } public void setMemento(Memento memento) { this.memento = memento; } } // 在一个场景中运行 public class Client { public static void main(String[] args) { // 准备时间机器——“月光宝盒” TimeMachine tm = new TimeMachine(); // 准备一个备忘录管理者 Caretaker caretaker = new Caretaker(); // 初始化时间机器 tm.setTime("公元前221年,秦始皇一统天下!"); System.out.println("==========月光宝盒当前时间==========="); System.out.println(tm.getTime()); // 使用备忘录记录一下 caretaker.setMemento(tm.createMemento()); // 改变一下年代 tm.changeTime(); System.out.println("==========月光宝盒修改后时间==========="); System.out.println(tm.getTime()); // 回到原始的那个年代 tm.restoreMemento(caretaker.getMemento()); System.out.println("==========月光宝盒恢复后时间==========="); System.out.println(tm.getTime()); } } 结果如下: 公元前221年,秦始皇一统天下! ==========月光宝盒修改后时间=========== 公元1970-1-1 00:00:00,我是月光宝盒,今天是我的生日! ==========月光宝盒恢复后时间=========== 公元前221年,秦始皇一统天下!
2.18.4 扩展形式
1)clone方式
// 发起人自主备份和恢复 public class Originator implements Cloneable { // 当前发起人的备份 private Originator backup; // 内部状态 private String state = ""; public String getState() { return state; } public void setState(String state) { this.state = state; } // 创建一个备忘录 public void createMemento() { this.backup = (Originator) this.clone(); } // 恢复一个备忘录 public void restoreMemento() { // 断言备忘录不为空 Assert.assertNotNull(backup); this.setState(this.backup.getState()); } // 克隆当前对象 @Override protected Object clone(){ try { return (Originator) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } } // 在一个场景中运行 public class Client { public static void main(String[] args) { // 定义发起人 Originator originator = new Originator(); // 建立初始状态 originator.setState("初始状态...."); System.out.println("初始状态是:" + originator.getState()); // 建立备份 originator.createMemento(); // 修改状态 originator.setState("修改后的状态..."); System.out.println("修改后的状态是:" + originator.getState()); // 恢复原有状态 originator.restoreMemento(); System.out.println("恢复后的状态是:" + originator.getState()); } } 结果如下: 初始状态是:初始状态.... 修改后的状态是:修改后的状态... 恢复后的状态是:初始状态....
我们发现结果没有变化,完美的实现了我们的需求,同时程序更加精简。但是由于深拷贝和浅拷贝存在的问题,在复杂场景下会让程序的逻辑变得很复杂,所以基于克隆的备忘录模式适用于较为简单的场景,不要与其他的对象产生严重的耦合关系。
2)多状态的备忘录模式
在前面介绍的案例中,发起人角色Originator只有一个状态,我们只需要备份一个状态就可以,那要是在复杂场景中出现了多个状态值需要备份怎么办呢?这时我们可以使用前面的介绍的通过克隆的备忘录模式
// 发起人角色:有多个状态 public class Originator { // 内部状态 private String state1 = ""; private String state2 = ""; private String state3 = ""; public String getState1() { return state1; } public void setState1(String state1) { this.state1 = state1; } public String getState2() { return state2; } public void setState2(String state2) { this.state2 = state2; } public String getState3() { return state3; } public void setState3(String state3) { this.state3 = state3; } // 创建一个备忘录 public Memento createMemento() { return new Memento(BeanUtils.backupProp(this)); } // 恢复一个备忘录 public void restoreMemento(Memento memento) { BeanUtils.restoreProp(this, memento.getStateMap()); } @Override public String toString() { return "Originator{" + "state1='" + state1 + '\'' + ", state2='" + state2 + '\'' + ", state3='" + state3 + '\'' + '}'; } } // 备忘录类 public class Memento { // 状态值存放在一个HashMap中 private HashMap<String, Object> stateMap; // 接收一个对象,建立一个备份 public Memento(HashMap<String, Object> map) { this.stateMap = map; } public HashMap<String, Object> getStateMap() { return stateMap; } public void setStateMap(HashMap<String, Object> stateMap) { this.stateMap = stateMap; } } // 备忘录管理员类 public class Caretaker { // 备忘录对象 private Memento memento; public Memento getMemento() { return memento; } public void setMemento(Memento memento) { this.memento = memento; } } // 工具类 public class BeanUtils { // 把bean的所有属性和数值放到HashMap中:使用反射获取 public static HashMap<String, Object> backupProp(Object bean) { HashMap<String, Object> result = new HashMap<String, Object>(16); try { // 获得bean的描述 BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass()); // 获得属性描述 PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors(); // 遍历所有的属性 for (PropertyDescriptor des:descriptors) { // 属性名称 String fieldName = des.getName(); // 读取属性的方法 Method getter = des.getReadMethod(); // 读取属性值 Object fieldValue = getter.invoke(bean, new Object[]{}); if (!fieldName.equalsIgnoreCase("class")) { result.put(fieldName, fieldValue); } } } catch (Exception e) { e.printStackTrace(); } return result; } // 把HashMap的值返回到bean中 public static void restoreProp(Object bean, HashMap<String, Object> propMap) { try { // 获得bean的描述 BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass()); // 获得属性描述 PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors(); // 遍历所有的属性 for (PropertyDescriptor des:descriptors) { // 属性名称 String fieldName = des.getName(); // 如果有这个属性,设置值 if (propMap.containsKey(fieldName)) { // 写属性的方法 Method setter = des.getWriteMethod(); setter.invoke(bean, new Object[]{propMap.get(fieldName)}); } } }catch (Exception e) { e.printStackTrace(); } } } //在一个场景中测试 public class Client { public static void main(String[] args) { // 定义一个发起人 Originator ori = new Originator(); // 定义出备忘录管理员 Caretaker caretaker = new Caretaker(); // 初始化 ori.setState1("篮球"); ori.setState2("足球"); ori.setState3("乒乓球"); System.out.println("=======初始化状态=====\n" + ori); // 创建一个备忘录 caretaker.setMemento(ori.createMemento()); // 修改状态值 ori.setState1("蔬菜"); ori.setState2("水果"); ori.setState3("海鲜"); System.out.println("\n=======修改后状态======\n" + ori); // 恢复备忘录中保存的状态 ori.restoreMemento(caretaker.getMemento()); System.out.println("\n=======恢复后状态======\n" + ori); } } 结果如下: =======初始化状态===== Originator{state1='篮球', state2='足球', state3='乒乓球'} =======修改后状态====== Originator{state1='蔬菜', state2='水果', state3='海鲜'} =======恢复后状态====== Originator{state1='篮球', state2='足球', state3='乒乓球'}
2.18.5 小结
Memento的使用场景
- 需要保存和恢复数据的相关状态场景
- 提供回滚操作:如数据库中事物提交失败后的回滚操作就是使用了备忘录模式
- 需要对一个对象进行监控的场景
Memento的注意事项
- 备忘录的生命周期:备忘录创建出来要在“最近”的代码中使用,要主动管理它的生命周期,建立了就要使用它,不使用了就删除它的引用,等待垃圾回收期对其进行回收
- 备忘录的性能:不要在频繁建立备份的场景中使用备忘录模式(比如for循环),原因如下
- 控制不了备忘录建立的对象数量
- 大对象的建立需要耗费很大的资源
2.19 解释器模式
2.19.1 定义
解释器模式,就是定义语言的文法,并建立一个解释器来解释该语言中的句子,通过构建解释器,解决某一频繁发生的特定类型问题实例。
这里我们将语言理解成使用规定格式和语法的代码
解释器模式描述了如何构成一个简单的语言解释器,主要应用在使用面向对象语言开发的编译器中,它描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解释这些句子。
解释器模式中除了能够使用文法规则来定义一个语言,还能通过使用抽象语法树来更加直观表示、更好地地表示一个语言的构成,每一颗抽象语法树对应一个语言实例。抽象语法树描述了如何构成一个复杂的句子,通过对抽象语法树的分析,可以识别出语言中的终结符和非终结符类。 在解释器模式中由于每一种终结符表达式、非终结符表达式都会有一个具体的实例与之相对应,所以系统的扩展性比较好。
2.19.2 实例代码
我们将创建一个接口 Expression 和实现了 Expression 接口的实体类。定义作为上下文中主要解释器的 TerminalExpression 类。其他的类 OrExpression、AndExpression 用于创建组合式表达式。
InterpreterPatternDemo,我们的演示类使用 Expression 类创建规则和演示表达式的解析。
1)创建一个表达式接口。
Expression.java
public interface Expression { boolean interpret(String context); }
2)创建实现了上述接口的实体类。
TerminalExpression.java
public class TerminalExpression implements Expression { private String data; public TerminalExpression(String data) { this.data = data; } @Override public boolean interpret(String context) { if (context.contains(data)) { return true; } return false; } }
3)创建实现了上述接口的实体类。
TerminalExpression.java
public class TerminalExpression implements Expression { private String data; public TerminalExpression(String data) { this.data = data; } @Override public boolean interpret(String context) { if (context.contains(data)) { return true; } return false; } }
4)创建实现了上述接口的实体类。
TerminalExpression.java
public class TerminalExpression implements Expression { private String data; public TerminalExpression(String data) { this.data = data; } @Override public boolean interpret(String context) { if (context.contains(data)) { return true; } return false; } }
5)OrExpression.java
public class OrExpression implements Expression { private Expression expr1 = null; private Expression expr2 = null; public OrExpression(Expression expr1, Expression expr2) { this.expr1 = expr1; this.expr2 = expr2; } @Override public boolean interpret(String context) { return expr1.interpret(context) || expr2.interpret(context); } }
6)AndExpression.java
public class AndExpression implements Expression { private Expression expr1 = null; private Expression expr2 = null; public AndExpression(Expression expr1, Expression expr2) { this.expr1 = expr1; this.expr2 = expr2; } @Override public boolean interpret(String context) { return expr1.interpret(context) && expr2.interpret(context); } }
7)InterpreterPatternDemo 使用 Expression 类来创建规则,并解析它们。
InterpreterPatternDemo.java
public class InterpreterPatternDemo { //规则:Robert 和 John 是男性 public static Expression getMaleExpression(){ Expression robert = new TerminalExpression("Robert"); Expression john = new TerminalExpression("John"); return new OrExpression(robert, john); } //规则:Julie 是一个已婚的女性 public static Expression getMarriedWomanExpression(){ Expression julie = new TerminalExpression("Julie"); Expression married = new TerminalExpression("Married"); return new AndExpression(julie, married); } public static void main(String[] args) { Expression isMale = getMaleExpression(); Expression isMarriedWoman = getMarriedWomanExpression(); System.out.println("John is male? " + isMale.interpret("John")); System.out.println("Julie is a married women? " + isMarriedWoman.interpret("Married Julie")); } }
8)运行结果:
John is male? true Julie is a married women? true
2.19.3 小结
应用实例:编译器、运算表达式计算。
优点 :1、可扩展性比较好,灵活。 2、增加了新的解释表达式的方式。 3、易于实现简单文法。
缺点: 1、可利用场景比较少。 2、对于复杂的文法比较难维护。 3、解释器模式会引起类膨胀。 4、解释器模式采用递归调用方法。
使用场景 :1、可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。 2、一些重复出现的问题可以用一种简单的语言来进行表达。 3、一个简单语法需要解释的场景。
注意事项:可利用场景比较少,JAVA 中如果碰到可以用 expression4J 代替。
2.20 状态模式
2.20.1 定义
状态模式,就是允许对象在内部状态发生改变时改变它的行为,对象看起来就好像修改了它的类,也就是说以状态为原子来改变它的行为,而不是通过行为来改变状态。
当对象的行为取决于它的属性时,我们称这些属性为状态,那该对象就称为状态对象。对于状态对象而言,它的行为依赖于它的状态,比如要预订房间,只有当该房间空闲时才能预订,想入住该房间也只有当你预订了该房间或者该房间为空闲时。对于这样的一个对象,当它的外部事件产生互动的时候,其内部状态就会发生变化,从而使得他的行为也随之发生变化。
2.20.2 UML图
- Context:环境类,可以包括一些内部状态
- State:抽象状态类,定义了所有具体状态的共同接口,任何状态都需要实现这个接口,从而实现状态间的互相转换
- ConcreteState:具体状态类,处理来自 Context 的请求,每一个 ConcreteState 都提供了它对自己请求的实现,所以,当 Context 改变状态时行为也会跟着改变
所以状态模式适用于:代码中包含大量与对象状态有关的条件语句,以及对象的行为依赖于它的状态,并且可以根据它的状态改变而改变它的相关行为。
2.20.3 实例代码
1)首先是状态接口:State
public interface State { /** * @desc 预订房间 */ public void bookRoom(); /** * @desc 退订房间 */ public void unsubscribeRoom(); /** * @desc 入住 */ public void checkInRoom(); /** * @desc 退房 */ public void checkOutRoom(); }
2)然后是房间类:
public class Room { /* * 房间的三个状态 */ State freeTimeState; //空闲状态 State checkInState; //入住状态 State bookedState; //预订状态 State state ; public Room(){ freeTimeState = new FreeTimeState(this); checkInState = new CheckInState(this); bookedState = new BookedState(this); state = freeTimeState ; //初始状态为空闲 } /** * @desc 预订房间 */ public void bookRoom(){ state.bookRoom(); } /** * @desc 退订房间 */ public void unsubscribeRoom(){ state.unsubscribeRoom(); } /** * @desc 入住 */ public void checkInRoom(){ state.checkInRoom(); } /** * @desc 退房 */ public void checkOutRoom(){ state.checkOutRoom(); } public String toString(){ return "该房间的状态是:"+getState().getClass().getName(); } /* * getter和setter方法 */ public State getFreeTimeState() { return freeTimeState; } public void setFreeTimeState(State freeTimeState) { this.freeTimeState = freeTimeState; } public State getCheckInState() { return checkInState; } public void setCheckInState(State checkInState) { this.checkInState = checkInState; } public State getBookedState() { return bookedState; } public void setBookedState(State bookedState) { this.bookedState = bookedState; } public State getState() { return state; } public void setState(State state) { this.state = state; } }
3) 然后是3个状态类,这个三个状态分别对于这:空闲、预订、入住。其中空闲可以完成预订和入住两个动作,预订可以完成入住和退订两个动作,入住可以退房。
/** * @Description: 空闲状态只能预订和入住 */ public class FreeTimeState implements State { Room hotelManagement; public FreeTimeState(Room hotelManagement){ this.hotelManagement = hotelManagement; } public void bookRoom() { System.out.println("您已经成功预订了..."); hotelManagement.setState(hotelManagement.getBookedState()); //状态变成已经预订 } public void checkInRoom() { System.out.println("您已经成功入住了..."); hotelManagement.setState(hotelManagement.getCheckInState()); //状态变成已经入住 } public void checkOutRoom() { //不需要做操作 } public void unsubscribeRoom() { //不需要做操作 } }
/** * @Description: 预定状态房间只能退订和入住 */ public class BookedState implements State { Room hotelManagement; public BookedState(Room hotelManagement) { this.hotelManagement = hotelManagement; } public void bookRoom() { System.out.println("该房间已近给预定了..."); } public void checkInRoom() { System.out.println("入住成功..."); hotelManagement.setState(hotelManagement.getCheckInState()); //状态变成入住 } public void checkOutRoom() { //不需要做操作 } public void unsubscribeRoom() { System.out.println("退订成功,欢迎下次光临..."); hotelManagement.setState(hotelManagement.getFreeTimeState()); //变成空闲状态 } }
/** * @Description: 入住可以退房 */ public class CheckInState implements State { Room hotelManagement; public CheckInState(Room hotelManagement) { this.hotelManagement = hotelManagement; } public void bookRoom() { System.out.println("该房间已经入住了..."); } public void checkInRoom() { System.out.println("该房间已经入住了..."); } public void checkOutRoom() { System.out.println("退房成功...."); hotelManagement.setState(hotelManagement.getFreeTimeState()); //状态变成空闲 } public void unsubscribeRoom() { //不需要做操作 } }
4)测试类:
public class Test { public static void main(String[] args) { //有3间房 Room[] rooms = new Room[2]; //初始化 for(int i = 0 ; i < rooms.length ; i++){ rooms[i] = new Room(); } //第一间房 rooms[0].bookRoom(); //预订 rooms[0].checkInRoom(); //入住 rooms[0].bookRoom(); //预订 System.out.println(rooms[0]); System.out.println("---------------------------"); //第二间房 rooms[1].checkInRoom(); rooms[1].bookRoom(); rooms[1].checkOutRoom(); rooms[1].bookRoom(); System.out.println(rooms[1]); } }
运行结果:
2.20.4 小结
从上面的UML结构图我们可以看出状态模式的优点在于:
(1)封装了转换规则,允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块
(2)将所有与状态有关的行为放到一个类中,可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
但是状态模式的缺点在于:
(1)需要在枚举状态之前需要确定状态种类
(2)会导致增加系统类和对象的个数。
(3)对 “开闭原则” 的支持并不友好,新增状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。
2.21 策略模式
2.21.1 定义
将类中经常改变或者可能改变的部分提取为作为一个抽象策略接口类,然后在类中包含这个对象的实例,这样类实例在运行时就可以随意调用实现了这个接口的类的行为。
比如定义一系列的算法,把每一个算法封装起来,并且使它们可相互替换,使得算法可独立于使用它的客户而变化,这就是策略模式。
2.21.2 UML结构图
(1)环境类(Context):通过 ConcreteStrategy 具体策略类来配置,持有 Strategy 对象并维护对Strategy 对象的引用。可定义一个接口来让 Strategy 访问它的数据。
(2)抽象策略类(Strategy):定义所有支持的算法的公共接口。 Context使用这个接口来调用某ConcreteStrategy 定义的算法。
(3)具体策略类(ConcreteStrategy): Strategy 接口的具体算法。
2.21.3 实例代码
场景如下,刘备要到江东娶老婆了,走之前诸葛亮给赵云三个锦囊妙计,说是按天机拆开能解决棘手问题。场景中出现三个要素:三个妙计(具体策略类)、一个锦囊(环境类)、赵云(调用者)。
public interface Strategy { public void operate(); }
//妙计一:初到吴国 public class BackDoor implements IStrategy { @Override public void operate() { System.out.println("找乔国老帮忙,让吴国太给孙权施加压力,使孙权不能杀刘备"); } } //求吴国太开绿灯放行 public class GivenGreenLight implements IStrategy { @Override public void operate() { System.out.println("求吴国太开个绿灯,放行"); } } //孙夫人断后,挡住追兵 public class BlackEnemy implements IStrategy { @Override public void operate() { System.out.println("孙夫人断后,挡住追兵"); } }
public class Context { private Strategy strategy; //构造函数,要你使用哪个妙计 public Context(Strategy strategy){ this.strategy = strategy; } public void setStrategy(Strategy strategy){ this.strategy = strategy; } public void operate(){ this.strategy.operate(); } }
测试:
public class Zhaoyun { public static void main(String[] args) { Context context; System.out.println("----------刚到吴国使用第一个锦囊---------------"); context = new Context(new BackDoor()); context.operate(); System.out.println("\n"); System.out.println("----------刘备乐不思蜀使用第二个锦囊---------------"); context.setStrategy(new GivenGreenLight()); context.operate(); System.out.println("\n"); System.out.println("----------孙权的追兵来了,使用第三个锦囊---------------"); context.setStrategy(new BlackEnemy()); context.operate(); System.out.println("\n"); } }
2.21.4 小结
策略模式的优点在于可以动态改变对象的行为;但缺点是会产生很多策略类,同时客户端必须知道所有的策略类,并自行决定使用哪一个策略类;
策略模式适用用于以下几种场景:
- (1)应用程序需要实现特定的功能服务,而该程序有多种实现方式使用,所以需要动态地在几种算法中选择一种
- (2)一个类定义了多种行为算法,并且这些行为在类的操作中以多个条件语句的形式出现,就可以将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。
2.22 责任链模式
案例引入:
学校规定参加校招的同学必须要请假,且要有相关人员的签字,三天以下需辅导员签字、三到七天需要系主任签字,一个星期以上需要院长签字,更多的则需要校长签字!
2.22.1 定义
职责链可以将请求的处理者组织成一条链,并将请求沿着链传递,如果某个处理者能够处理请求则处理,否则将该请求交由上级处理。客户端只需将请求发送到职责链上,无须关注请求的处理细节,通过职责链将请求的发送者和处理者解耦了,这也是职责链的设计动机。
职责链模式可以简化对象间的相互连接,因为客户端和处理者都没有对方明确的信息,同时处理者也不知道职责链中的结构,处理者只需保存一个指向后续者的引用,而不需要保存所有候选者的引用。
另外职责链模式增加了系统的灵活性,我们可以任意增加或更改处理者,甚至更改处理者的顺序,不过有可能会导致一个请求无论如何也得不到处理,因为它可能被放置在链末端。
2.22.2 UML结构图
- (1)Handler:抽象处理者,定义了一个处理请求的方法。所有的处理者都必须实现该抽象类。
- (2)ConcreteHandler:具体处理者,处理它所负责的请求,同时也可以访问它的后继者,如果它能够处理该请求则处理,否则将请求传递到它的后继者。
- (3)Client: 客户类
2.22.3 示例代码
public class LeaveNode { /** 请假天数 **/ private int number; /** 请假人 **/ private String person; public LeaveNode(String person,int number){ this.person = person; this.number = number; } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } public String getPerson() { return person; } public void setPerson(String person) { this.person = person; } }
public abstract class Leader { /** 姓名 **/ public String name; /** 后继者 **/ protected Leader successor; public Leader(String name){ this.name = name; } public void setSuccessor(Leader successor) { this.successor = successor; } public abstract void handleRequest(LeaveNode LeaveNode); }
public class Instructor extends Leader{ public Instructor(String name){ super(name); } public void handleRequest(LeaveNode LeaveNode) { if(LeaveNode.getNumber() <= 3){ //小于3天辅导员审批 System.out.println("辅导员" + name + "审批" +LeaveNode.getPerson() + "同学的请假条,请假天数为" + LeaveNode.getNumber() + "天。"); } else{ //否则传递给系主任 if(this.successor != null){ this.successor.handleRequest(LeaveNode); } } } }
public class DepartmentHead extends Leader{ public DepartmentHead(String name) { super(name); } public void handleRequest(LeaveNode LeaveNode) { if(LeaveNode.getNumber() <= 7){ //小于7天系主任审批 System.out.println("系主任" + name + "审批" +LeaveNode.getPerson() + "同学的请假条,请假天数为" + LeaveNode.getNumber() + "天。"); } else{ //否则传递给院长 if(this.successor != null){ this.successor.handleRequest(LeaveNode); } } } }
public class Dean extends Leader{ public Dean(String name) { super(name); } public void handleRequest(LeaveNode LeaveNode) { if(LeaveNode.getNumber() <= 10){ //小于10天院长审批 System.out.println("院长" + name + "审批" +LeaveNode.getPerson() + "同学的请假条,请假天数为" + LeaveNode.getNumber() + "天。"); } else{ //否则传递给校长 if(this.successor != null){ this.successor.handleRequest(LeaveNode); } } } }
public class President extends Leader{ public President(String name) { super(name); } public void handleRequest(LeaveNode LeaveNode) { if(LeaveNode.getNumber() <= 15){ //小于15天校长长审批 System.out.println("校长" + name + "审批" +LeaveNode.getPerson() + "同学的请假条,请假天数为" + LeaveNode.getNumber() + "天。"); } else{ //否则不允批准 System.out.println("请假天天超过15天,不批准..."); } } }
public class Client { public static void main(String[] args) { Leader instructor = new Instructor("陈毅"); //辅导员 Leader departmentHead = new DepartmentHead("王明"); //系主任 Leader dean = new Dean("张强"); //院长 Leader president = new President("王晗"); //校长 instructor.setSuccessor(departmentHead); //辅导员的后续者是系主任 departmentHead.setSuccessor(dean); //系主任的后续者是院长 dean.setSuccessor(president); //院长的后续者是校长 //请假3天的请假条 LeaveNode leaveNode1 = new LeaveNode("张三", 3); instructor.handleRequest(leaveNode1); //请假9天的请假条 LeaveNode leaveNode2 = new LeaveNode("李四", 9); instructor.handleRequest(leaveNode2); //请假15天的请假条 LeaveNode leaveNode3 = new LeaveNode("王五", 15); instructor.handleRequest(leaveNode3); //请假20天的请假条 LeaveNode leaveNode4 = new LeaveNode("赵六", 20); instructor.handleRequest(leaveNode4); } }
结果:
2.22.4 小结
纯的与不纯的责任链模式:
(1)纯的责任链模式要求处理者对象只能在两个行为中选择一个:一是承担责任,二是把责任推给下家,不允许出现某一个具体处理者对象在承担了一部分责任后又把责任向下传的情况。
(2)在纯的责任链模式里面,请求必须被某一个处理者对象所接收;在不纯的责任链模式里面,一个请求可以最终不被任何接收端对象所接收。
责任链模式有以下几个优点:
- (1)降低耦合度,将请求的发送者和接收者解耦。反映在代码上就是不需要在类中写很多丑陋的 if….else 语句,如果用了职责链,相当于我们面对一个黑箱,只需将请求递交给其中一个处理者,然后让黑箱内部去负责传递就可以了。
- (2)简化了对象,使得对象不需要链的结构。
- (3)增加系统的灵活性,通过改变链内的成员或者调动他们的次序,允许动态地新增或者删除处理者
- (4)增加新的请求处理类很方便。
但是责任链模式也存在一些缺点:
- (1)不能保证请求一定被成功处理
- (2)系统性能将受到一定影响,并且可能会造成循环调用。
- (3)可能不容易观察运行时的特征,而且在进行代码调试时不太方便,有碍于除错。