一、什么是设计模式
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
使用设计模式是为了 提高代码可复用性、可维护性、可读性、稳健性以及安全性的 。
毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。
项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。
二、设计模式的分类
总体来说设计模式分为三大类
1)创建型模式:
单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式。
2)结构型模式:
适配器模式、桥接模式、装饰器模式、组合模式、外观模式、享元模式、代理模式。
3)行为型模式:
模板方法模式、命令模式、迭代子模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、责任链模式、访问者模式。
二、设计模式的六大原则
1)开闭原则(Open Close Principle)
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。
2)里氏代换原则(Liskov Substitution Principle)
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
3)依赖倒转原则(Dependence Inversion Principle)
这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。
4)接口隔离原则(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。
5)迪米特法则(最少知道原则)(Demeter Principle)
为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。
6)合成复用原则(Composite Reuse Principle)
原则是尽量使用合成/聚合的方式,而不是使用继承。
三、常用的设计模式
1)工厂方法模式(Factory Method)
工厂方法模式分为三种:
1.1 普通工厂模式
就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。首先看下关系图:
举例如下:(我们举一个发送邮件和短信的例子)
① 首先,创建二者的共同接口:
public interface Sender { public void send(); }
② 其次,创建实现类:
public class SmsSender implements Sender { public void send() { System.out.println("this is sender!"); } }
public class MailSender implements Sender { public void send() { System.out.println("this is mailSender!"); } }
③ 最后,建工厂类:
public class SendFactory { public Sender produce(String type) { if ("mail".equals(type)) { return new MailSender(); } else if ("sms".equals(type)) { return new SmsSender(); } else { System.out.println("请输入正确的类型!"); return null; } } }
④ 我们来测试下:
public class FactoryTest { public static void main(String[] args) { // 创建工厂类 SendFactory sendFactory = new SendFactory(); // 使用工厂创建mailSender Sender mail = sendFactory.produce("mail"); mail.send(); System.out.println("================================="); // 使用工厂创建smsSender Sender sms = sendFactory.produce("sms"); sms.send(); } }
输出:
this is mailSender!
=================================
this is sender!
1.2 多个工厂方法模式
是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。关系图:
将上面的代码做下修改,改动下SendFactory类就行,如下:
public class SendFactory {
public Sender produceMail() { return new MailSender(); }
public Sender produceSms() { return new SmsSender(); } }
测试类如下:
public class FactoryTest { public static void main(String[] args) { SendFactory sendFactory = new SendFactory(); Sender mail = sendFactory.produceMail(); mail.send(); System.out.println("================================="); Sender sms = sendFactory.produceSms(); sms.send(); } }
输出:
this is mailSender!
=================================
this is sender!
1.3 静态工厂方法模式★
将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
public class SendFactory { public static Sender produceMail() { return new MailSender(); } public static Sender produceSms() { return new SmsSender(); } }
测试类:
public class FactoryTest { public static void main(String[] args) { Sender mail = SendFactory.produceMail(); mail.send(); System.out.println("================================="); Sender sms = SendFactory.produceSms(); sms.send(); } }
输出:
this is mailSender!
=================================
this is sender!
总体来说,工厂模式适合:凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。
在以上的三种模式中,第一种如果传入的字符串有误,不能正确创建对象,第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。
2)抽象工厂模式(Abstract Factory)
工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。因为抽象工厂不太好理解,我们先看看图,然后就和代码,就比较容易理解。
① 创建二者的共同接口:
public interface Sender { public void Send(); }
② 两个实现类:
public class MailSender implements Sender { @Override public void Send() { System.out.println("this is mailsender!"); } }
public class SmsSender implements Sender { @Override public void Send() { System.out.println("this is sms sender!"); } }
③ 提供一个超级工厂的接口:
public interface Provider { public Sender produce(); }
④ 两个超级工厂实现类:
public class SendMailFactory implements Provider{ @Override public Sender produce() { return new MailSender(); } }
public class SendSmsFactory implements Provider { @Override public Sender produce() { return new SmsSender(); } }
⑤ 测试类:
public class AbstractTest { public static void main(String[] args) { Provider sendMailFactory = new SendMailFactory(); Sender sender1 = sendMailFactory.produce(); sender1.Send(); System.out.println("========================================="); SendSmsFactory sendSmsFactory = new SendSmsFactory(); Sender sender2 = sendSmsFactory.produce(); sender2.Send(); } }
输出:
this is mailSender!
=================================
this is sender!
其实这个模式的好处就是,如果你现在想增加一个功能:发及时信息,则只需做一个实现类,实现Sender接口,同时做一个工厂类,实现Provider接口,就OK了,无需去改动现成的代码。这样做,拓展性较好!
3)单例模式(Singleton)
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
3.1 注意
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
3.2 多种单例的特性
单例模式 | 是否推荐 | 懒加载 | 反序列化单例 | 反射单例 | 克隆单例 | 性能、失效问题 |
饿汉式 | Eager加载推荐 | x | x | x | x | 类加载时就初始化,浪费内存。 |
懒汉式(同步方法) | x | √ | x | x |
x |
存在性能问题,每次获取示例都会进行同步 |
双重检测锁(DCL) | 可用 | √ | x | x | x |
JDK < 1.5 失效 |
静态内部类 | 推荐 | √ | x | x | x | |
枚举 | 最推荐 | x | √ | √ | √ |
JDK < 1.5 不支持 自动支持序列化机制,绝对防止多次实例化 |
3.3 单例模式的几种实现方式
① 饿汉式:该模式在类被加载时就会实例化一个对象。
该模式能简单快速的创建一个单例对象,而且是线程安全的(只在类加载时才会初始化,以后都不会)。但它有一个缺点,就是不管你要不要都会直接创建一个对象,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单)
// 饿汉式单例 public class Hungry { // 在类装载时就实例化 private static Hungry HUNGRY = new Hungry(); // 私有化构造方法 private Hungry() { } // 提供方法让外部获取实例 public static Hungry getInstance() { return HUNGRY; } }
这种做法很方便的帮我们解决了多线程实例化的问题,但是缺点也很明显。
因为这句代码 private static Hungry HUNGRY = new Hungry(); 的关系,所以该类一旦被jvm加载就会马上实例化!
那如果我们不想用这个类怎么办呢? 是不是就浪费了呢?既然这样,我们来看下替代方案! 懒汉式。
② 懒汉式:该模式只在你需要对象时才会生成单例对象(比如调用getInstance方法)
这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
public class LazyMan { private static LazyMan LAZY_MAN; private LazyMan() { } public synchronized static LazyMan getInstance() { if (LAZY_MAN== null) return new LazyMan(); return LAZY_MAN; } }
从线程安全性上讲,不加同步的懒汉式是线程不安全的,比如说:有两个线程,一个是线程A,一个是线程B,它们同时调用getInstance方法,那就可能导致并发问题。
所以只要加上 Synchronized 即可,但是这样一来,会降低整个访问的速度,而且每次都要判断,也确实是稍微慢点。
那么有没有更好的方式来实现呢?双重检查加锁,可以使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受到大的影响。
③ 双检锁/双重校验锁(DCL,即 double-checked locking):这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
双重检查加锁机制 并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
public class DoubleCheck { private volatile static DoubleCheck DOUBLE_CHECK; private DoubleCheck() { } public static DoubleCheck getInstance() { // 先检查实例是否存在,如果不存在才进入下面同步块 if (DOUBLE_CHECK == null) { // 同步块,线程安全的创建实例 synchronized (DoubleCheck.class) { // 再次检查实例是否存在,如果不存在则创建实例 if (DOUBLE_CHECK == null) return new DoubleCheck(); } } return DOUBLE_CHECK; } }
volatile关键字:将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
注意:在Java1.4及以前版本中,很多JVM对于volatile关键字的实现有问题,会导致双重检查加锁的失败,因此双重检查加锁的机制只能用在Java5及以上的版本。
这种实现方式既可使实现线程安全的创建实例,又不会对性能造成太大的影响,它 只在第一次创建实例的时候同步,以后就不需要同步了,从而加快运行速度。
但 由于 volatile 关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高 ,因此一般建议,没有特别的需要,不要使用。
也就是说,虽然可以使用双重加锁机制来实现线程安全的单例,但并不建议大量采用,根据情况来选用吧。
④ 静态内部类:采用类级内部类,在这个类级内部类里面去创建对象实例,只要不使用到这个类级内部类,那就不会创建对象实例
这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会被虚拟机在装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
public class Holder { // 私有构造方法 private Holder() { } /** * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 * 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载 */ private static class InnerClass { // 静态初始化器,由JVM来保证线程安全 private static final Holder HOLDER = new Holder(); } public static final Holder getInstance() { return Holder.getInstance(); } }
这个模式的优势在于,getInstance 方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
⑤ 枚举:枚举实现单例是最为推荐的一种方法,因为它更简洁并且就算通过序列化,反射等也没办法破坏单例性
- Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法;
- Java枚举类型的基本思想:通过公有的静态final域为每个枚举常量导出实例的类;
- 从某个角度讲,枚举是单例的泛型化,本质上是单元素的枚举;
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
public enum EnumSingle { INSTANCE; public EnumSingle getInstance() { return INSTANCE; } }
使用枚举来实现单实例控制,会更加简洁,而且无偿的提供了序列化的机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。
4)建造者模式
建造者模式属于创建型设计模式,用来组装复杂的实例。
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。
4.1 使用场景
- 需要生成的对象具有复杂的内部结构。
- 需要生成的对象内部属性本身相互依赖。
4.2 优缺点
优点:
- 建造者独立,易扩展。
- 便于控制细节风险。
缺点:
- 产品必须有共同点,范围有限制。
- 如内部变化复杂,会有很多的建造类。
4.3 角色
① 抽象建造者 - Builder:目的是为了将建造的具体过程交给它的子类来实现。这样更容易扩展。一般至少会有两个抽象方法,一个用来建造产品,一个是用来返回产品
② 具体的建造者 - ConcreteBuilder:实现抽象建造者的所有未实现的方法,具体来说一般是两项任务:组建产品;返回组建好的产品
③ 产品类 - Product:一般是一个较为复杂的对象
④ 管理类 - Director:负责调用适当的建造者来组建产品,被用来封装程序中易变的部分
4.4 实例:
以电脑为例,电脑拥有 cpu,内存,硬盘,涉及的类:Computer(产品类),Builder(抽象建造者),ConcreteBuilder(具体的建造者),Director(管理类)
我要组装一台电脑,电脑被抽象为 Computer 类,它有三个部件:CPU 、内存和磁盘。
并在里面提供了三个方法分别用来设置CPU 、内存和磁盘:
public class Computer { private String cpu; // cpu private String memory; // 内存 private String disk; // 磁盘
// get and set and toString ...
}
② Builder(抽象建造者):
商家组装电脑有一套组装方法的模版,就是一个抽象的 Builder 类,里面提供了安装CPU、内存和磁盘的方法,以及组装成电脑的 create 方法:
public interface Builder { /** * 组装cpu */ public abstract void builderCpu(String cpu); /** * 组装内存 */ public abstract void builderMemory(String memory); /** * 组装硬盘 */ public abstract void builderDisk(String disk); /** * 获取电脑 * @return */ public abstract Computer getComputer(); }
③ ConcreteBuilder(具体的建造者):
商家实现了抽象的 Builder 类,ConcreteBuilder 类用于组装电脑
public class ConcreteBuilder implements Builder { private Computer computer = new Computer(); @Override public void builderCpu(String cpu) { computer.setCpu(cpu); } @Override public void builderMemory(String memory) { computer.setMemory(memory); } @Override public void builderDisk(String disk) { computer.setDisk(disk); } @Override public Computer getComputer() { return computer; } }
④ Director(管理类):
商家的指挥者类用来规范组装电脑的流程规范,先安装磁盘,再安装CPU,最后安装内存并组装成电脑:
public class Director { private Builder builder = new ConcreteBuilder(); /** * 组装电脑 */ public Computer builderComputer(String cpu, String memory, String disk) { builder.builderCpu(cpu); builder.builderMemory(memory); builder.builderDisk(disk); return builder.getComputer(); } }
⑤ 测试:
最后商家用指挥者类组装电脑。我们只需要提供我们想要的CPU,磁盘和内存就可以了,至于商家怎样组装的电脑我们无需知道。
public class CreatComputer { public static void main(String[] args) { Director director = new Director(); Computer computer = director.builderComputer("Intel cpu", "内存", "硬盘"); System.out.println(computer); } }
输出:
Computer{cpu='Intel cpu', memory='内存', disk='硬盘'}
建造者模式与工程方法模式的不同在于建造者模式关注的是零件类型和装配工艺(顺序)
5)原型模式
原型模式虽然是创建型的模式,但是与工程模式没有关系,从名字即可看出该模式的思想就是 将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象 。
本小结会通过对象的复制,进行讲解。在 Java 中,复制对象是通过 clone() 实现的,先创建一个原型类:
public class Prototype implements Cloneable { public Object clone() throws CloneNotSupportedException { Prototype proto = (Prototype) super.clone(); return proto; } }
很简单,一个原型类,只需要实现 Cloneable 接口,覆写 clone 方法,此处 clone 方法可以改成任意的名称,因为 Cloneable 接口是个空接口,你可以任意定义实现类的方法名,如 cloneA 或者 cloneB ,因为此处的重点是 super.clone() 这句话,super.clone() 调用的是 Object 的 clone() 方法。在这儿,我将结合对象的浅复制和深复制来说一下,首先需要了解对象深、浅复制的概念:
● 浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。
● 深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。
深浅复制的例子:
public class Prototype implements Cloneable, Serializable { private static final long serialVersionUID = 1L; private String string; private SerializableObject obj; /* 浅复制 */ public Object clone() throws CloneNotSupportedException { Prototype proto = (Prototype) super.clone(); return proto; } /* 深复制 */ public Object deepClone() throws IOException, ClassNotFoundException { /* 写入当前对象的二进制流 */ ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); /* 读出二进制流产生的新对象 */ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } public String getString() { return string; } public void setString(String string) { this.string = string; } public SerializableObject getObj() { return obj; } public void setObj(SerializableObject obj) { this.obj = obj; } } class SerializableObject implements Serializable { private static final long serialVersionUID = 1L; }
要实现深复制,需要采用流的形式读入当前对象的二进制输入,再写出二进制数据对应的对象。
6)适配器模式
将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
使用场景:有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。
注意:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。
6.1 适配器分类
根据适配器与适配者类的关系不同,可以分为对象适配器模式以及类适配器模式。
- 对象适配器模式:适配器与适配者之间是关联关系。
- 类适配器模式:适配器与适配者之间是继承或实现关系。
在Java等语言中大部分情况下使用对象适配器模式。
6.2 角色
Target(目标抽象类):
目标抽象类定义客户所需的接口,可以是一个抽象类或接口,也可以是一个具体类
Adapter(适配器类):
适配器可以调用另一个接口,作为一个转换器,对 Adaptee 和 Target 进行适配。
适配器类是适配器模式的核心,在对象适配器模式中,它通过继承 Target 并关联一个 Adaptee 对象使两者产生联系,在类适配器模式,通过继承 Adaptee 并实现 Target 使两者产生联系
Adaptee(适配者类):
适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码
6.3 实例
假设目前只有一条 Micro USB 线以及一台只有 Type-C 接口的手机,需要对其进行充电,这时候就需要一个转接头把 Micro USB 转为 Type-C 接口,才能给手机充电,使用适配器模式对其进行设计。
① 目标抽象类:TypeC
// Target:给 TypeC 接口的手机充电 public interface TypeC { public void chargeWithTypeC(); }
② 适配者类:MicroUSB
// Adaptee:适配者,MicroUSB 线 public class MicroUSB { public void chargeWithMicroUSB() { System.out.println("MicroUSB充电"); } }
③ 适配器:MicroUSBToTypeC(适配器继承或依赖已有的对象,实现想要的目标接口)
- 对象适配器(依赖):对象适配器种适配器与适配者是关联关系,适配器中包含一个适配者成员。
// Adapter:适配器,MicroUSB 到 TypeC 的转接头
// 适配器实现目标接口类的方法,并将请求转发,交由适配者完成。 class MicroUSBToTypeC implements TypeC{
private MicroUSB microUSB = new MicroUSB();
@Override public void chargeWithTypeC() { microUSB.chargeWithMicroUSB(); } }
- 类适配器(继承):类适配器中适配器与适配者是继承关系,其中适配者为父类,适配器为子类。但是在 Java 中由于不支持多重继承,因此想要在 Java 中实现类适配器模式,并且如果适配者是具体类的话,那么必须将目标抽象类指定为接口
// Adapter:适配器,MicroUSB 到 TypeC 的转接头
// 同时适配器继承了适配者并实现了 Target,在方法内直接调用 super.xxx,也就是适配者的方法。
public class MicroUSBToTypeC extends MicroUSB implements TypeC { @Override public void chargeWithTypeC() { super.chargeWithMicroUSB(); } }
④ 测试:Phone充电
public class Phone { public static void main(String[] args) { TypeC typeC = new MicroUSBToTypeC(); typeC.chargeWithTypeC(); } }
6.4 优缺点
优点:
- 可以让任何两个没有关联的类一起运行。
- 提高了类的复用。
- 增加了类的透明度。
- 灵活性好。
缺点:
- 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
- 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。
7)桥接模式
桥接模式就是将抽象部分与实现部分分离,使它们都可以独立的变化。对于两个独立变化的维度,使用桥接模式再适合不过了;
主要解决在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合;
7.1 优缺点
优点:
- 抽象和实现的分离。
- 优秀的扩展能力。
- 实现细节对客户透明。
缺点:
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
7.2 使用场景:
- 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
7.3 实现
我们常用的JDBC桥DriverManager一样,JDBC进行连接数据库的时候,在各个数据库之间进行切换,基本不需要动太多的代码,甚至丝毫不用动,原因就是JDBC提供统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了。
我们来看看关系图:
① 创建桥接实现接口。
public interface Sourceable { public void method(); }
② 创建实现了 Sourceable 接口的实体桥接实现类。
public class SourceSub1 implements Sourceable { @Override public void method() { System.out.println("SourceSub1 这是桥接的第 1 个实现!"); } }
public class SourceSub2 implements Sourceable { @Override public void method() { System.out.println("SourceSub2 这是桥接的第 2 个实现!"); } }
③ 定义一个桥 抽象类Bridge,持有 Sourceable 的一个实例
public abstract class Bridge { private Sourceable sourceable; public Bridge(Sourceable sourceable) { this.sourceable = sourceable; } public void method() { sourceable.method(); } }
④ 创建实现了 Bridge 接口的实体类。
public class MyBridge extends Bridge { public MyBridge(Sourceable sourceable) { super(sourceable); } @Override public void method() { super.method(); } }
⑤ 测试:使用 Bridge 和 Sourceable 类输出不同的结果
public class BridgeTest { public static void main(String[] args) { /*调用第一个对象*/ Bridge bridge1 = new MyBridge(new SourceSub1()); bridge1.method(); /*调用第二个对象*/ Bridge bridge2 = new MyBridge(new SourceSub2()); bridge2.method(); } }
执行程序,输出结果:
SourceSub1 这是桥接的第 1 个实现!
SourceSub2 这是桥接的第 2 个实现!
这样,就通过对 Bridge 类的调用,实现了对接口 Sourceable 的实现类 SourceSub1 和 SourceSub2 的调用。
接下来我再画个图,大家就应该明白了,因为这个图是我们 JDBC 连接的原理,有数据库学习基础的,一结合就都懂了。
7.4 总结
8)代理模式(静态)
代理模式就是给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
代理模式是一种对象结构型模式。
8.1 角色
● Subject(抽象主题角色):真实主题与代理主题的共同接口。
● RealSubject(真实主题角色):实现 Subject 抽象主题,定义真实主题所要实现的业务逻辑,供代理主题调用。
● Proxy(代理主题角色):实现 Subject 抽象主题,是真实主题的代理。通过真实主题的业务逻辑方法来实现抽象方法,并可以附加自己的操作。
8.2 应用实例
① 我们在租房子的时候会去找中介,为什么呢?因为你对该地区房屋的信息掌握的不够全面,希望找一个更熟悉的人去帮你做,而此处的代理就是这个意思。
② 我们有的时候打官司,我们需要请律师,因为律师在法律方面有专长,可以替我们进行操作,表达我们的想法。
先来看看关系图:
8.3 代码实现
① 抽象主题角色:创建一个接口。
// 房子 public interface Sourceable { // 出租方法 public void method(); }
② 真实主题角色:实现抽象主题接口,执行真正的业务操作:
// 房东:拥有房子,想向外出租 public class Source implements Sourceable { @Override public void method() { System.out.println("房东要出租房子!"); } }
③ 代理主题角色:同样实现抽象主题接口,一般来说在调用真正的业务方法之前或之后会有相关操作
// 房产中介 public class Proxy implements Sourceable { private Source source = new Source(); @Override public void method() { atfer(); source.method(); before(); } private void atfer() { System.out.println("代理前操作 - 中介寻找资源(向外出租的房子)..."); } private void before() { System.out.println("代理后操作 - 中介带客户看房、签订合同、金钱交易、交付房子..."); } }
④ 测试:
// 客户找房子 public class ProxyTest { public static void main(String[] args) { System.out.println("头几天小明自己去找房子,找不到!"); System.out.println("后来,小明new了一个中介代理找房子!"); Proxy proxy = new Proxy(); proxy.method(); System.out.println("找到一个舒适的家,小明很满意(●▽●)"); } }
执行程序,输出结果:
头几天小明自己去找房子,找不到!
后来,小明new了一个中介代理找房子!
代理前操作 - 中介寻找资源(向外出租的房子)...
房东要出租房子!
代理后操作 - 中介带客户看房、签订合同、金钱交易、交付房子...
找到一个舒适的家,小明很满意(●▽●)
8.4 优缺点
优点:
- 职责清晰。
- 高扩展性。
- 智能化。
缺点:
- 速度变慢:由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。比如保护代理
- 实现复杂:实现代理模式需要额外的工作,有些代理模式的实现非常复杂。比如远程代理
8.5 代理模式的应用场景
应用场景:
- 需要控制对目标对象的访问。
- 需要对目标对象进行方法增强。如:添加日志记录,计算耗时等。
- 需要延迟加载目标对象。
按职责来划分,通常有以下使用场景:
- 远程代理:客户端需要访问远程主机中的对象,使用远程代理
- 虚拟代理:需要一个消耗资源较少的对象来代表资源较多的对象时,使用虚拟代理
- Copy-on-Write 代理。
- 保护(Protect or Access)代理:需要控制访问权限,使用保护代理
- 缓存(Cache)代理:需要为一个频繁访问的操作结果提供临时存储空间,使用缓存代理
- 防火墙(Firewall)代理。
- 同步化(Synchronization)代理。
- 智能引用(Smart Reference)代理:需要为一个对象的访问(引用)提供额外的操作时,使用智能引用代理
9)JDK动态代理模式
9.1 为什么要使用动态代理
静态代理通常情况下,每一个代理类编译之后都会生成一个字节码文件,代理所实现的接口和所代理的方法都固定,这种代理称为静态代理。
静态代理中,客户端通过 Proxy 调用 RealSubject 的 request 方法,同时封装其他方法(代理前/代理后操作),比如上面的查询验证以及日志记录功能。
静态代理的优点是实现简单,但是,代理类以及真实主题类都需要事先存在,代理类的接口以及代理方法都明确指定,但是如果需要:
- 代理不同的真实主题类
- 代理一个真实主题类的不同方法
需要增加新的代理类,这会导致系统中类的个数大大增加。
这是静态代理最大的缺点,为了减少系统中类的个数,可以采用动态代理。
9.2 动态代理模式
动态代理可以让系统根据实际需要动态创建代理类,同一个代理类可以代理多个不同的真实主题类,而且可以代理不同方法,在 Java 中实现动态代理需要 Proxy 类以及 InvocationHandler 接口。
9.3 Proxy
Proxy 类提供了用于创建动态代理类和实例对象的方法,最常用的方法包括:
● public static Class<?> getProxy(ClassLoader loader,Class<?> ... interfaces) :该方法返回一个 Class 类型的代理类
参数 interfaces - 表示需要提供类加载器并指定代理的接口数组,这个数组应该与真实主题类的接口列表一致。
● public staitc Object newProxyInstance(ClassLoader loader,Class<?> [] interfaces,InvocationHandler h) :返回一个动态创建的代理类实例
参数 ① loader - 类加载器 ② interfaces - 代理类实现的接口列表,同理与真实主题的接口列表一致 ③ h - 所指派的调用处理程序类。
9.4 InvocationHandler
InvocationHandler 接口是代理程序类的实现接口,该接口作为代理实例的调用处理者的公共父类,每一个代理类的实例都可以提供一个相关的具体调用者(也就是实现了 InvocationHandler 的类)。
该接口中声明以下方法:
● public Object invoke(Object proxy,Method method,Object [] args) :该方法用于处理对代理类实例的方法调用并返回相应结果,当一个代理实例中的业务方法被调用时自动调用该方法。
参数 ① proxy - 代理类的实例 ② method - 需要代理的方法 ③ args - 方法的参数数组
动态代理类需要在运行时指定所代理的真实主题类的接口,客户端在调用动态代理对象的方法时,调用请求会自动转发到 InvocationHandler 的 invoke 方法,由 invoke 实现对请求的统一处理。
9.5 实例
为一个数据访问 Dao 层增加方法调用日志,记录每一个方法被调用的时间和结果,使用动态代理模式进行设计。
① 抽象主题角色:
public interface AbstractUserDao { void findUserById(String id); }
② 真实主题角色:创建真实主题角色,实现上面的接口
public class UserDao implements AbstractUserDao { @Override public void findUserById(String id) { System.out.println("findUserById(" + id + ") => " + ("1".equals(id) ? "success!" : "fail!")); } }
③ 动态代理主题角色:实现 InvocationHandler 接口,构建一个与真实主题角色相关联的代理类,
public class DaoLogHandler implements InvocationHandler { private Object object; // 被代理的对象,实际的方法执行者 public DaoLogHandler(Object object) { this.object = object; } /** * @param proxy - 代理对象 * @param method - 代理的方法对象 * @param args - 方法调用时参数 * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { beforeInvoke(); // 代理前操作 Object result = method.invoke(object, args); // method.invoke(object,args) 相当于 object.method(args) postInvoke(); // 代理后操作 return result; // 返回方法的执行结果 } private void beforeInvoke() { System.out.println("记录执行时间"); } private void postInvoke() { System.out.println("记录执行结果"); } }
④ 测试:
cast() 方法相当于是对强制类型转换进行了包装,转换前进行了安全检查。
Java 提供了一个 Proxy 类,调用它的 newInstance 静态方法返回一个代理实例,该方法需要三个参数:
- ClassLoader loader:真实主题角色的类加载器
- Class<?>[] interfaces:真实主题角色实现的所有的接口,接口是特殊的类,使用Class[]装载多个接口
- InvocationHandler h:与真实主题角色相关联的 InvocationHandler 代理类
public class ProxyTest { public static void main(String[] args) { // 创建一个实例对象,这个对象是被代理的对象 AbstractUserDao userDao = new UserDao(); // 创建一个与代理对象相关联的 InvocationHandler InvocationHandler handler = new DaoLogHandler(userDao); // 创建一个代理对象 daoProxy 来代理真实主题角色,代理对象的每个执行方法都会替换执行 Invocation 中的 invoke 方法 AbstractUserDao daoProxy = AbstractUserDao.class.cast(Proxy.newProxyInstance( UserDao.class.getClassLoader(), // 真实主题角色的类加载器 new Class<?>[]{AbstractUserDao.class}, // 真实主题角色实现的所有的接口,接口是特殊的类,使用Class[]装载多个接口 handler // 与真实主题角色相关联的 InvocationHandler 代理类 )); // 使用代理类执行 UserDao 的方法 daoProxy.findUserById("1"); System.out.println("============================"); daoProxy.findUserById("2"); } }
输出如下:
记录执行时间
findUserById(1) => success!
记录执行结果
============================
记录执行时间
findUserById(2) => fail!
记录执行结果