设计模式(GoF23)是关于代码开发经验的总结,是解决特定问题的一系列套路。不是语法规定,而是一套用来提高代码的可复用性、可维护性、可读性等的解决方案。
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性、多态性以及类的关联关系和组合关系的充分理解。
优点:
- 提高程序员的抽象能力(架构师)、编程能力和设计能力。
- 程序设计更加标准化,代码编制更加工程化,提高开发效率缩短开发周期。
- 提高代码的可重用性、可读性、可靠性、灵活性和可维护性。
设计模式的基本要素:模式名称(便于记忆和讨论)、问题(应用环境,如单例模式解决系统开销问题)、解决方案(提供解决问题的抽象描述)和效果(优缺点,如时间空间,灵活性、可扩展性等)。
- 创建型模式(怎么创建对象,创建与使用分离):单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式。
- 结构型模式(将类和对象组成更大的对象):适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
- 行为型模式(描述对象和类之间相互协作,共同完成单个对象无法完成的任务,分配职责):模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
提纲
- 单例模式,某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例。
- 工厂方法模式,定义一个用于创建产品的接口,由子类决定生产什么产品。
- 抽象工厂模式,提供一个创建产品组的接口,其子类可以生产一系列相关的产品。
- 建造者模式,将复杂对象分解成多个相对简单的部分,然后根据不同的需要分别创建它们,最后构建成复杂对象。
- 原型模式,将一个对象作为原型,通过对其复制而克隆出多个和原型类似的新实例。
- 适配器模式,将一个类的接口转换成客户希望的另一个接口,使原本由于接口不兼容而不能一起工作的那些类可以一起工作。
- 桥接模式,将抽象与实现分离,使他们可以独立变化。它使用组合关系代替继承关系来实现,从而降低了抽象和实现两个可变维度的耦合度。
- 装饰模式,动态的给对象增加一些职责,即增加其额外的功能。
- 组合模式,将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
- 外观模式,为多个复杂的子系统提供一个一致性的接口,使这些子系统更加容易被访问。
- 享元模式,运用共享技术来有效的支持大量细粒度对象的复用。
- 代理模式,为某个对象提供一种代理以控制对该对象的访问。即客户端通过代理间接的访问该对象,从而限制、增强或修改对象中的一些特性。
- 模板方法模式,定义一个操作中的算法骨架,而将这些算法的步骤延迟到子类中,使得子类可以不改变算法结构的情况下重定义该算法的某些特定步骤。
- 命令模式,将一个请求封装为对象,使发出请求的责任和执行请求的责任分隔开。
- 迭代器模式,提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
- 观察者模式,多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
- 中介者模式,定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
- 备忘录模式,在不破化封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
- 解释器模式,提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。
- 状态模式,允许一个对象在其内部状态发生改变时改变其行为能力。
- 策略模式,定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
- 职责链模式,把请求从链中的一个对象传到下一个对象,直到请求被响应。通过这种方式去除对象之间的耦合。
- 访问者模式,在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问对象访问。
面向对象的七大原则
- 开闭原则:对扩展开放,对修改关闭(新添加的模块对原有模块没影响,易于扩展)。
- 里氏替换原则:继承必须确保超类所拥有的性质在子类中仍然成立(任何时候都可以使用子类型替换父类型,子类扩展父类的功能,而不改变父类原有的功能,否则复用性降低)。
- 依赖倒置原则:面向接口编程,不要面向实现编程。
- 单一职责原则: 控制类的力度大小,对对象解耦、提高其内聚性(一个类只做它该做的事)。
- 接口隔离原则:要为各个类建立它们需要的专用接口(接口小而专,绝不能大而全)。
- 迪米特法则:只与你的直接朋友交谈,不和陌生人说话(一个对象尽可能少的了解其他对象,降低耦合性,提高独立性,弊端是产生很多中间类,增加复杂性)。
合成复用原则:尽量先使用组合和聚合等关联来实现,其次才考虑使用继承关系来实现(优先使用聚合和合成,而不使用继承关系)。
1. 单例模式
单例模式可以保证一个类只生成唯一的实例对象(在整个程序空间中,只有一个实例对象),并提供对该实例全局访问的方法。
场景:多线程环境中,比如servlet环境,共享一个资源或操作同一个对象;
- 整个程序空间使用全局变量,共享资源;
- 大规模系统中,为了考虑性能,节省创建对象的时间,如线程池和数据库连接池。
懒汉模式
延迟加载,只有在使用的时候才会实例化。
这样可以了嘛?由于CPU和编译器等指令重排,可能会有其他问题,解决方案:class LazySingleton{
private static LazySingleton instance;
private LazySingleton(){
}
//假如不使用synchronized加锁,则在多线程环境下,可能返回不同的对象,但是对于方法加锁,性能损失较大。
public synchronized static LazySingleton getInstance(){
if(instance==null){
instance = new LazySingleton();
}
return instance;
}
}
//不对方法加锁,提升版本如下:
public static LazySingleton getInstance(){
if(instance==null){
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
总结:class LazySingleton{
private volatile static LazySingleton instance;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(instance==null){
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
/*字节码文件:分配空间、初始化、引用复制,但是底层如CPU,编译器可能优化,会导致问题.如,顺序是分配空间、引用复制、初始化,这样第二步完成,第三步未开始,多了线程,得到的对象是没有初始化的,这样就会抛出空指针异常,可以在变量前加入volatile关键字,防止指令重排序*/
}
}
}
return instance;
}
}
多线程安全问题;使用双重检查进行加锁优化;防止指令重排造成的使用到未初始化的实例,加上volatile关键字。饿汉模式
类的加载在初始化阶段完成了实例的初始化。基于JVM的类加载方式,保证线程安全的。
类加载过程:
- 加载二进制数据到内存中,生成对应的Class数据结构;
- 连接:验证(class文件是否符合JVM规范)、准备(给类的静态成员变量赋默认值)、解析;
- 初始化:给类的静态变量赋初值。
class HungerySingleton{
private static HungerySingleton instance = new HungerySingleton();
private HungerySingleton(){
}
public static HungerySingleton getInstance(){
return instance;
}
}
静态内部类
本质上也是利用类的加载价值保证线程安全的。只有在实际使用的时候,才会触发类的初始化,所以也是一种懒加载形式。class InnerclassSingleton{
private static class InnerclassHolder{
private static InnerclassSingleton instance = new InnerclassSingleton();
}
private InnerclassSingleton(){
//私有构造函数,防止外部进行初始化
}
public static InnerclassSingleton getInstance(){
return InnerclassHolder.instance;
}
}
对单例模式的破坏
反射
但JAVA还有个特征就是反射,是一种间接操作目标对象的机制,核心在于JVM在运动的时候才能动态加载类,并且对于一个任意的类,都能知道这个类对应的所有属性和方法,调用方法/访问属性,不需要提前在编译期知道运行的对象是谁,他允许运行中的Java程序获取类的信息,并且可以操作类或对象内部属性。程序中对象的类型一般都是在编译期就确定下来的,而当我们的程序在运行时,可能需要动态的加载一些类,由于之前没用到没加载到JVM中,这时使用Java反射机制可以在运行期动态的创建对象并调用其属性,它是在运行时根据需要才加载。
这个就会破环单例模式,得到其他的实体对象,如以下代码:
得到的运行结果可知,两者的对象是不一样的,这样就破坏了单例模式的诸多性质。public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//得到构造函数
Constructor<HungerySingleton> construcetor = HungerySingleton.class.getDeclaredConstructor();
construcetor.setAccessible(true);
HungerySingleton instance = construcetor.newInstance();
HungerySingleton test = construcetor.newInstance();
System.out.println("反射得到的对象:"+test);
System.out.println("单例得到的对象:"+instance);
System.out.println("两者一样否:"+(test==instance));
}
解决方法就是,使用一种安全的单例模式的写法,即在单例模式构造函数中加一个判断实例是否为空。反射得到的对象:com.xzm.HungerySingleton@1b6d3586
单例得到的对象:com.xzm.HungerySingleton@4554617c
两者一样否:false
运行结果可知,避免了再次创建对象。private HungerySingleton(){
if(instance!=null){
throw new RuntimeException("已经有实例化对象。");
}
}
通过对枚举类的测试,发现它一种安全的单例模式,不能通过反射注入得到新的实例对象。Exception in thread "main" 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.xzm.SingletonTest.main(SingletonTest.java:11)
Caused by: java.lang.RuntimeException: 已经有实例化对象。
at com.xzm.HungerySingleton.<init>(HungrySingletonTest.java:20)
... 5 more
序列化
序列化是在传递和保存对象时,保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。反序列化则是通过文件进行重建对象。
首先,我们将对象进行序列化,写入磁盘中,然后在进行反序列化读取文件,重建对象。
得到的运行结果为false,可知破坏了单例模式的性质,重新创建了一个新的对象,解决方案如下,在序列化对象中添加该方法,上面程序运行则为true,避免破坏单例模式://序列化
public static void main(String[] args) throws IOException {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("file.txt"));
HungerySingleton object = HungerySingleton.getInstance();
os.writeObject(object);
}
//反序列化
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("file.txt"));
HungerySingleton test =(HungerySingleton)is.readObject();
HungerySingleton instance = HungerySingleton.getInstance();
System.out.println("两者一样否:"+(test==instance));
}
private Object readResolve() {
return instance;
}
应用场景
- 在应用场景中,某类只要求生成一个对象的时候,如一个班的班长,每个人的身份证。
- 当对象需要被共享的场合,由于创建一个对象,可以节省内存,并加快对象的访问速度,如Web中的配置对象和数据库的连接池。
当某个类需要被频繁实例化后有频繁销毁,如多线程的线程池和网络连接池。
2. 工厂方法模式
工厂方法模式:不修改原来代码的情况下,引进新的产品。具体做法是:定义一个用于创建对象的接口,让子类决定实例化哪一个类。使得一个类的实例化延迟到子类。
该部分使用的框架如下,我们模拟生产两种手机,小米和苹果,实现对应的返回类型。//phone接口
public interface Phone {
void make();
}
//MiPhone类
public class MiPhone implements Phone {
public MiPhone(){
this.make();
}
@Override
public void make() {
System.out.println("make xiaomi phone.");
}
}
//省略IPhone类
//不使用模式,则会在使用时,如使用静态方法,返回一个实体对象MiPhone,这样不易于扩展。
简单工厂
该模式创建管理方式最为简单,因为其仅仅简单的对不同类对象的创建进行了一层薄薄的封装。该模式通过向工厂传递类型来指定要创建的对象,简单工厂模式不属于GOF23经典设计模式,它会违反开闭原则。
public class PhoneFactory {
public static Phone makePhone(String phonetype){
if(phonetype.equals("MiPhone")){
return new MiPhone();
}else if(phonetype.equals("IPhone")){
return new IPhone();
}else{
return null;
}
}
}
//测试方法
public static void main(String[] args) {
Phone miphone = PhoneFactory.makePhone("MiPhone");
Phone iphone = PhoneFactory.makePhone("IPhone");
}
使用简单工厂实现后,加入后期业务扩展,就需要重写PhoneFactory类,所以我们引进工厂方法模式。
工厂方法模式
工厂方法模式由抽象方法、具体工厂、抽象产品和具体产品等四个要素构成。
抽象工厂(AbstractFactory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品,实例中为AbstractFactory类,方法是makePhone()。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建,实例中为XiaoMiFactory、AppleFactory类。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,实例中为Phone类。
具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应,实例中为MiPhone,IPhone类。
//抽象工厂
public interface AbstractFactory {
Phone makePhone();
}
//具体工厂,省略AppleFactory
public class XiaoMiFactory implements AbstractFactory{
@Override
public Phone makePhone() {
return new MiPhone();
}
}
//测试方法
public static void main(String[] arg) {
AbstractFactory miFactory = new XiaoMiFactory();
AbstractFactory appleFactory = new AppleFactory();
miFactory.makePhone();//返回实例对象
appleFactory.makePhone();//返回实例对象
}
工厂方法模式符合开闭原则和单一职责原则(产品MiPhone,IPhone没有相互干扰)。
对应的UML图如下。
应用场景
用户只知道创建产品的工厂名,而不知道具体的产品名,如TCL、创维电视机等;
- 当你希望为库或框架提供扩展其内部组件的方法的时候。
创建对象的任务由多个具体子工厂的某一个完成,而抽象工厂只是提供创建产品的接口。
主要优点
用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程(具体产品与创建者解耦)。
- 灵活性增强,对于新产品的创建,只需多写一个相应的工厂类。
典型的解耦框架。高层模块只需要知道产品的抽象类,无须关心其他实现类,满足迪米特法则、依赖倒置原则和里氏替换原则。
主要缺点
类的个数容易过多,增加复杂度,以及系统的抽象性和理解难度。
-
3. 抽象工厂模式
模式定义:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
上面的模式不管工厂怎么拆分现象,都只是针对一类产品Phone(AbstractProduct),但如果要产生另一种产品PC呢?最简单方法是将上面的工厂方法模式复制一遍,但生产PC,但这样不利于维护和扩展。
抽象工厂模式通过在AbstarctFactory中增加创建产品的接口PC makePC();
,并在具体子工厂中实现新加产品的创建。
通过实例来解释清楚。
如图,对于抽象工厂AbstractFactory,不再是单纯的生产Phone,还生产PC,定义抽象的AbstractFactory类:增加PC产品制造接口。public interface AbstractFactory {
Phone makePhone();
PC makePC();
}
对于PC抽象类,其实现与Phone抽象类没有太大差别,如对于MiPC的实现如下:
//PC抽象类
public interface PC {
void make();
}
//MiPC的实现
public class MiPC implements PC {
public MiPC(){
this.make();
}
@Override
public void make() {
System.out.println("make xiaomi PC.");
}
}
实现了以上抽象类,则实现抽象工厂的主要类(与其他模式的不同之处)。
//XiaoMiFactory实现,省略AppleFactory
public class XiaoMiFactory implements AbstractFactory{
@Override
public Phone makePhone() {
return new MiPhone();
}
@Override
public PC makePC() {
return new MiPC();
}
}
//测试方法
public static void main(String[] arg) {
AbstractFactory miFactory = new XiaoMiFactory();
AbstractFactory appleFactory = new AppleFactory();
miFactory.makePhone();//同一抽象工厂,既可以制造Phone,也可以制造PC,这就是不同之处。
miFactory.makePC();
appleFactory.makePhone();
appleFactory.makePC();
}
优点
可以确信你从工厂得到的产品是彼此兼容的,避免具体产品和客户端代码之间的紧密耦合;
符合单一职责原则,和当增加一个新的产品时,不需要修改原有代码,符合开闭原则。
缺点
后期产品的扩展将比较麻烦,假如产品族中增加产品,则需要修改所有的工厂类。故使用抽象工厂模式时,最开始对产品等级结构的划分是非常重要的。
应用场景
当需要创建的对象是一系列相互关联或相互依赖的产品族时,便可以使用抽象工厂模式。也就是一个继承体系中,如果存在着多个等级结构(即存在着多个抽象类),并且分属各个等级结构中的实现类之间存在着一定的关联或者约束,就可以使用抽象工厂模式,如源码中Connection接口中,就是由一组抽象接口构成。
工厂模式总结
无论是简单工厂模式,工厂方法模式,还是抽象工厂模式,他们都属于工厂模式,在形式和特点上也是极为相似的,他们的最终目的都是为了解耦,他们之间在我们实际的开发者随着需求变化而转化。
4. 建造者模式
创建者模式又叫建造者模式,是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。创建者模式隐藏了复杂对象的创建过程,它把复杂对象的创建过程加以抽象,通过子类继承或者重载的方式,动态的创建具有复合属性的对象,其UML图如下:
四个要素产品类:一般是一个较为复杂的对象,创建复杂(代码量大)的对象。在图中,产品类是一个具体的类,而非抽象类。实际编程中,产品类可以是由一个抽象类与它的不同实现组成,也可以是由多个抽象类与他们的实现组成。
- 抽象建造者:引入抽象建造者的目的,是为了将建造的具体过程交与它的子类来实现,使他更容易扩展。一般至少会有两个抽象方法,分别用于建造产品和返回产品。
- 建造者:实现抽象类(抽象建造者)的所有未实现的方法,具体而言就是建造产品和返回建造好的产品。
导演类:具有重要作用,用于指导具体构建者如何构建产品,控制调用的先后顺序,并向调用者返回完整的产品类。有时简化系统,他会与抽象建造者结合。负责调用适当的建造者来组建产品,导演类一般不与产品类发生依赖关系。一般来说,导演类被用来封装程序中易变的部分。
主要作用
在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象。
用户只需要给出指定复杂对象的类型和内容(方便用户创建复杂对象);
建造者模式负责按创建顺序创建复杂对象(把内部的建造过程和细节隐藏)。
实例
在实例中,我们以建造房子为例,主要过程有地基、钢筋工程、铺电线和粉刷四步,分别抽象为A、B、C和D。
产品类Product,具体建造的实例。public class Product {
private String buildA;
private String buildB;
private String buildC;
private String buildD;
//省略get、set和toString方法
}
抽象建造者类和具体的建造者,前者用于与导演类进行交互,后者实现前者接口,注意构造函数中采用的是new方式创建对象,而不是传参方式。
//抽象的建造者,定义方法和接口
public abstract class Builder {
abstract void buildA();//地基
abstract void buildB();//钢筋工程
abstract void buildC();//铺电线
abstract void buildD();//粉刷
//经过ABCD四部,得到完整的房子
abstract Product getProduct();
}
//工人:具体的实现类
public class Worker extends Builder {
private Product product;
//添加构造函数
public Worker() {
//不采用传入的方式 this.product = product;,需要自己new一个,即工人创建产品
product = new Product();
}
@Override
void buildA() {
product.setBuildA("地基");
System.out.println("地基");
}
//省略BCD重复代码
@Override
Product getProduct() {
return product;
}
}
导演类,用于控制建造的主要过程顺序,返回建造的实例对象。
//指挥:核心处,负责指挥构建一个工程,由他决定工程怎么构建
public class Director {
//指挥工人按照顺序建房子
public Product build(Builder builder){
//核心的构造顺序
builder.buildA();
builder.buildB();
builder.buildC();
builder.buildD();
return builder.getProduct();
}
}
测试方法如下:
public static void main(String[] args) {
//指挥
Director director = new Director();
//指挥具体的工人完成产品
Product build = director.build(new Worker());
System.out.println(build);
}
但导演可能是客户自己,所以可以通过静态内部类的方式实现零件的无序装配构造,这种方式更加灵活,更符合定义。<br />同样是上面的代码,赋予不同的背景,对于一个KFC套餐,ABCD分别代表食物。<br />产品类Product,实例代码如下:
public class Product {
private String buildA = "汉堡";
private String buildB = "炸鸡";
private String buildC = "可乐";
private String buildD = "薯条";
//省略get、set和toString方法
}
抽象的建造类及其实现:
//抽象的建造者,定义方法和接口
public abstract class Builder {
abstract Builder buildA(String msg);
abstract Builder buildB(String msg);
abstract Builder buildC(String msg);
abstract Builder buildD(String msg);
abstract Product getProduct();
}
public class Worker extends Builder {
private Product product;
//添加构造函数
public Worker() {
//不采用传入的方式 this.product = product;,需要自己new一个,即工人创建产品
product = new Product();
}
@Override
Builder buildA(String msg) {
product.setBuildA(msg);
return this;
}
//省略BCD的方法
@Override
Product getProduct() {
return product;
}
}
通过以上方式,可以发现建造者常用的是链式结构,如果你调用build*(String msg)方法,就会重新赋值,不然采用默认值,并且,这种方法没有导演类,客户端就是导演类,控制赋值顺序。
public static void main(String[] args) {
Worker worker = new Worker();
Product product = worker.buildA("鸡肉卷").buildC("雪碧").
getProduct();
System.out.println(product);
}
//运行结果
demo.Product{buildA='鸡肉卷', buildB='炸鸡', buildC='雪碧', buildD='薯条'}
应用场景
需要生成的产品对象有复杂的内部结构,这些产品对象具备共性。
- 隔离复杂对象的创建和使用,并使得相同的构建过程可以构建不同的产品。
-
优点
产品的建造和表示分离,实现了解耦,使用建造者可以使客户不必知道产品的内部细节,便于控制细节风险。
- 将复杂产品的创捷分解到不同的方法中,使创建过程更加清晰。
具体的建造者类之间是相互独立的,有利于系统的扩展(有抽象的接口),符合开闭原则。
缺点
建造者模式所创建的产品有较多共同点,其组成部分相似,如果产品差异过大,不适合使用该模式,范围会有一定限制。
- 如果产品的内部变化复杂,可能会导致需要定义很多具体的建造者适应这种变化,导致系统变得十分庞大。
5. 原型模式
原型(Prototype)模式的定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节,如现实生活中的复印机。原型模式的结构
由于 java 提供了对象的 clone() 方法,所以用 Java 实现原型模式很简单。模式的结构原型模式包含以下主要角色。
抽象原型类:规定了具体原型对象必须实现的接口。
具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
访问类:使用具体原型类中的 clone() 方法来复制新的对象。实例
在实例中,我们实现Video类,如要利用原型创建对象,那么我们的Video类就要通过以下两个步骤实现克隆复制。
- 实现一个接口 Cloneable
- 重写一个方法 clone()
实现如下:
public class Video implements Cloneable{
private String name;
private Date createTime;
//省略构造、get、set和toString方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
测试方法和运行结果如下:
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Date date = new Date();
Video v1 = new Video("视频1", date);
Video v2 = ((Video) v1.clone());
System.out.println("v1="+v1);
System.out.println("v2="+v2);
date.setTime(1234567);
System.out.println("v1="+v1);
System.out.println("v2="+v2);
}
}
//运行结果
v1=demo.Video{name='视频1', createTime=Thu Oct 15 14:41:14 CST 2020}
v2=demo.Video{name='视频1', createTime=Thu Oct 15 14:41:14 CST 2020}
v1=demo.Video{name='视频1', createTime=Thu Jan 01 08:20:34 CST 1970}
v2=demo.Video{name='视频1', createTime=Thu Jan 01 08:20:34 CST 1970}
有运行结果可知,我们在改变date的时候,无论是v1、v2对象,都将时间改变。这也就是浅拷贝的原因,在复制的对象v2中,存储的还是v1存储date的引用,简而言之,就是在复制的时候,未复制date对象,导致原型和副本的时间都指向date对象,解决方法也就是在复制的时候,也复制一份date对象。
@Override
protected Object clone() throws CloneNotSupportedException {
Video object = (Video) super.clone();
//将这个对象的属性也克隆
object.createTime = (Date) this.createTime.clone();
return object;
}
应用场景
- 对象之间相同或相似,即只是个别的几个属性不同的时候。
-
优点
当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
- 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
缺点
需要为每一个类配备一个克隆方法,而且该克隆方法类的内部,需要对已有类进行改造,违背“开闭原则”。
在实现深度复制时需要编写较为复杂的代码,当对象之间存在多重的嵌套引用时,每一层对象对应的类都必须支持深克隆,实现麻烦。
6. 适配器模式
将一个类的接口转换成客户希望的另外一个接口,Adapter模式使得原来由于接口不兼容而不能一起工作的那些类可以在一起工作(如版本问题引起的)。如Usb网线转换器。
模式结构
目标接口:客户所期待的接口,目标可以是具体的或抽象的类,也可以是接口,如USB。
需要适配的类:需要适配的类或适配者的类,如网线。
适配器:通过包装一个需要适配的对象,把原接口转换成目标对象,用来包装网线,如USB转化器。实例
在实例中,我们通过现实中例子进行讲解,如很多电脑没有网线接口,只有USB接口,我们可以使用USB转网线接口(适配器),让电脑能够上网。
主要有两种方法实现,类适配器(继承的方式,但java只能单继承)和对象适配器(组合的方式,是比较常用的)。
需要适配的类,也就是网线类,连接他进行上网。//要被适配的类:网线
public class Adaptee {
public void request(){
System.out.println("连接网线上网.");
}
}
目标接口,也就是客户端类,电脑类,想上网,不能插网线。
//客户端类:想上网,插不上网线
public class Computer {
public void net(Adapter adapter){
//上网的具体实现,找一个转接头
adapter.handleRequest();//可以上网
}
}
转换接口的抽象实现。
//接口转换器的抽象实现
public interface NetToUsb {
//作用:处理请求,网线插到usb上
public void handleRequest();
}
适配器,将网线转接成Usb线,使网线接口成为客户需要的目标接口,为类适配器实现,测试程序中,也不需要实例化被适配的类,此时由于继承,适配器已经具有上网功能。
//真正的适配器,需要连接Usb、连接网线
public class Adapter extends Adaptee implements NetToUsb {
@Override
public void handleRequest() {
super.request();
}
}
//测试程序
public static void main(String[] args) {
Computer computer = new Computer();
Adapter adapter = new Adapter();
Adaptee adaptee = new Adaptee();//可注释不要
computer.net(adapter);
}
由于类适配器,只能单继承,一般采用组合的方式实现适配器,如源代码,综上,尽量使用对象适配器,避免使用类适配器(继承的方式)。
public class Adapter2 implements NetToUsb {
Adaptee adaptee;
public Adapter2(Adaptee adaptee){
this.adaptee=adaptee;
}
@Override
public void handleRequest() {
adaptee.request();
}
}
//测试程序
public static void main(String[] args) {
Computer computer = new Computer();
Adaptee adaptee = new Adaptee();
Adapter2 adapter = new Adapter2(adaptee);
computer.net(adapter);
}
对象适配器优点
一个对象适配器可以把多个不同的适配者适配到同一个目标。
可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配。
类适配器缺点
对于Java、C#等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者;
在Java、C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。
适用场景
系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
7. 桥接模式
桥接模式是将抽象部分与他的实现部分分离,使它们可以独立的变化,它是一种对象结构型模式,又称为柄体模式或接口模式。
实例
电脑有联想、苹果、戴尔等品牌,又有台式电、笔记本、平板等类型,加入现在在图示过程中,加入小米品牌,则需要加入三个类,这样会系统的复杂程度增加,不易于扩展,并且如联想台式、苹果台式等,违背了单一职责原则。
鉴于以上困境,我们可以将其维度化,品牌和类型分别作为一个维度。得到如下图。
类型与品牌之间采用桥接的方式连接,这样我们只需要在品牌中加入小米,就可以简单创建小米平板、小米笔记本以及小米台式机。
实现品牌接口,然后apple和lenovo实现该接口。//品牌
public interface Brand {
public void info();
}
//苹果品牌
public class Apple implements Brand {
@Override
public void info() {
System.out.print("苹果");
}
}
//联想品牌
public class Lenovo implements Brand {
@Override
public void info() {
System.out.print("联想");
}
}
将电脑类抽象出来,然后实现台式电脑和笔记本电脑。
//抽象出来的电脑类
public abstract class Computer {
//组合,品牌,桥的作用
protected Brand brand;//使用protected使让子类可以直接使用
public Computer(Brand brand) {
this.brand = brand;
}
public void info(){
brand.info();//自带品牌
}
}
class Desktop extends Computer{
public Desktop(Brand brand) {
super(brand);
}
@Override
public void info() {
super.info();
System.out.println("台式机");
}
}
class Laptop extends Computer{
public Laptop(Brand brand) {
super(brand);
}
@Override
public void info() {
super.info();
System.out.println("笔记本");
}
}
添加测试类,如生成苹果笔记本和联想台式电脑,则比较方便。
public static void main(String[] args) {
//苹果笔记本
Computer computer = new Laptop(new Apple());
computer.info();
//联想台式机
Computer computer1 = new Desktop(new Lenovo());
computer1.info();
}
//运行结果
苹果笔记本
联想台式机
优点
桥接模式偶尔类似于多继承方案,但多继承违背了类的单一职责原则,复用性较差,类的个数较多,而桥接减少了子类的个数,降低管理和维护的成本。
桥接模式提高了系统的扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。符合开闭原则,就像一座桥,可以把两个变化的维度连接起来!
缺点
桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
应用场景
如果一个系统需要在构建的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
与适配器的比较
桥接模式与适配器模式比较相像,都是A类需要持有另一个B类并调用B的方法,但对于适配器模式,两个维度(电脑和各种端口)是没有包含关系的,使用时只需获取适配器,将端口对象传入适配器即可,而对于桥接模式,电脑类包含了品牌类,两者有耦合关系。
8. 装饰模式
模式结构
抽象构件角色(Component):定义一个对象接口或抽象类,可以给这些对象动态地添加职责。
具体构件角色(ConcreteComponent):实际被动态地添加职责的对象。
抽象装饰者角色(Decorator):实现了Component接口,用来扩展Component类的功能,但对于Component来说,是无需知道Decorator的存在的。
具体装饰者角色(ConcreteDecorator):动态地添加职责的对象。
实例中,我们以煎饼果子为例,我们要在煎饼果子里面添加火腿、鸡蛋等材料。
实例中,抽象构建类为ABattercake类,后面需要具体构建类继承抽象构建类。public abstract class ABattercake {
protected abstract String getDesc();
protected abstract int cost();
}
//具体构建类
public class Battercake extends ABattercake {
@Override
protected String getDesc() {
return "煎饼";
}
@Override
protected int cost() {
return 8;
}
}
对于抽象装饰者,同样需要继承该类,在实例中抽象装饰者为AbstractDecorator 类。
public class AbstractDecorator extends ABattercake {
private ABattercake aBattercake;
public AbstractDecorator(ABattercake aBattercake) {
this.aBattercake = aBattercake;
}
@Override
protected String getDesc() {
return this.aBattercake.getDesc();
}
@Override
protected int cost() {
return this.aBattercake.cost();
}
}
后面需要在煎饼果子中添加鸡蛋或香肠,那么需要鸡蛋装饰类和火腿装饰类。这两个类继承自抽象装饰类。
public class EggDecorator extends AbstractDecorator {
public EggDecorator(ABattercake aBattercake) {
super(aBattercake);
}
@Override
protected String getDesc() {
return super.getDesc()+" 加一个鸡蛋";
}
@Override
protected int cost() {
return super.cost()+1;
}
}
public class SausageDecorator extends AbstractDecorator{
public SausageDecorator(ABattercake aBattercake) {
super(aBattercake);
}
@Override
protected String getDesc() {
return super.getDesc()+" 加一根香肠";
}
@Override
protected int cost() {
return super.cost()+2;
}
}
最后是测试类,创建一个实体煎饼果子类并赋值给抽象煎饼果子类,然后将这个父类对象注入装饰类,再把得到的对象赋值给创建的抽象对象。
public class DecoratorV2Test {
public static void main(String[] args) {
ABattercake aBattercake;
aBattercake = new Battercake();
aBattercake = new EggDecorator(aBattercake);
aBattercake = new EggDecorator(aBattercake);
aBattercake = new SausageDecorator(aBattercake);
System.out.println(aBattercake.getDesc()+" 销售价格:"+aBattercake.cost());
}
}
//运行结果
煎饼 加一个鸡蛋 加一个鸡蛋 加一根香肠 销售价格:12
应用场景
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类).
优点
不改变原有对象的情况下给一个对象扩展功能。
- 使用不同的组合可以实现不同的效果。
-
9. 组合模式
如对于一个大学,里面有计算机学院和电子信息学院,计算机学院下面有计算机、软件等专业,电子信息学院下面有通信工程、自动化等专业,如图示结构。
常规思路是让专业继承院系,院系继承学校,这样的层次结构(以组织大小进行分层次)。实际上,我们要求的是组合关系,即专业组成学院,学院组成学校。常规思路不能很好的实现管理工作,如对学院的添加、删除和遍历。
解决方案:把学校、院系、专业都看成组织结构,它们之间没有继承关系,而是树状结构,可以很好的实现管理操作。(组合模式)
组合模式,又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以表示“整体—部分”的层次关系。模式结构
抽象构件(Component)角色:这是组合中对象声明接口, 在适当情况下, 实现所有类共有的接口默认行为,用于访问和管理 Component 子部件, Component 可以是抽象类或者接口。
- 叶子构件(Leaf)角色:在组合中表示叶子节点,叶子节点没有子节点。
树枝构件(Composite)角色:非叶子节点, 用于存储子部件, 在 Component 接口中实现 子部件的相关操作,比如增加(add), 删除。
实例
如上面所讲,我们进行如下编码,编写抽象构建OrganizationComponent,作为叶子构件、树枝构建的父类。
public abstract class OrganizationComponent {
private String name;//名字
private String des;//说明
protected void add(OrganizationComponent organizationComponent){
//默认实现,设置为默认的,不使用抽象的,是因为叶子节点不用实现
throw new UnsupportedOperationException();
}
protected void remove(OrganizationComponent organizationComponent){
//默认实现,设置为默认的,不使用抽象的,是因为叶子节点不用实现
throw new UnsupportedOperationException();
}
//方法print,做成抽象的,每个子类都要实现该方法
protected abstract void print();
}
后面的学校、院系、专业类都继承抽象构件OrganizationComponent。
//University就是Composite,可以管理Department
public class University extends OrganizationComponent{
List<OrganizationComponent> organizationComponents = new ArrayList<OrganizationComponent>();//最好放在实现类中,否则每个叶子节点都存在该变量
//构造器
public University(String name, String des) {
super(name, des);
}
//重写add方法
@Override
protected void add(OrganizationComponent organizationComponent) {
organizationComponents.add(organizationComponent);
}
//重写remove方法
@Override
protected void remove(OrganizationComponent organizationComponent) {
organizationComponents.remove(organizationComponent);
}
//输出University包含的院系
@Override
protected void print() {
System.out.println("-------"+getName()+"-------");
//遍历
for (OrganizationComponent organizationComponent:organizationComponents
) {
organizationComponent.print();
}
}
}
//Department类与University类相似,省略。
public class Major extends OrganizationComponent{
public Major(String name, String des) {
super(name, des);
}
@Override
protected void print() {
System.out.println(getName());
}
}
最后的测试类,如下所示。
public static void main(String[] args) {
//从大到小创建对象
OrganizationComponent university = new University("野鸡大学", "中国的大学");
//学院
OrganizationComponent department1 = new Department("计算机学院", "码农的学院");
OrganizationComponent department2 = new Department("电子信息学院", "码农的兄弟学院");
university.add(department1);
university.add(department2);
//专业
department1.add(new Major("计算机","码农"));
department1.add(new Major("软件工程","也是码农"));
department2.add(new Major("通信工程","通信比较难"));
department2.add(new Major("自动化","自动化也难"));
university.print();
}
优点
可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
- 客户端调用简单,客户端可以一致的使用组合结构或其中单个对象。
- 定义了包含叶子对象和容器对象的类层次结构,叶子对象可以被组合成更复杂的容器对象,而这个容器对象又可以被组合,这样不断递归下去,可以形成复杂的树形结构。
更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。
缺点
使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联。
应用场景
需要表示一个对象整体或部分层次,在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,可以一致地对待它们。
让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节。
10. 外观模式
组建一个家庭影院,我们需要的设备有多种,如DVD播放器、投影仪、自动屏幕、环绕立体声、爆米花机等,我们所有设备都使用遥控器打开,可能就要打开爆米花机、放下屏幕、开投影仪、开音响等,这样导致操作比较麻烦,容易出错。
外观模式(Facade),也就是过程模式,它是为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,使得这一子系统更加容易使用。并且外观模式通过定义一致的接口,屏蔽了内部子系统的细节,使得调用端只需要和这个接口发生调用,无需关心内部细节。模式结构
其中子系统1、子系统2、子系统3都是通过外观类统一管理,客户端只需要操作外观类,就可以达到目的。
外观类(Facade):为调用端提供统一的调用接口,明白某个子系统负责处理请求,从而将调用端的请求代理给适当子系统对象。
- 调用者(client):外观接口的调用者。
子系统的集合:指模块或者子系统,处理Facade对象指派的任务,是功能的实际提供者。
实例
如上面的背景,搭建家庭影院,其中HomeThreadFacade为外观类,我们为减少代码量,以DVD播放器和爆米花机为实例,其他类相似。
我们在实例化类的时候采用的是饿汉模式,获得单例对象,,以下为DVD播放器类和爆米花机类。public class DVDplayer {
//使用单例模式, 使用饿汉式
private static DVDplayer instance = new DVDplayer();
private DVDplayer(){
//防止外部进行初始化
}
public static DVDplayer getInstanc() {
return instance;
}
public void on() {
System.out.println(" dvd on ");
}
public void off() {
System.out.println(" dvd off ");
}
public void play() {
System.out.println(" dvd is playing ");
}
public void pause() {
System.out.println(" dvd pause ..");
}
}
public class Popcorn {
private static Popcorn instance = new Popcorn();
private Popcorn(){
//防止外部进行初始化
}
public static Popcorn getInstance() {
return instance;
}
public void on() {
System.out.println(" popcorn on ");
}
public void off() {
System.out.println(" popcorn ff ");
}
public void pop() {
System.out.println(" popcorn is poping ");
}
}
对于外观类,因为为单例模式,我们不需要进行传入实体对象,可以直接获得单例对象,所以代码如下(注意构造函数)
public class HomeTheaterFacade {
//定义各个子系统对象
private Popcorn popcorn;
private DVDplayer dVDPlayer;
//构造器
public HomeTheaterFacade() {
super();
this.popcorn = Popcorn.getInstance();
this.dVDPlayer = DVDplayer.getInstanc();
}
//操作分成 4 步
public void ready() {
popcorn.pop();
dVDPlayer.on();
}
public void play() {
dVDPlayer.play();
}
public void pause() {
dVDPlayer.pause();
}
public void end() {
popcorn.off();
dVDPlayer.off();
}
}
测试函数和对应的运行结果。
public static void main(String[] args) {
HomeTheaterFacade homeTheaterFacade = new HomeTheaterFacade();
homeTheaterFacade.ready();
homeTheaterFacade.play();
}
//运行结果
popcorn is poping
dvd on
dvd is playing
优点
外观(Facade)模式是“迪米特法则”的典型应用,它有以下主要优点。
降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。
缺点
不能很好地限制客户使用子系统类,很容易带来未知风险。
增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
应用场景
通常在以下情况下可以考虑使用外观模式。
对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
- 一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
- 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
11. 享元模式
在跳棋中,我们每个棋子几乎一样,只是颜色和位置有差异,这就意味着每个棋子对象几乎一样。我们通过创建3种颜色的对象来画出10个圆圈来描述享元模式,圆圈的位置、半径可以随机。
享元模式,也叫蝇量模式:运用共享技术有效的支持大量细粒度的对象。常用于系统底层开发,解决系统的性能问题。像数据库连接池。享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似对象,需要缓冲池时。不需总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率。享:共享,元:对象
享元模式经典的应用场景就是池技术,String常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式。
模式结构
- FlyWeight : 是抽象的享元角色,他是产品的抽象类(公共部分),同时定义出对象的外部状态和内部状态的接口或实现。
- ConcreteFlyWeight:是具体的享元角色,是具体的产品类,实现抽象角色定义相关业务。
- UnSharedConcreteFlyWeight:是不可共享的角色,一般不会出现在享元工厂。
- FlyWeightFactory :享元工厂类,用于构建一个池容器(集合),同时提供从池中获取对象方法。
享元模式中,重要的是分析出对象的外部状态和内部状态。如我们使用的围棋,棋子的颜色相对比较稳定,为内部状态,而对于棋子下下去的位置,是变化的,就是所谓的外部状态。
- 内部状态指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变;
外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态。
实例
使用享元模式,解决画多个相似对象的问题,UML结构图如下。
定义Shape抽象类,然后通过继承该抽象类,表示内部状态。public interface Shape {
void draw();
}
//接口的实现类
public class Circle implements Shape {
private String color;
private int x;
private int y;
private int radius;
public Circle(String color){
this.color = color;
}
//省略其他方法
}
我们圈圈的颜色,属于内部状态,这里只是一个字符串,实际情况中比较复杂,可能是一个复杂对象,创建比较消耗性能效率,外部情况则是圆圈的位置、圆圈的半径。
public class ShapeFactory {
private static final HashMap<String, Shape> circleMap = new HashMap<>();
public static Shape getCircle(String color) {
Circle circle = (Circle)circleMap.get(color);
if(circle == null) {
circle = new Circle(color);
circleMap.put(color, circle);
System.out.println("Creating circle of color : " + color);
}
return circle;
}
}
测试类,创建10个随机颜色对象。
public class FlyweightPatternDemo {
private static final String colors[] =
{ "Red", "Green", "Blue"};
public static void main(String[] args) {
for(int i=0; i < 10; ++i) {
Circle circle =
(Circle)ShapeFactory.getCircle(getRandomColor());
circle.setX(getRandomX());
circle.setY(getRandomY());
circle.setRadius(100);
circle.draw();
}
}
private static String getRandomColor() {
return colors[(int)(Math.random()*colors.length)];
}
private static int getRandomX() {
return (int)(Math.random()*100 );
}
private static int getRandomY() {
return (int)(Math.random()*100);
}
}
优点
享元模式大大减少了对象的创建,降低了程序内存的占用,提高效率。
缺点
提高了系统的复杂度。需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变。
应用场景
系统中有大量相似对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式。
享元模式经典的应用场景是需要缓冲池的场景,比如String常量池、数据库连接池。
12. 代理模式
代理模式:为对象提供一个替身,以控制这个对象的访问,即通过代理对象访问目标对象。好处是:在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
代理的对象:远程对象、创建开销大的对象或需要安全控制的对象。
代理模式有三种不同的形式,主要是三种,静态代理、动态代理(JDK代理,接口代理)和Cglib代理(可以在内存中动态的创建对象,而不需要实现接口,属于动态代理的范畴)。静态代理
静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类。
如下例的ITeacherDao,就是代理类,和被代理类的相同接口或相同父类。实例
实现ITeacherDao接口,然后目标类实现该接口。public interface ITeacherDao {
void teach();
}
public class TeacherDao implements ITeacherDao{
@Override
public void teach() {
System.out.println("老师正在教书...");
}
}
通过接口的方式聚合该目标对象,然后进行目标方法的增强。
//代理对象,静态代理
public class TeacherDaoProxy implements ITeacherDao{
private ITeacherDao target;//目标对象,通过接口来聚合
//构造器
public TeacherDaoProxy(ITeacherDao target) {
this.target = target;
}
@Override
public void teach() {
System.out.println("代理开始...");
target.teach();
System.out.println("提交...");
}
}
测试方法和测试结果。
public static void main(String[] args) {
//创建目标对象,也就是被代理对象
TeacherDao teacherDao = new TeacherDao();
//创建代理对象,同时将被代理对象传递给代理对象
TeacherDaoProxy teacherDaoProxy = new TeacherDaoProxy(teacherDao);
//通过代理对象,调用到被代理对象的方法
teacherDaoProxy.teach();
}
//运行结果
代理开始...
老师正在教书...
提交...
优点
在不改变目标对象的功能的前提下,能通过代理对象对目标功能的扩展。
缺点
代理对象需要与目标对象实现一样的接口,所以会有很多的代理类。
一旦接口增加方法,目标对象与代理对象都要维护。
特别注意:代理对象与目标对象要实现相同的接口,然后通过调用相同的方法来调用目标对象的方法。动态代理
代理对象不需要实现接口,但目标对象要实现接口,否则不能实现动态代理。
代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象。动态代理所以又叫接口代理或者JDK代理。(java反射机制)实例
对于上例,我们的ITeacherDao和TeacherDao类的书写是一样的,可以直接复制。其不同的是ProxyFactory类的书写。
该类主要的是他的构造函数,用于传入target对象(Object类型);让后就是getProxyInstance方法,生成并返回一个代理对象。public class ProxyFactory {
//维护一个目标对象,object
private Object target;
//构造器,对target进行初始化
public ProxyFactory(Object target) {
this.target = target;
}
//给目标对象生成一个代理对象
public Object getProxyInstance(){
/*
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
ClassLoader loader,指定当前目标对象使用的类加载器,获取加载器的方法固定。
Class<?>[] interfaces,目标对象实现的接口类型,使用泛型方法确认类型。
InvocationHandler h,事情处理,执行目标对象的方法时,会触发事情处理器方法,会把当前执行的目标对象方法作为参数传入。
*/
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("JDK代理开始...");
//反射机制,调用目标对象的方法
Object invoke = method.invoke(target, args);
return invoke;
}
});
}
}
测试方法和运行结果。
public static void main(String[] args) {
//创建目标对象
ITeacherDao target = new TeacherDao();
//给目标对象创建代理对象,可以转成ITeacherDao
ITeacherDao proxyInstance = (ITeacherDao)new ProxyFactory(target).getProxyInstance();
//运行结果是proxyInstance=class com.sun.proxy.$Proxy0,可知在内存中动态生成了代理对象
System.out.println("proxyInstance="+proxyInstance.getClass());
//通过代理对象调用目标对象的方法
proxyInstance.teach();
}
//运行结果
proxyInstance=class com.sun.proxy.$Proxy0
JDK代理开始...
老师正在教书...
Cglib代理
由于静态代理或动态代理都需要我们实现实现一个接口,如果目标对象没有实现任何接口,我们就可以使用 Cglib代理代理。
Cglib代理又叫子类代理,它是在内存中构建一个子类对象从而实现对目标对象的功能拓展。Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口。
Cglib的底层原理是通过使用字节码处理框架ASM来转换字节码并生成新的类。注意细节
引入相关的依赖包
- 内存中动态构建子类,注意代理的类不能为final,否则会报错(final不能生成子类);
对于Cglib代理,如果方法为final或者static,就不会被代理(拦截)。
实例
public class TeacherDao {
public String teach() {
System.out.println(" 老师授课中 , 我是cglib代理,不需要实现接口 ");
return "hello";
}
}
对于cglib实现动态代理,我们需要导入额外的jar包,对于maven工程,需要我们引入依赖。
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
对于代理类的书写,我们可以这样实现。
public class ProxyFactory implements MethodInterceptor {
//维护一个目标对象
private Object target;
//构造器,传入一个被代理的对象
public ProxyFactory(Object target) {
this.target = target;
}
//返回一个代理对象: 是 target 对象的代理对象
public Object getProxyInstance() {
//1. 创建一个工具类
Enhancer enhancer = new Enhancer();
//2. 设置父类
enhancer.setSuperclass(target.getClass());
//3. 设置回调函数
enhancer.setCallback(this);
//4. 创建子类对象,即代理对象
return enhancer.create();
}
//重写 intercept 方法,会调用目标对象的方法
@Override
public Object intercept(Object arg0, Method method, Object[] args, MethodProxy arg3) throws Throwable {
// TODO Auto-generated method stub
System.out.println("Cglib代理模式 ~~ 开始");
Object returnVal = method.invoke(target, args);
System.out.println("Cglib代理模式 ~~ 提交");
return returnVal;
}
}
测试类,和上面几种方法差不多类似。
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建目标对象
TeacherDao target = new TeacherDao();
//获取到代理对象,并且将目标对象传递给代理对象
TeacherDao proxyInstance = (TeacherDao)new ProxyFactory(target).getProxyInstance();
//执行代理对象的方法,触发intecept 方法,从而实现 对目标对象的调用
String res = proxyInstance.teach();
System.out.println("res=" + res);
}
代理模式的变体
防火墙代理:内网通过代理穿透防火墙,实现对公网的访问。
- 缓存代理:当请求图片文件等资源的时候,先到缓存代理取,如果取到则ok,如果取不到,再到公网或数据库中,然后缓存。
- 远程代理:远程对象的本地代表,通过它可以把远程对象当本地对象来调用,远程代理通过网络和真正的远程对象沟通信息。
-
13. 模板方法模式
模方法模式,又叫模板模式,在一个抽象类公开定义了执行它的方法的模板,他的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。(行为模式)
换句话说,模板方法定义了一个操作的算法骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤。模式结构
AbstractClass抽象类:类中实现了模板方法,定义了算法的骨架,具体子类需要去实现其他的抽象方法operation2,3,4。
ConcreteClass实现抽象方法operation2,3,4,已完成算法中特定子类中的相关步骤。
实例
模拟豆浆的制作过程,定义模板,步骤依次为选材、添加原材料、浸泡、打磨四个步骤,对于不同的豆浆,都会经过这几个步骤,只是具体的实现细节不一样,我们可以使用模板方法进行定义。
实现模板方法的抽象类。//抽象类,表示豆浆
public abstract class SoyaMilk {
//模板方法,make,make方法可以是final修饰,不让子类去覆盖。
final void make(){
select();
add();
soak();
beat();
}
//选材料
private void select(){
System.out.println("第一步,选择新鲜的黄豆。");
}
//添加不同的配料,抽象方法,子类具体实现
abstract void add();
//浸泡
private void soak(){
System.out.println("第三步,黄豆和配料进行浸泡。");
}
//打磨
private void beat(){
System.out.println("第四步,黄豆和配料进行打磨。");
}
}
对抽象类进行继承,并重写抽象类中的抽象方法。
public class RedBeanSoyaMilk extends SoyaMilk {
@Override
void add() {
System.out.println("第二步,添加上好的红豆。");
}
}
添加测试方法,以及运行结果。
public static void main(String[] args) {
//制作红豆豆浆
SoyaMilk redBeanSoyaMilk = new RedBeanSoyaMilk();
redBeanSoyaMilk.make();
}
//运行结果
第一步,选择新鲜的黄豆。
第二步,添加上好的红豆。
第三步,黄豆和配料进行浸泡。
第四步,黄豆和配料进行打磨。
模板方法模式中的钩子方法,在模板方法模式的父类中,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖他,该方法视为钩子。
如上面的例子,做纯豆浆,不加任何的配料,修改如下,添加钩子函数。//抽象类,表示豆浆
public abstract class SoyaMilk {
//模板方法,make,make方法可以是final修饰,不让子类去覆盖。
final void make(){
select();
if(customerWantadd()){
add();
}
soak();
beat();
}
//省略选材、添加调料和浸泡步骤
//钩子函数
boolean customerWantadd( ){
return true;
}
}
对于纯豆浆类的书写,如下。
public class PureSoyaMilk extends SoyaMilk{
@Override
void add() {
//空实现
}
//覆盖父类的方法,返回结果为false
@Override
boolean customerWantadd() {
return false;
}
}
测试方法及结果。
SoyaMilk pureSoyaMilk = new PureSoyaMilk();
pureSoyaMilk.make();
//测试结果
第一步,选择新鲜的黄豆。
第三步,黄豆和配料进行浸泡。
第四步,黄豆和配料进行打磨。
优点
算法只存在于一个地方,也就是在父类中,容易修改,如果需要修改算法时,只需要改父类的方法。
- 实现了最大代码复用,父类的模板方法和已经实现的某些步骤会被子类继承而直接使用。
既统一了算法,也提供了最大的灵活性,父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现。
缺点
每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大。
一般模板方法我们会加上final关键字,防止子类重写模板方法。,如实例的make方法。使用场景
当要完成某个阶段,该过程要执行一系列的步骤,这一系列的步骤基本相同,但其个别步骤在实现时可能不同,通常考虑用模板方法模式进行处理。
14. 命令模式
如我们买了一套智能家电,有照明、风扇、冰箱等,我们只要安装app就能实现这些设备的智能控制,但不想每一种家电都需要一个app,我们希望有一个app就能控制所有的家电。要实现这种请求,则每个家电就要提供一个统一的接口,供APP调用,考虑命令模式。
命令模式就是将动作的请求者从动作的执行者对象中解耦出来。
在命令模式中,会将一个请求封装成一个对象,以便使用不同的参数来表示不同的请求,同时命令模式也支持可撤销的操作。(必须要实现撤销方法)模式结构
Invoker,是调用者角色。
- Command,是命令角色,需要执行的命令都在这里,可以是接口,也可以是抽象类。
- Receiver,接收者角色,知道如何执行一个请求的相关操作。
ConcreteCommand,是将一个接收者对象与一个动作绑定,调用接收者相应的操作,实现execute。
实例
使用命令模式,实现前面的智能家居项目,命令模式相当于一个遥控器,可以独立的对某个家居进行开关操作。,而外观模式,则是只能所有的设备一起控制。
实例中只实现电灯、电视,其他电器进行类比操作,就相当于一个遥控器可以控制多个电器,如电灯的开关,电视机的开关。
对于被控制的灯类,其实现不依赖任何接口,只是被聚合。public class LightReceiver {
public void on() {
System.out.println(" 电灯打开了.. ");
}
public void off() {
System.out.println(" 电灯关闭了.. ");
}
}
命令接口的实现,及其实现子类的实现代码。
//创建命令接口
public interface Command {
//执行动作(操作)
public void execute();
//撤销动作(操作)
public void undo();
}
//三个实现子类
public class LightOnCommand implements Command {
//聚合LightReceiver
LightReceiver light;
//构造器
public LightOnCommand(LightReceiver light) {
this.light = light;
}
@Override
public void execute() {
// TODO Auto-generated method stub
//调用接收者的方法
light.on();
}
@Override
public void undo() {
// TODO Auto-generated method stub
//调用接收者的方法
light.off();
}
}
public class LightOffCommand implements Command {
LightReceiver light;
public LightOffCommand(LightReceiver light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
@Override
public void undo() {
light.on();
}
}
/**
* 没有任何命令,即空执行: 用于初始化每个按钮, 当调用空命令时,对象什么都不做
* 其实,这样是一种设计模式, 可以省掉对空判断
* @author Administrator
*/
public class NoCommand implements Command {
@Override
public void execute() {}
@Override
public void undo() {}
}
对于远程控制类,相当于遥控器,我们这样实现,其中数组下标为0表示电灯,数组下标为1表示电视机。
public class RemoteController {
// 开 按钮的命令数组
Command[] onCommands;
Command[] offCommands;
// 执行撤销的命令
Command undoCommand;
// 构造器,完成对按钮初始化
public RemoteController() {
onCommands = new Command[2];
offCommands = new Command[2];
for (int i = 0; i < 2; i++) {
onCommands[i] = new NoCommand();
offCommands[i] = new NoCommand();
}
}
// 给我们的按钮设置你需要的命令
public void setCommand(int no, Command onCommand, Command offCommand) {
onCommands[no] = onCommand;
offCommands[no] = offCommand;
}
// 按下开按钮
public void onButtonWasPushed(int no) { // no 0
// 找到你按下的开的按钮, 并调用对应方法
onCommands[no].execute();
// 记录这次的操作,用于撤销
undoCommand = onCommands[no];
}
// 按下开按钮
public void offButtonWasPushed(int no) { // no 0
// 找到你按下的关的按钮, 并调用对应方法
offCommands[no].execute();
// 记录这次的操作,用于撤销
undoCommand = offCommands[no];
}
// 按下撤销按钮
public void undoButtonWasPushed() {
undoCommand.undo();
}
}
对于测试类,我们需要创建接收者,以及相关的命令,遥控器,遥控器设置相关的命令。
public class Client {
public static void main(String[] args) {
//使用命令设计模式,完成通过遥控器,对电灯的操作
//创建电灯的对象(接受者)
LightReceiver lightReceiver = new LightReceiver();
//创建电灯相关的开关命令
LightOnCommand lightOnCommand = new LightOnCommand(lightReceiver);
LightOffCommand lightOffCommand = new LightOffCommand(lightReceiver);
//需要一个遥控器
RemoteController remoteController = new RemoteController();
//给我们的遥控器设置命令, 比如 no = 0 是电灯的开和关的操作
remoteController.setCommand(0, lightOnCommand, lightOffCommand);
System.out.println("--------按下灯的开按钮-----------");
remoteController.onButtonWasPushed(0);
System.out.println("--------按下灯的关按钮-----------");
remoteController.offButtonWasPushed(0);
System.out.println("--------按下撤销按钮-----------");
remoteController.undoButtonWasPushed();
}
}
注意细节
将发起请求的对象与执行请求的对象解耦。发起请求的对象是调用者,调用者只要调用命令对象的 execute()方法就可以让接收者工作,而不必知道具体的接收者对象是谁、是如何实现的,命令对象会负责让接收者执行请 求的动作,也就是说:”请求发起者”和“请求执行者”之间的解耦是通过命令对象实现的,命令对象起到了 纽带桥梁的作用。
- 容易设计一个命令队列。只要把命令对象放到列队,就可以多线程的执行命令,可以实现对请求的撤销和重做。
空命令也是一种设计模式,它为我们省去了判空的操作。在上面的实例中,如果没有用空命令,我们每按下一 个按键都要判空,这给我们编码带来一定的麻烦。
缺点
可能导致某些系统有过多的具体命令类,增加了系统的复杂度,这点在在使用的时候要注意。
应用场景
界面的一个按钮都是一条命令、模拟 CMD(DOS 命令)订单的撤销/恢复、触发反馈机制。
15. 迭代器模式
本模式采用组合模式的中的实例,即一个学校有多个院系,每个院系有多个专业。如果每个院系采用不同的结构来存储专业(计算机学院用数组,电子信息学院采用集合的方式存储),这样对于遍历其专业就比较麻烦,若遍历则需要使用多种遍历方式,可能会暴露元素的内部结构。
迭代器模式(Iterator Pattern)是常用的设计模式,属于行为模式。迭代器模式提供了一种遍历集合元素的统一接口,用一致的方法遍历集合元素,不需要知道集合对象的底层表示,即不暴露其内部的结构。模式结构
抽象容器(Aggregate):一般是一个接口,提供一个iterator()方法,例如java中的Collection接口,List接口,Set接口等。
- 具体容器(ConcreteAggregate):就是抽象容器的具体实现类,比如List接口的有序列表实现ArrayList,List接口的链表实现LinkList,Set接口的哈希列表的实现HashSet等。
- 抽象迭代器(Iteator):定义遍历元素所需要的方法,一般来说会有这么三个方法:取得第一个元素的方法first(),取得下一个元素的方法next(),判断是否遍历结束的方法isDone()(或者叫hasNext()),移出当前对象的方法remove(),
- 迭代器实现(ConcreteIterator):实现迭代器接口中定义的方法,完成集合的迭代。
实例
在这个实例中,我们采用的是组合模式相似的背景,只是一个学校有多个学院,一个学院有多个系,**我们不关心学院内部采用的是数组还是集合进行存储**,所以使用迭代器模式。(用迭代器完成学校院系的展示)<br />![](https://ftp.bmp.ovh/imgs/2020/10/d8c01ea507d456ec.png#crop=0&crop=0&crop=1&crop=1&id=RZ4q8&originHeight=609&originWidth=954&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />首先,关于系,学院的编码如下。
//系
public class Department {
private String name;
private String desc;
//省略get、set方法和构造器
}
public interface College {
public String getName();
//增加系的方法
public void addDepartment(String name, String desc);
//返回迭代器,用于遍历
public Iterator createIterator();
}
对于迭代器接口,JDK中已经实现了,所以我们就不需要编写相关代码,只需要创建ComputerCollegeIterator类,该类主要通过实现Iterator接口,通过构造器传入数据(在实例中ComputerCollegeIterator和InfoCollegeIterator类差不多,此处限于篇幅,省略)。
public class ComputerCollegeIterator implements Iterator {
//在这里我们需要知道Department是以怎样的方式存放的
Department[] departments;//以数组的方式存放
int position = 0; //遍历的位置
public ComputerCollegeIterator(Department[] departments) {
this.departments = departments;
}
//判断是否还有下一个元素
@Override
public boolean hasNext() {
if(position >= departments.length || departments[position] == null) {
return false;
}else {
return true;
}
}
@Override
public Object next() {
Department department = departments[position];
position += 1;
return department;
}
//删除的方法我们默认空实现
public void remove() {
}
}
对于输出类的编写,主要是通过传入学院参数,然后遍历学院,逐一输出学院中的专业或系。
public class OutPutImpl {
//学院集合
List<College> collegeList;
public OutPutImpl(List<College> collegeList) {
this.collegeList = collegeList;
}
//遍历所有的学院,调用printDepartment,输出各个学院的系
public void printCollege() {
//从collegeList取出所有的学院, Java 中的List已经实现Iterator
Iterator<College> iterator = collegeList.iterator();
while(iterator.hasNext()) {
//取出一个学院
College college = iterator.next();
System.out.println("=== "+college.getName() +"=====" );
printDepartment(college.createIterator());
}
}
//输出 学院输出系
public void printDepartment(Iterator iterator) {
//此处使用的是我们实现的方法。
while(iterator.hasNext()) {
Department d = (Department)iterator.next();
System.out.println(d.getName());
}
}
}
测试类和运行结果。
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建学院
List<College> collegeList = new ArrayList<College>();
ComputerCollege computerCollege = new ComputerCollege();
InfoCollege infoCollege = new InfoCollege();
collegeList.add(computerCollege);
collegeList.add(infoCollege);
OutPutImpl outPutImpl = new OutPutImpl(collegeList);
outPutImpl.printCollege();
}
//运行结果省略
优点
- 提供一个统一的方法遍历对象,客户不用再考虑聚合的类型,使用一种方法就可以遍历对象。
- 隐藏了聚合的内部结构,客户端要遍历聚合的时候只能取到迭代器,而不会知道聚合的具体组成(无需关心是List还是数组)。
- 提供了一种设计思想,就是一个类应该只有一个引起变化的原因(叫做单一责任原则)。在聚合类中,我们把迭代器分开,就是要把管理对象集合和遍历对象集合的责任分开,这样一来集合改变的话,只影响到聚合对象。 而如果遍历方式改变的话,只影响到了迭代器。
当要展示一组相似对象,或者遍历一组相同对象时使用, 适合使用迭代器模式。
缺点
每个聚合对象都要一个迭代器,会生成多个迭代器不好管理类,如ArrayList或LinkList。
应用场景
当需要为聚合对象提供多种遍历方式的时候。
- 当需要为遍历不同的聚合结构提供一个统一的接口的时候。
- 当访问一个聚合对象的内容而无需暴露其内部细节的表示的时候。
案例:Java集合框架:List, Set, Map 都支持迭代
16. 观察者模式
气象站将每天测量的温度、湿度,气压等以公告的形式发布出去(自己的网站或第三方),这需要我们设计开放的api,便于第三方也能接入数据,测量数据更新的时候,能够实时的通知给第三方。
普通方案实现
下面的示意图,WheatherData表示获取数据的类,然后CurrentConditions表示气象局自己的网站展示类,我们以推送的方式,改变气象局展示的数据。
显示当前天气情况(可以理解成是气象站自己的网站)
public class CurrentConditions {
// 温度,气压,湿度
private float temperature;
private float pressure;
private float humidity;
//更新 天气情况,是由 WeatherData 来调用,我使用推送模式
public void update(float temperature, float pressure, float humidity) {
this.temperature = temperature;
this.pressure = pressure;
this.humidity = humidity;
display();
}
//显示
public void display() {
System.out.println("***Today mTemperature: " + temperature + "***");
System.out.println("***Today mPressure: " + pressure + "***");
System.out.println("***Today mHumidity: " + humidity + "***");
}
}
核心类
- 包含最新的天气情况信息 。
- 含有 CurrentConditions 对象。
- 当数据有更新时,就主动的调用 CurrentConditions对象update方法(含 display), 这样他们(接入方)就看到最新的信息。
测试类的书写如下。public class WeatherData {
private float temperatrue;
private float pressure;
private float humidity;
private CurrentConditions currentConditions;
//加入新的第三方
public WeatherData(CurrentConditions currentConditions) {
this.currentConditions = currentConditions;
}
//省略get/set方法
public void dataChange() {
//调用 接入方的 update
currentConditions.update(getTemperature(), getPressure(), getHumidity());
}
//当数据有更新时,就调用 setData
public void setData(float temperature, float pressure, float humidity) {
this.temperatrue = temperature;
this.pressure = pressure;
this.humidity = humidity;
//调用dataChange, 将最新的信息 推送给 接入方 currentConditions
dataChange();
}
}
那么问题来了,添加其他第三方的时候,不利于动态加入和维护(现实生活中,气象局维护第三方API也不可能)。不符合“开闭原则”,所以引出了观察者模式。public static void main(String[] args) {
//创建接入方 currentConditions
CurrentConditions currentConditions = new CurrentConditions();
//创建 WeatherData 并将 接入方 currentConditions 传递到 WeatherData中
WeatherData weatherData = new WeatherData(currentConditions);
//更新天气情况
weatherData.setData(30, 150, 40);
//天气情况变化
weatherData.setData(40, 160, 20);
}
观察者模式
观察模式相当于订阅服务(订牛奶),如气象局(牛奶站)相当于Subject,用户和第三方相当于Observer。
对于Subject有的操作,如登记注册、移除和通知。
- registerObserver,注册Observer
- removeObserver,移除Observer
- notifyObserver,通知所有注册的用户,根据不同需求,可以是更新数据,也可以是用户来取。
对于Observer,接受输入的,一定会有update方法。
观察者模式:对象之间多对一依赖的一种设计方案,被依赖的对象为Subject,依赖的对象为Observer,Subject通知Observer变化。
首先我们要实现Subject和Observer接口。
public interface Observer {
public void update(float temperature, float pressure, float humidity);
}
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObservers();
}
然后在WeatherDate类中实现上面的Subject接口,然后重要的是使用List存储观察者。
import java.util.ArrayList;
/**
* 类是核心
* 1. 包含最新的天气情况信息
* 2. 含有 观察者集合,使用ArrayList管理
* 3. 当数据有更新时,就主动的调用 ArrayList, 通知所有的(接入方)就看到最新的信息
* @author Administrator
*
*/
public class WeatherData implements Subject {
private float temperatrue;
private float pressure;
private float humidity;
//观察者集合
private ArrayList<Observer> observers;
//加入新的第三方
public WeatherData() {
observers = new ArrayList<Observer>();
}
//当数据有更新时,就调用 setData
public void setData(float temperature, float pressure, float humidity) {
this.temperatrue = temperature;
this.pressure = pressure;
this.humidity = humidity;
//将最新的信息 推送给 接入方 currentConditions
notifyObservers();
}
//注册一个观察者
@Override
public void registerObserver(Observer o) {
// TODO Auto-generated method stub
observers.add(o);
}
//移除一个观察者
@Override
public void removeObserver(Observer o) {
// TODO Auto-generated method stub
if(observers.contains(o)) {
observers.remove(o);
}
}
//遍历所有的观察者,并通知
@Override
public void notifyObservers() {
// TODO Auto-generated method stub
for(int i = 0; i < observers.size(); i++) {
observers.get(i).update(this.temperatrue, this.pressure, this.humidity);
}
}
}
对于上面的CurrentConditions类,我们则需要实现该接口,其他与传统方式没有差别,故省略。测试方法如下,重要的是观察者的注册操作,可以动态添加执行。
public static void main(String[] args) {
//创建一个WeatherData
WeatherData weatherData = new WeatherData();
//创建观察者
CurrentConditions currentConditions = new CurrentConditions();
//注册到weatherData
weatherData.registerObserver(currentConditions);
//测试
System.out.println("通知各个注册的观察者, 看看信息");
weatherData.setData(10f, 100f, 30.3f);
}
观察模式的好处:
- 观察模式设计好后,以集合的方式管理观察者,包括注册,移除和通知。
这样我们增加观察者,就不用修改核心类,在实例中也就是WeatherData,遵守了ocp原则。
17. 中介者模式
智能家庭包括多种设备,闹钟、咖啡机、电视机和窗帘,主人需要看电视的时候,各个设备可以协同工作,自动完成看电视的准备工作,如窗帘落下,咖啡机开始工作等。(注意此处我们使用中介者模式,不使用外观模式)
中介者模式,用一个中介对象来封装一系列的对象交互,中介者模式使各对象不需要显式的相互引用,从而使其耦合松散,可以独立地改变它们之间的交互。
如MVC模式,就是使用了中介者模式。模式结构
Mediator就是抽象中介者,定义了同事对象到中介者对象的接口。
- College是抽象同事类,也就是后面我们要实现子系统的父类。
- ConcreteMediator具体的中介者对象,实现抽象者方法,需要知道所有的具体的同事类,即以一个集合来管理,并接受某个同事对象的消息,完成相应的任务。
- ConcreteCollgague,具体的同事类,会有很多,每个同事只知道自己的行为,而不了解其他同事类的行为,但是他们都依赖中介者对象。(同事与同事之间是独立的)
实例
我们的实例以上面的例子为实例,进行相关的编码操作。
对于该类,可能有一点抽象,我们进行智能家庭的相关流程讲解。
- 创建一个ConcreteMediator
- 创建各个同事类对象,如TV(构造器传入ConcreteMediator)
- 在创建同事类的时候,直接通过构造器,加入到colleagueMap中
- 同事类的对象调用sendMessage,最终会调用ConcreteMediator中的getMessage方法
- getMessage会根据接收到的同事对象发出的消息协调调用其他同事类,完成任务。
- 可以看到getMessage是核心方法,,完成相对应的文字。
在实例中,我们以TV类为主要的类,进行相关的代码编写。
对于上面Mediator和Colleague抽象类的编写如下。
//同事抽象类
public abstract class Colleague {
private Mediator mediator;
public String name;
public Colleague(Mediator mediator, String name) {
this.mediator = mediator;
this.name = name;
}
public Mediator GetMediator() {
return this.mediator;
}
public abstract void SendMessage(int stateChange);
}
public abstract class Mediator {
//将给中介者对象,加入到集合中
public abstract void Register(String colleagueName, Colleague colleague);
//接收消息, 具体的同事对象发出
public abstract void GetMessage(int stateChange, String colleagueName);
public abstract void SendMessage();
}
ConcreteMediator和TV类则继承上面的抽象类,并完成相关代码。
public class TV extends Colleague {
public TV(Mediator mediator, String name) {
super(mediator, name);
mediator.Register(name, this);
}
@Override
public void SendMessage(int stateChange) {
this.GetMediator().GetMessage(stateChange, this.name);
}
public void StartTv() {
System.out.println("It's time to StartTv!");
}
public void StopTv() {
System.out.println("StopTv!");
}
}
//具体的中介者类,重点函数
public class ConcreteMediator extends Mediator {
//集合,放入所有的同事对象
private HashMap<String, Colleague> colleagueMap;
private HashMap<String, String> interMap;
public ConcreteMediator() {
colleagueMap = new HashMap<String, Colleague>();
interMap = new HashMap<String, String>();
}
@Override
public void Register(String colleagueName, Colleague colleague) {
colleagueMap.put(colleagueName, colleague);
if (colleague instanceof Alarm) {
interMap.put("Alarm", colleagueName);
} else if (colleague instanceof CoffeeMachine) {
interMap.put("CoffeeMachine", colleagueName);
} else if (colleague instanceof TV) {
interMap.put("TV", colleagueName);
} else if (colleague instanceof Curtains) {
interMap.put("Curtains", colleagueName);
}
}
//具体中介者的核心方法
//1. 根据得到消息,完成对应任务
//2. 中介者在这个方法,协调各个具体的同事对象,完成任务
@Override
public void GetMessage(int stateChange, String colleagueName) {
//处理闹钟发出的消息
if (colleagueMap.get(colleagueName) instanceof Alarm) {
if (stateChange == 0) {
((CoffeeMachine) (colleagueMap.get(interMap
.get("CoffeeMachine")))).StartCoffee();
((TV) (colleagueMap.get(interMap.get("TV")))).StartTv();
} else if (stateChange == 1) {
((TV) (colleagueMap.get(interMap.get("TV")))).StopTv();
}
} else if (colleagueMap.get(colleagueName) instanceof CoffeeMachine) {
((Curtains) (colleagueMap.get(interMap.get("Curtains"))))
.UpCurtains();
} else if (colleagueMap.get(colleagueName) instanceof TV) {//如果TV发现消息
} else if (colleagueMap.get(colleagueName) instanceof Curtains) {
}
}
@Override
public void SendMessage() {
}
}
编写测试类,得到测试结果。
public static void main(String[] args) {
//创建一个中介者对象
Mediator mediator = new ConcreteMediator();
//创建Alarm 并且加入到 ConcreteMediator 对象的HashMap
Alarm alarm = new Alarm(mediator, "alarm");
//创建了CoffeeMachine 对象,并 且加入到 ConcreteMediator 对象的HashMap
CoffeeMachine coffeeMachine = new CoffeeMachine(mediator,
//创建 Curtains , 并 且加入到 ConcreteMediator 对象的HashMap
Curtains curtains = new Curtains(mediator, "curtains");
TV tV = new TV(mediator, "TV");
//让闹钟发出消息
alarm.SendAlarm(0);
coffeeMachine.FinishCoffee();
alarm.SendAlarm(1);
}
//运行结果
It's time to startcoffee!
It's time to StartTv!
After 5 minutes!
Coffee is ok!
I am holding Up Curtains!
StopTv!
优点
- 多个类相互耦合,会形成网状结构,使用中介者模式会将网状结构分离成星型结构,进行解耦。
-
缺点
中介者承担了较多的责任,一旦中介者出现问题,整个系统都会受到影响。
如果设计不当,中介者对象本身会变得过于复杂,实际使用中需要注意。
18. 备忘录模式
游戏角色状态恢复问题,游戏角色有攻击力和防御力,在大战前保存自生的状态(攻击力和防御力),当大战Boss后攻击力和防御力下降,从备忘录对象恢复到大战前的状态。
传统的方案是在创建一个对象,保存,但是这样不利于管理。开销较大,同时也会暴露了对象内部的细节,备忘录模式可以解决那个问题。
备忘录模式是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样就可以对象原先的状态恢复。模式结构
originator,对象(需要保存状态的对象)
- Memento,备忘录对象,她负责保存好记录,即originator类的对象。
Caretaker ,管理者,负责保存好备忘录的Memento,不能对备忘录的内容进行操作或检查。
实例
我们通过编码,实现上面的类图,编码如下。
public class Originator {
private String state;//状态信息
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
//编写一个方法,可以保存一个状态对象 Memento
//因此编写一个方法,返回 Memento
public Memento saveStateMemento() {
return new Memento(state);
}
//通过备忘录对象,恢复状态
public void getStateFromMemento(Memento memento) {
state = memento.getState();
}
}
对于备忘录对象,我们利用它来存储对象的某些状态。
public class Memento {
private String state;
//构造器
public Memento(String state) {
super();
this.state = state;
}
public String getState() {
return state;
}
}
Caretaker 管理者的编写,主要是要添加相关的类,进行存储备忘录对象。
public class Client {
public static void main(String[] args) {
Originator originator = new Originator();
Caretaker caretaker = new Caretaker();
originator.setState(" 状态#1 攻击力 100 ");
//保存了当前的状态
caretaker.add(originator.saveStateMemento());
originator.setState(" 状态#2 攻击力 80 ");
caretaker.add(originator.saveStateMemento());
originator.setState(" 状态#3 攻击力 50 ");
caretaker.add(originator.saveStateMemento());
System.out.println("当前的状态是 =" + originator.getState());
//希望得到状态 1, 将 originator 恢复到状态1
originator.getStateFromMemento(caretaker.get(0));
System.out.println("恢复到状态1 , 当前的状态是");
System.out.println("当前的状态是 =" + originator.getState());
}
}
使用场景
需要记录一个对象的内部状态时,为了允许用户取消不确定或者错误的操作,能够恢复到原先的状态。
具体而言,如:需要保存和恢复数据的相关场景
- 提供一个可回滚的操作,如ctrl+z、浏览器回退按钮、Backspace键等
-
优点
给用户提供了一种可以恢复状态的机制,可以使用能够比较方便地回到某个历史的状态。
-
注意
不要在频繁建立备份的场景中使用备忘录模式。为了节约内存,可使用原型模式+备忘录模式。
-
19. 解释器模式
我们设计一个只含加减的计算器,如计算a+b-c的值,具体要求就是先输入表达式,在输入表达式中字符对应的值,使用解释器模式。
解释器模式:是指给定一个语言(表达式),定义他的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子(表达式)。模式结构
Context,是环境角色,含有解释器之外的全局信息。
AbstractException,是抽象表达式,它是声明一个抽象的解释操作,这个方法为抽象语法树中所有的节点(相当于子类)所共享。
TerminalExpression,终结符表达式,实现与文法中的终结符相关的解释操作,具体而言,根据具体的逻辑实现interpret方法。
NonTerminalExpression,为非终结表达式,为文法中的非终结符实现解释操作。
说明:输入Context和TerminalExpression信息通过Client进行输入。实例
输入表达式a+b-c,使用解释器模式计算出数值,此处的客户端我们就省略,与编译原理知识相关。
抽象表达式类,通过Map键值对,使键对应公式参数,如a、b、c等,值为运算时取得的具体数值。public abstract class Expression {
//解析公式和数值,key是公式中的参数,value是具体的数值
public abstract int interpreter(HashMap<String, Integer> var);
}
对于变量解析器和抽象符号解析器,以及及其子类(省略SubExpression的展示)
public class VarExpression extends Expression {
private String key;
public VarExpression(String key) {
this.key = key;
}
@Override
public int interpreter(HashMap<String, Integer> var) {
return var.get(this.key);
}
}
public class SymbolExpression extends Expression {
protected Expression left;
protected Expression right;
public SymbolExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpreter(HashMap<String, Integer> var) {
return 0;
}
}
public class SubExpression extends SymbolExpression {
public SubExpression(Expression left, Expression right) {
super(left, right);
}
public int interpreter(HashMap<String, Integer> var) {
return super.left.interpreter(var) - super.right.interpreter(var);
}
}
其中对于运算的计算类,我们直接展示代码,有兴趣可以看下编译原理。
public class Calculator {
3 //定义表达式
4 private Expression expression;
6 //构造函数传参,并解析
7 public Calculator(String expStr) {
8 //安排运算先后顺序
9 Stack<Expression> stack = new Stack<>();
10 //表达式拆分为字符数组
11 char[] charArray = expStr.toCharArray();
13 Expression left = null;
14 Expression right = null;
15 for(int i=0; i<charArray.length; i++) {
16 switch (charArray[i]) {
17 case '+': //加法
18 left = stack.pop();
19 right = new VarExpression(String.valueOf(charArray[++i]));
20 stack.push(new AddExpression(left, right));
21 break;
22 case '-': //减法
23 left = stack.pop();
24 right = new VarExpression(String.valueOf(charArray[++i]));
25 stack.push(new SubExpression(left, right));
26 break;
27 default: //公式中的变量
28 stack.push(new VarExpression(String.valueOf(charArray[i])));
29 break;
30 }
31 }
32 this.expression = stack.pop();
33 }
35 //计算
36 public int run(HashMap<String, Integer> var) {
37 return this.expression.interpreter(var);
38 }
40 }
应用场景
应用可以将一个需要解释执行的语句中的句子表达为一个抽象语法树
- 一些重复出现的问题可以用一种简单语言来表达
- 一个简单语法需要解释的场景
优点
可扩展性好,当有一个语言需要解释执行,可将语言中的句子表示为一个抽象语法树,就可以考虑解释器模式,它的可扩展性好。
缺点
- 解释器模式会引起类膨胀
- 解释器模式采用递归调用方法,将会导致调试非常复杂,一般解决的是复杂问题。
- 使用了大量的循环和递归,效率是一个不容忽视的问题
20. 状态模式
状态模式,它主要是用来解决对象在多种状态转换时,需要对外输出不同的行为问题,状态和行为是一一对应的,状态之间可以相互转换。当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是改变了其类。
模式结构
Context类为环境角色,用于维护State实例,这个实例定义当前状态。
State是个抽象的状态角色,定义一个接口封装与Context的一个特点接口相关。
ConcreteState具体的状态角色,每个子类实现一个与Context的一个状态相关行为。
实例
实例的背景如上图,即抽奖活动的状态表示,其抽象出来的对应的UML图如下,该代码只是展示部分类,具体的代码可见链接。
状态抽象类,对于上图中的四个类都需要继承该类,用于状态的表示。
public abstract class State {
// 扣除积分 - 50
public abstract void deductMoney();
// 是否抽中奖品
public abstract boolean raffle();
// 发放奖品
public abstract void dispensePrize();
}
四个实现类的编写,其中注意的是,对于Activity类的注入。
不能抽奖状态:NoRaffleState
public class NoRaffleState extends State {
// 初始化时传入活动引用,扣除积分后改变其状态
RaffleActivity activity;
public NoRaffleState(RaffleActivity activity) {
this.activity = activity;
}
// 当前状态可以扣积分 , 扣除后,将状态设置成可以抽奖状态
@Override
public void deductMoney() {
System.out.println("扣除50积分成功,您可以抽奖了");
activity.setState(activity.getCanRaffleState());
}
// 当前状态不能抽奖
@Override
public boolean raffle() {
System.out.println("扣了积分才能抽奖喔!");
return false;
}
// 当前状态不能发奖品
@Override
public void dispensePrize() {
System.out.println("不能发放奖品");
}
}
对于Activity要组合四种状态。
//抽奖活动
public class RaffleActivity {
// state 表示活动当前的状态,是变化
State state = null;
// 奖品数量
int count = 0;
// 四个属性,表示四种状态
State noRafflleState = new NoRaffleState(this);
State canRaffleState = new CanRaffleState(this);
State dispenseState = new DispenseState(this);
State dispensOutState = new DispenseOutState(this);
//构造器
//1. 初始化当前的状态为 noRafflleState(即不能抽奖的状态)
//2. 初始化奖品的数量
public RaffleActivity( int count) {
this.state = getNoRafflleState();
this.count = count;
}
//扣分, 调用当前状态的 deductMoney
public void debuctMoney(){
state.deductMoney();
}
//抽奖
public void raffle(){
// 如果当前的状态是抽奖成功
if(state.raffle()){
//领取奖品
state.dispensePrize();
}
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
//这里请大家注意,每领取一次奖品,count--
public int getCount() {
int curCount = count;
count--;
return curCount;
}
//省略get、set方法
}
测试方法如下.
public static void main(String[] args) {
// 创建活动对象,奖品有1个奖品
RaffleActivity activity = new RaffleActivity(1);
// 我们连续抽300次奖
for (int i = 0; i < 30; i++) {
System.out.println("--------第" + (i + 1) + "次抽奖----------");
// 参加抽奖,第一步点击扣除积分
activity.debuctMoney();
// 第二步抽奖
activity.raffle();
}
}
优点
- 代码有很强的可读性。状态模式将每个状态的行为封装到对应的一个类中
- 方便维护。将容易产生问题的if-else语句删除了,如果把每个状态的行为都放到一个类中,每次调用方法时都要判断当前是什么状态,不但会产出很多if-else语句,而且容易出错,
-
缺点
会产生很多类。每个状态都要- 一个对应的类,当状态过多时会产生很多类,加大维护难度。
使用场景
当一个事件或者对象有很多种状态,状态之间会相互转换,对不同的状态要求有不同的行为的时候,可以考虑使用状态模式。
21. 策略模式
编写鸭子项目:有各种鸭子(比如野鸭、北京鸭,鸭子有各种行为,比如叫、飞行),显示鸭子的信息。
传统的方法,我们编写一个抽象类Duck,这个类里面有quack、swim、fly等方法,分别表示叫、游泳、飞等行为,最后编写display汇总相关消息。然后编写相关类继承Duck抽象类。具体而言,WildDuck所有的动作都可以做,PekingDuck不会飞,我们重写fly方法覆盖就好,对于ToyDuck,以上三个行为都不会,都需要重写覆盖。
由此,可见继承带来的问题,对类的局部改动,会影响其他类,尤其是超类,会有溢出效应。我们对于PekingDuck采用重写覆盖解决,但是对于ToyDuck则不推荐,这是可以只用策略模式。
策略模式中,定义算法族,分别封装起来,让他们可以相互替换,此模式让算法的变化独立于使用算法的客户。模式结构
从图中看到,客户Context拥有成员变量Strategy或者其他的策略接口,至于需要使用到那个策略,我们可以在构造器中指定。实例
类图如下。
对于该实例,我们只是象征性的实现与Fly有关的,其他的类比进行编码。
对于FlyBehavior接口,我们设计会飞行和不会飞行两个实现类,表示不同的算法实现,代码如下。public interface FlyBehavior {
void fly(); // 子类具体实现
}
public class GoodFlyBehavior implements FlyBehavior {
@Override
public void fly() {
System.out.println(" 飞行技术高超 ~~~");
}
}
public class NoFlyBehavior implements FlyBehavior{
public void fly() {
System.out.println(" 不会飞行啊");
}
}
对于Duck的抽象类,我们定义了属性,表示使用的算法,并且提供了Set方法,可以在运行时动态的改变鸭子的飞行行为。
public abstract class Duck {
//属性, 策略接口
FlyBehavior flyBehavior;
public Duck() { }
public void fly() {
//改进
if(flyBehavior != null) {
flyBehavior.fly();
}
}
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
}
在Duck的实现子类中,我们通过构造函数赋予子类的属性。
public class WildDuck extends Duck {
//构造器,传入FlyBehavor 的对象
public WildDuck() {
flyBehavior = new GoodFlyBehavior();
}
}
对于测试方法,我们可以动态的改变他的属性,附上运行结果。
public static void main(String[] args) {
// TODO Auto-generated method stub
WildDuck wildDuck = new WildDuck();
wildDuck.fly();
//动态改变算法
wildDuck.setFlyBehavior(new NoFlyBehavior());
wildDuck.fly();
}
//运行结果
飞行技术高超 ~~~
不会飞行啊
优点
体现的原则:把变化的代码从不变的代码中分离出来;针对接口编程而不是具体的类(定义了策略接口);多用组合/聚合,少用继承(客户端通过组合方式使用策略)以及开闭原则,客户端增加行为不需要对原有代码。
提供了可以替换继承关系的方法,策略模式将算法封装在独立的Strategy类中,使得可以独立于其Context改变它,使得它易于切换、理解和扩展。
缺点
容易形成类爆炸,每增加一个策略就要增加一个类,当策略模式过多会导致类的数目庞大。
22. 职责链模式
OA系统采购审批任务,需求是采购员采购教学器材,如果金额小于5000元,由教学主任审批;如果小于等于10000元,由院长审批;如果大于10000元,有校长审批。
职责链模式又叫责任链模式,为请求创建一个接收者对象的链,这种模式对请求的发送和接收者进行解耦,即将多个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
职责链模式通常每个接收者都包含对另一个接收者的引用,如果一个对象不能处理该请求,那么他会把相同的请求床给下一个接收者,以此类推。模式结构
Handler,抽象的处理者,定义了一个处理请求的接口,同时包含另外一个Handler。
ConcreteHandlerA,B是具体的处理者,处理它自己负责的请求,可以访问他的后继者(即下一个处理者),如果可以处理该请求则处理,否则就将这个请求交给后继者去处理,从而形成一个职责链。
Request含有一个属性,表示一个请求。实例
实例我们实现背景介绍中的例子,OV系统审核,对应的UML图如下。
首先,我们要实现请求的封装类PurchaseRequest//请求类
public class PurchaseRequest {
private int type = 0; //请求类型
private float price = 0.0f; //请求金额
private int id = 0;
//省略get/set方法
}
然后就是处理抽象类Approver,以及继承它的类,如系主任类,院长类和校长类。
public abstract class Approver {
Approver approver; //下一个处理者
String name; // 名字
public Approver(String name) {
this.name = name;
}
//下一个处理者
public void setApprover(Approver approver) {
this.approver = approver;
}
//处理审批请求的方法,得到一个请求, 处理是子类完成,因此该方法做成抽象
public abstract void processRequest(PurchaseRequest purchaseRequest);
}
//系主任类
public class DepartmentApprover extends Approver {
public DepartmentApprover(String name) {
super(name);
}
@Override
public void processRequest(PurchaseRequest purchaseRequest) {
if(purchaseRequest.getPrice() <= 5000) {
System.out.println(" 请求编号 id= " + purchaseRequest.getId() + " 被 " + this.name + " 处理");
}else {
approver.processRequest(purchaseRequest);
}
}
}
//院长类
public class CollegeApprover extends Approver {
public CollegeApprover(String name) {
super(name);
}
@Override
public void processRequest(PurchaseRequest purchaseRequest) {
if(purchaseRequest.getPrice() < 5000 && purchaseRequest.getPrice() <= 10000) {
System.out.println(" 请求编号 id= " + purchaseRequest.getId() + " 被 " + this.name + " 处理");
}else {
approver.processRequest(purchaseRequest);
}
}
}
//校长类
public class SchoolMasterApprover extends Approver {
public SchoolMasterApprover(String name) {
super(name);
}
@Override
public void processRequest(PurchaseRequest purchaseRequest) {
if(purchaseRequest.getPrice() > 10000) {
System.out.println(" 请求编号 id= " + purchaseRequest.getId() + " 被 " + this.name + " 处理");
}else {
approver.processRequest(purchaseRequest);
}
}
}
最后就是测试程序的编写,其中主要的请求审批对象、以及系院校长的创建,责任链的构建。
public static void main(String[] args) {
//创建一个请求
PurchaseRequest purchaseRequest = new PurchaseRequest(1, 31000, 1);
//创建相关的审批人
DepartmentApprover departmentApprover = new DepartmentApprover("系主任");
CollegeApprover collegeApprover = new CollegeApprover("院长");
SchoolMasterApprover schoolMasterApprover = new SchoolMasterApprover("校长");
//需要将各个审批级别的下一个设置好
departmentApprover.setApprover(collegeApprover);
collegeApprover.setApprover(schoolMasterApprover);
//处理请求
departmentApprover.processRequest(purchaseRequest);
}
优点
将请求和处理分隔开,实现解耦,提高了系统的灵活性。
-
缺点
性能会受到影响,特别是在链比较长的时候,因此需要控制链中的最大节点数目,避免超长链无意识的破坏系统性能。
-
应用场景
有多个对象可以处理同意请求时,比如:多级请求、请假/加薪等审批流程,JAVA Web中Tomcat对Encoding的处理器、拦截器。
23.访问者模式
以下的主要内容来自简书文章《访问者模式一篇就够》。
年底,CEO和CTO开始评定员工一年的工作绩效,员工分为工程师和经理,CTO关注工程师的代码量、经理的新产品数量;CEO关注的是工程师的KPI和经理的KPI以及新产品数量。由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。
访问者模式,封装一些作用于某些数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用域这些元素的新操作。该模式主要是将数据结构与数据操作相分离,解决数据结构和操作耦合性问题。
原理:在被访问的类里面加一个对外提供接待访问者的接口。模式结构
Visitor:接口或者抽象类,定义了对每个 Element 访问的行为,它的参数就是被访问的元素,它的方法个数理论上与元素的个数是一样的,因此,访问者模式要求元素的类型要稳定,如果经常添加、移除元素类,必然会导致频繁地修改 Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式。
ConcreteVisitor:具体的访问者,它需要给出对每一个元素类访问时所产生的具体行为。
Element:元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问。
ElementA、ElementB:具体的元素类,它提供接受访问的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素提供访问者访问。实例
通过使用访问者模式,解决背景问题,工作绩效评定。
// 员工基类
public abstract class Staff {
public String name;
public int kpi;// 员工KPI
public Staff(String name) {
this.name = name;
kpi = new Random().nextInt(10);
}
// 核心方法,接受Visitor的访问
public abstract void accept(Visitor visitor);
}
Staff 类定义了员工基本信息及一个 accept 方法,accept 方法表示接受访问者的访问,由子类具体实现。Visitor 是个接口,传入不同的实现类,可访问不同的数据。下面看看工程师和经理的代码:
//工程师
public class Engineer extends Staff {
public Engineer(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
// 工程师一年的代码数量
public int getCodeLines() {
return new Random().nextInt(10 * 10000);
}
}
// 经理
public class Manager extends Staff {
public Manager(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
// 一年做的产品数量
public int getProducts() {
return new Random().nextInt(10);
}
}
工程师是代码数量,经理是产品数量,他们的职责不一样,也就是因为差异性,才使得访问模式能够发挥它的作用。Staff、Engineer、Manager 3个类型就是对象结构,这些类型相对稳定,不会发生变化。
然后将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的 showReport 方法查看所有员工的业绩,具体代码如下:// 员工业务报表类
public class BusinessReport {
private List<Staff> mStaffs = new LinkedList<>();
public BusinessReport() {
mStaffs.add(new Manager("经理-A"));
mStaffs.add(new Engineer("工程师-A"));
mStaffs.add(new Engineer("工程师-B"));
mStaffs.add(new Engineer("工程师-C"));
mStaffs.add(new Manager("经理-B"));
mStaffs.add(new Engineer("工程师-D"));
}
/**
* 为访问者展示报表
* @param visitor 公司高层,如CEO、CTO
*/
public void showReport(Visitor visitor) {
for (Staff staff : mStaffs) {
staff.accept(visitor);
}
}
}
下面看看 Visitor 类型的定义, Visitor 声明了两个 visit 方法,分别是对工程师和经理对访问函数,具体代码如下:
public interface Visitor {
// 访问工程师类型
void visit(Engineer engineer);
// 访问经理类型
void visit(Manager manager);
}
首先定义了一个 Visitor 接口,该接口有两个 visit 函数,参数分别是 Engineer、Manager,也就是说对于 Engineer、Manager 的访问会调用两个不同的方法,以此达成区别对待、差异化处理。具体实现类为 CEOVisitor、CTOVisitor类,具体代码如下:
// CEO访问者
public class CEOVisitor implements Visitor {
@Override
public void visit(Engineer engineer) {
System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
}
@Override
public void visit(Manager manager) {
System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
", 新产品数量: " + manager.getProducts());
}
}
//CTO访问者
public class CTOVisitor implements Visitor {
@Override
public void visit(Engineer engineer) {
System.out.println("工程师: " + engineer.name + ", 代码行数: " + engineer.getCodeLines());
}
@Override
public void visit(Manager manager) {
System.out.println("经理: " + manager.name + ", 产品数量: " + manager.getProducts());
}
}
在CEO的访问者中,CEO关注工程师的 KPI,经理的 KPI 和新产品数量,通过两个 visitor 方法分别进行处理。如果不使用 Visitor 模式,只通过一个 visit 方法进行处理,那么就需要在这个 visit 方法中进行判断,然后分别处理,代码大致如下:
public class ReportUtil {
public void visit(Staff staff) {
if (staff instanceof Manager) {
Manager manager = (Manager) staff;
System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
", 新产品数量: " + manager.getProducts());
} else if (staff instanceof Engineer) {
Engineer engineer = (Engineer) staff;
System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
}
}
}
这就导致了 if-else 逻辑的嵌套以及类型的强制转换,难以扩展和维护,当类型较多时,这个 ReportUtil 就会很复杂。所以拒绝该方法而选择重载的 visit 方法会对元素进行不同的操作,而通过注入不同的 Visitor 又可以替换掉访问者的具体实现,使得对元素的操作变得更灵活,可扩展性更高,同时也消除了类型转换、if-else 等“丑陋”的代码。
下面是客户端代码:public class Client {
public static void main(String[] args) {
// 构建报表
BusinessReport report = new BusinessReport();
System.out.println("=========== CEO看报表 ===========");
report.showReport(new CEOVisitor());
System.out.println("=========== CTO看报表 ===========");
report.showReport(new CTOVisitor());
}
}
运行结果如下。
=========== CEO看报表 ===========
经理: 经理-A, KPI: 9, 新产品数量: 0
工程师: 工程师-A, KPI: 6
工程师: 工程师-B, KPI: 6
工程师: 工程师-C, KPI: 8
经理: 经理-B, KPI: 2, 新产品数量: 6
工程师: 工程师-D, KPI: 6
=========== CTO看报表 ===========
经理: 经理-A, 产品数量: 3
工程师: 工程师-A, 代码行数: 62558
工程师: 工程师-B, 代码行数: 92965
工程师: 工程师-C, 代码行数: 58839
经理: 经理-B, 产品数量: 6
工程师: 工程师-D, 代码行数: 53125
在上述示例中,Staff 扮演了 Element 角色,而 Engineer 和 Manager 都是 ConcreteElement;CEOVisitor 和 CTOVisitor 都是具体的 Visitor 对象;而 BusinessReport 就是 ObjectStructure;Client就是客户端代码。
访问者模式最大的优点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个访问者,只要新实现一个 Visitor 接口的类,从而达到数据对象与数据操作相分离的效果。如果不实用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用 if-else 和类型转换,这使得代码难以升级维护。优点
各角色职责分离,符合单一职责原则。通过UML类图和上面的示例可以看出来,Visitor、ConcreteVisitor、Element 、ObjectStructure,职责单一,各司其责。
- 具有优秀的扩展性。如果需要增加新的访问者,增加实现类 ConcreteVisitor 就可以快速扩展。
使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化。 员工属性(数据结构)和CEO、CTO访问者(数据操作)的解耦,以及灵活性。
缺点
具体元素对访问者公布细节,违反了迪米特原则。CEO、CTO需要调用具体员工的方法。
- 具体元素变更时导致修改成本大,变更员工属性时,多个访问者都要修改。
违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没有依赖抽象,访问者 visit 方法中,依赖了具体员工的具体方法。
应用场景
对象结构比较稳定,但经常需要在此对象结构上定义新的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。