设计模式最初并不是出现在软件设计中,而是被用于建筑领域的设计中。
1977 年美国著名建筑大师、加利福尼亚大学伯克利分校环境结构中心主任 Christopher Alexander 在他的著作《建筑模式语言:城镇、建筑、构造》中描述了一些常见的建筑设计问题,并提出了 253 种关于对城镇、邻里、住宅、花园和房间等进行设计的基本模式。
1990 年软件工程界开始研讨设计模式的话题,后来召开了多次关于设计模式的研讨会。直到 1995 年,ErichGamma、Richard Helm、Ralph Johnson、John Vlissides 等 4 位作者合作出版了《设计模式:可复用面向对象软件的基础》一书,在此书中收录了 23 个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破,这 4 位作者在软件开发领域里也以他们的 Gang of Four,GoF 著称
软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和包装性以及类的关联关系和组合关系的充分理解。
正确使用设计模式具有以下优点:
统一建模语言(Unified Modeling Language, UML) 是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。
UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。
类图(Class Diagram)是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。
类图的作用:
在 UML 图中,类使用包含类名、属性(field)和方法(method)且带有分割线的矩形来表示,比如下图表示一个 Employee 类,它包含 name、age 和 address 这 3 个属性,以及 work() 方法。
属性或方法名称前加的加号和减号表示了这个属性或方法的可见性,UML类图中表示可见性的符号有三种:
属性的完整表达方式是: 可见性 名称:类型 [= 缺省值]
方法的完整表达方式是: 可见性 名称(参数列表):类型 [: 返回类型]
关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。我们先来介绍一般关联。
关联又可以分为单向关联、双向关联、自关联。
1, 单向关联: 在 UML 类图中单向关联用一个带箭头的实现表示。下图表示每个顾客都有一个地址,这通过让 Customer 类持有一个类型为 Address 的成员变量类实现。
2, 双向关联: 在 UML 类图中,双向关联用一个不带箭头的直线表示。下图中在 Customer 类中维护一个 List
3, 自关联: 在 UML 类图中用一个带有箭头且指向自身的线表示。下图的意思就是 Node 类包含类型为 Node 的成员变量,也就是“自己包含自己”。
聚合关系是关联关系的一种,是强关联关系,是整体和局部之间的关系。
聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
在 UML 图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。下图所示是大学和教师的关系图:
组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和脑的关系,没有了头,嘴也就不存在了。
在 UML 图中,组合关系用带实心菱形的实线来表示,菱形指向整体。下图所示是头和嘴的关系图:
依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。
在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。下图所示是司机和汽车的关系图,司机驾驶骑车:
继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。
在 UML 类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类。其类图如下图所示:
实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。
在 UML 类图中,实现关系使用带空心三角形箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,其类图如下图所示。
在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据 6 条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。
对扩展开放,对修改关闭。
在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。
想要达到这样的效果,我们需要使用接口和抽象类。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需求发生变化时,只要根据需求重新派生一个实现类来扩展就可以了。
开闭原则的应用:
里氏代换原则是面向对象设计的基本原则之一。
里氏代换原则:任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
里氏代换原则应用:正方形不是长方形
高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
依赖倒转原则应用:
客户端不应该被迫依赖于它不使用的方法:一个类对另一个类的依赖应该建立在最小的接口上。
接口隔离原则的应用:
迪米特法则又叫最少知识原则。
只和你的直接朋友交谈,不跟 “陌生人” 说话 (Talk only to your immediate friends and not to strangers)。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则中的 “朋友” 是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等;这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
通常类的复用分为继承复用和合成复用两种。
继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
合成复用原则应用:
继承复用:
组合或聚合复用示例:
创建模式的主要关注点是 “怎样创建对象?”,它的主要特点是 “将对象的创建与使用分离”。
这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。
创建模式分为:
单例模式 (Singleton Pattern) 是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型设计模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式主要有以下角色:
单例设计模式分类两种:
饿汉式实现的实例:
/**
* @Description 单例模式:饿汉式 静态成员变量
* @Author 高建伟-joe
* @Date 2023-12-27
*/
public class Singleton {
// 1.私有构造方法
private Singleton(){}
// 2.在本类中创建本类对象
private static Singleton instance = new Singleton();
// 3.提供一个公共的访问方法,让外界获取该对象
public static Singleton getInstance(){
return instance;
}
}
/**
* @Description 单例模式:饿汉式 静态代码块
* @Author 高建伟-joe
* @Date 2023-12-27
*/
public class Singleton {
// 1.私有构造方法
private Singleton(){}
// 2.声明Singleton类型的变量
private static Singleton instance;
// 3.在静态代码块中赋值
static {
instance = new Singleton();
}
// 4.对外提供获取该类对象的方法
public static Singleton getInstance(){
return instance;
}
}
提示: \color{red}{提示:} 提示: instance 对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
懒汉式实现实例:
/**
* @Description 懒汉式 线程不安全
* @Author 高建伟-joe
* @Date 2023-12-27
*/
public class Singleton {
// 1.私有构造方法
private Singleton(){}
// 2.声明Singleton类型的变量
private static Singleton instance;
// 3.对外提供访问方式
public static Singleton getInstance(){
if (instance == null){
// 线程1等待,线程2获取到cpu的执行权,也会进入到该判断里面
instance = new Singleton();
}
return instance;
}
}
提示: \color{red}{提示:} 提示: 线程不安全
/**
* @Description 懒汉式 线程安全
* @Author 高建伟-joe
* @Date 2023-12-27
*/
public class Singleton {
// 1.私有构造方法
private Singleton(){}
// 2.声明Singleton类型的变量
private static Singleton instance;
// 3.对外提供访问方式
public static synchronized Singleton getInstance(){
if (instance == null){
// 线程1等待,线程2获取到cpu的执行权,会等待线程1执行完
instance = new Singleton();
}
return instance;
}
}
提示: \color{red}{提示:} 提示: 线程安全,性能不高
/**
* @Description 懒汉式 双重检查锁
* @Author 高建伟-joe
* @Date 2023-12-27
*/
public class Singleton {
// 1.私有构造方法
private Singleton(){}
// 2.声明Singleton类型的变量
private static Singleton instance;
// 3.对外提供访问方式
public static Singleton getInstance(){
// 第一次判断,如果 instance 的值不为 null, 不需要抢占锁,直接返回对象
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
提示: \color{red}{提示:} 提示: 线程安全、性能不错,但是在多线程情况下,会出现空指针问题,原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作。
/**
* @Description 懒汉式 双重检查锁
* @Author 高建伟-joe
* @Date 2023-12-27
*/
public class Singleton {
// 1.私有构造方法
private Singleton(){}
// 2.声明Singleton类型的变量
private static volatile Singleton instance;
// 3.对外提供访问方式
public static Singleton getInstance(){
// 第一次判断,如果 instance 的值不为 null, 不需要抢占锁,直接返回对象
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
提示: \color{red}{提示:} 提示: 添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。
/**
* @Description 懒汉式 静态内部类的方式
* 静态内部类单例模式中实例由内部类创建,
* 由于 JVM 在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。
* 静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。
* @Author 高建伟-joe
* @Date 2023-12-27
*/
public class Singleton {
// 1. 私有构造方法
private Singleton(){}
//2. 静态内部类中创建对象
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
// 3. 对外提供静态方法获取该对象
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
提示: \color{red}{提示:} 提示: 第一次加载 Singleton 类时不会去初始化 INSTANCE,只有第一次调用 getInstance,虚拟机加载 SingletonHolder 并初始化 INSTANCE,这样不仅能保证线程安全,也能保证 Singleton 类的唯一性。
小结: 静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间浪费。
/**
* @Description 饿汉式 枚举方式
* @Author 高建伟-joe
* @Date 2023-12-27
*/
public enum Singleton {
INSTANCE;
}
枚举方式:枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
破坏单例模式: 使单例类可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。
序列化反序列化:
/**
* @Description 懒汉式 静态内部类
* @Author 高建伟-joe
* @Date 2023-12-28
*/
public class Singleton implements Serializable {
private Singleton(){}
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
/**
* @Description 序列化和反序列化 破坏单例类 测试类
* @Author 高建伟-joe
* @Date 2023-12-28
*/
public class Client {
public static void main(String[] args) throws Exception {
// writeObj2File();
readObjFromFile();
readObjFromFile();
}
// 向文件中写对象
public static void writeObj2File() throws Exception{
Singleton singleton = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./a.txt"));
oos.writeObject(singleton);
oos.close();
}
// 从文件中读对象
public static void readObjFromFile() throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./a.txt"));
Singleton singleton = (Singleton) ois.readObject();
System.out.println(singleton);
ois.close();
}
}
反射:
/**
* @Description 懒汉式 静态内部类
* @Author 高建伟-joe
* @Date 2023-12-28
*/
public class Singleton {
private Singleton(){}
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
/**
* @Description 反射 破坏单例模式 测试类
* @Author 高建伟-joe
* @Date 2023-12-28
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1.获取Singleton 的字节码对象
Class<Singleton> clazz = Singleton.class;
// 2.获取无参构造方法
Constructor<Singleton> declaredConstructor = clazz.getDeclaredConstructor();
// 3.取消访问检查
declaredConstructor.setAccessible(true);
// 4.创建 Singleton 对象
Singleton singleton = declaredConstructor.newInstance();
Singleton singleton1 = declaredConstructor.newInstance();
System.out.println(singleton == singleton1);
}
}
序列化和反序列化方式破坏单例模式的解决方法:在 Singleton 类中添加 readResolve() 方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新 new 出来的对象。
/**
* @Description 懒汉式 静态内部类
* @Author 高建伟-joe
* @Date 2023-12-28
*/
public class Singleton implements Serializable {
private Singleton(){}
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
// 当进行反序列化时,会自动调用该方法,将该方法的返回值直接返回。
public Object readResolve(){
return SingletonHolder.INSTANCE;
}
}
反射方式破解单例模式的解决方法:
/**
* @Description 懒汉式 静态内部类
* @Author 高建伟-joe
* @Date 2023-12-28
*/
public class Singleton {
private static boolean flag = false;
private Singleton(){
synchronized (Singleton.class){
// 判断 flag 的值是否为 true,如果是 true,说明非第一次访问,直接抛一个异常,如果是 false ,说明是第一次访问。
if (flag){
throw new RuntimeException("不能创建多个对象");
}
// 将 flag 的值设为 true
flag = true;
}
}
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
Runtime 类使用的设计模式是单例模式:饿汉式 静态成员
/**
* Every Java application has a single instance of class
* Runtime
that allows the application to interface with
* the environment in which the application is running. The current
* runtime can be obtained from the getRuntime
method.
*
* An application cannot create its own instance of this class.
*
* @author unascribed
* @see java.lang.Runtime#getRuntime()
* @since JDK1.0
*/
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class Runtime
are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the Runtime
object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
}
Runtime 类的简单使用:
/**
* @Description Runtime 类的使用
* @Author 高建伟-joe
* @Date 2023-12-28
*/
public class RuntimeDemo {
public static void main(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime();
// 返回 java 虚拟机中的内存总量
System.out.println(runtime.totalMemory());
// 返回 java 虚拟机试图使用的最大内存量
System.out.println(runtime.maxMemory());
// 调用 runtime 的 exec 方法,参数是一个命令
Process ipconfig = runtime.exec("ipconfig");
// Process 对象的获取输入流的方法
InputStream inputStream = ipconfig.getInputStream();
byte[] arr = new byte[1024 * 1024 * 100];
// 读取数据
int len = inputStream.read(arr); // 返回读到的字节个数
// 将字节数组转化为字符串输出到控制台
System.out.println(new String(arr,0, len, "GBK"));
}
}
需求:设计一个咖啡店点餐系统。
在 Java 中,万物皆对象,这些对象都需要创建,如果创建的时候直接 new 该对象,就会对该对象耦合严重,假如我们要更换对象,所有 new 对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦。
本文介绍 3 种工厂模式的使用:
简单工厂不是一种设计模式,反而比较像是一种编程习惯。
简单工厂包含以下角色:
使用简单工厂对上面示例进行该进,类图如下:
工厂 (factory) 处理创建对象的细节,一旦有了 SimpleCoffeFactory, CoffeeStore 类中的 orderCoffee() 就变成了此对象的客户,后期如果需要 Coffee 对象直接从工厂中获取即可。这样就解除了和 Coffee 实现类的耦合,同时又产生了新的耦合,CoffeeStore 对象和 SimpleCoffeeFactory 工厂对象的耦合,工厂对象和商品对象的耦合。
后期如果再加新品种的咖啡,我们势必要求修改 SimpleCoffeeFactory 的代码,违反了开闭原则。工厂类的客户端可能有很多,比如新建一个店等,这样只需要修改工厂类的代码,省去其他的修改操作。
静态工厂:在开发中也有一部分人将工厂类的创建对象的功能定义为静态的,这个就是静态工厂模式,它也不是 23 种设计模式中的。代码如下
/**
* @Description
* @Author 高建伟-joe
* @Date 2023-12-28
*/
public class SimpleCoffeeFactory {
public static Coffee createCoffee(String type){
Coffee coffee = null;
if ("American".equals(type)){
coffee = new AmericanCoffee();
} else if ("Latte".equals(type)){
coffee = new LatteCoffee();
} else {
throw new RuntimeException("您点的咖啡,没有了");
}
return coffee;
}
}
优点: 封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。
缺点: 增加新产品时还是需要修改工厂类的代码,违背了“开闭原则”。
针对上例中的缺点,使用工厂方法模式就可以完美解决,完全遵循开闭原则。
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。
工厂方法模式中的主要角色:
使用工厂方法模式对上例进行改进,类图如下:
从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了,这样就解决了简单工厂模式的缺点。
工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。
优点:
缺点:
前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机场只生产电视等。
这些工厂只生产同种类产品,同种类产品称为同等级产品,也就是说:工厂方法模式只考虑生产同等级产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类)的产品,如电器厂既生产电视机又生产洗衣机或空调等。
本节要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,下图所示横轴是产品等级,也就是同一类产品;纵轴是产品族,也就是同一品牌的产品,同一品牌的产品产自同一工厂。
是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
抽象工厂模式的主要角色如下:
使用抽象工厂模式实现,类图如下:
使用抽象工厂模式,如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。
优点: 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点: 当产品族中需要添加一个新产品时,所有的工厂类都需要进行修改。
当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
如:输入法换皮肤,一整套一起换。生成不同操作系统的程序。
简单工厂 + 配置文件解除耦合
可以通过工厂模式 + 配置文件的方式解除工厂对象和产品对象的耦合。在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。
单列集合获取迭代器的方法就使用到了工厂方法模式。类图如下:
Collection 接口是抽象工厂类,ArrayList是具体的工厂类,Iterator 接口是抽象产品类,ArrayList类中的Iter内部类是具体的产品类。在具体的工厂类中 iterator() 方法创建具体的产品类的对象。
DateForamt 类中的 getInstance() 方法使用的是工厂模式。
Calendar 类中的 getInstance() 方法使用的是工厂模式。
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。
原型模式包含以下角色:
接口类图如下:
原型模式的克隆分为浅克隆和深克隆。
浅克隆: 创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型的属性,仍指向原有属性所指向的对象的内存地址。
深克隆: 创建一个新对象,属性中引用的其他对象也会被克隆,不会指向原有对象地址。
Java 中的 Object 类中提供了 clone() 方法来实现浅克隆。Cloneable 接口是上面的类图中的抽象原型类,而实现了 Cloneable 接口的子实现类就是具体的原型类。
进行深克隆需要使用对象流。
将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。
建造者 (Builder) 模式包含如下角色:
类图如下:
生产自行车。类图如下:
上面示例是 Builder 模式的常规用法,指挥者类 Director 在建造者模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况下需要简化系统结构,可以把指挥者类和抽象建造者进行结合。
这样做确实简化了系统结构,但同时也加重了抽象建造者类的职责,也不是太符合单一职责原则,如果 construct() 过于复杂,建议还是封装到 Director 中。
/**
* @Description
* @Author 高建伟-joe
* @Date 2024-01-05
*/
public abstract class Builder {
protected Bike mbike = new Bike();
public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();
public Bike construct(){
this.buildSeat();
this.buildFrame();
return this.createBike();
}
}
优点:
缺点:
建造者 (Builder) 模式创建的是复杂对象,其产品的各个部分经常面临着剧烈变化,但将它们组合在一起的算法缺相对稳定,所以它通常在以下场合使用:
建造者模式除了上面的用途外,在开发中还有一个常用的使用方式,就是当一个类构造器需要传入很多参数时,如果创建这个类的实例,代码可读性会非常差,而且很容易引入错误,此时就可以利用建造者模式进行重构。
/**
* @Description
* @Author 高建伟-joe
* @Date 2024-01-05
*/
public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainboard;
@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
private Phone(Builder builder){
this.cpu = builder.cpu;
this.screen = builder.screen;
this.memory = builder.memory;
this.mainboard = builder.mainboard;
}
public static final class Builder{
private String cpu;
private String screen;
private String memory;
private String mainboard;
public Builder cpu(String cpu){
this.cpu = cpu;
return this;
}
public Builder screen(String screen){
this.screen = screen;
return this;
}
public Builder memory(String memory){
this.memory = memory;
return this;
}
public Builder mainboard(String mainboard){
this.mainboard = mainboard;
return this;
}
public Phone build(){
return new Phone(this);
}
}
}
重构后的代码在使用起来更方便,某种程度上也可以提高开发效率。从软件设计上,对程序员的要求比较高。
工厂方法模式注重的是整体对象的创建方式。
建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象。
抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品,具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。
建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。
如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装厂,通过对部件的组装可以返回一辆完整的汽车。
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象。
由于组合关系和聚合关系比继承关系耦合度低,满足"合成复用原则",所以对象结构型模式比类结构型模式具有更大的灵活性。
结构型模式分为以下 7 种:
由于某些原因需要给对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
Java 中的代理按照代理类的生成时机不同分为静态代理和动态代理。静态代理代理类在编译器就生成,而动态代理代理类则是在 Java 运行时动态生成。动态代理又有 JDK 代理和 CGLib 代理两种。
代理(Proxy)模式分为三种角色:
通过火车站卖票案例,来学习静态代理。
火车站是目标对象,代售点是代理对象。
JDK提供的动态代理:Java 中提供了一个动态代理类 Proxy,Proxy 并不是我们所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。
执行流程:
如果没有定义 SellTickets 接口,只定义了 TrainStation。很显然 JDK 代理是无法使用了,因为 JDK 动态代理要求必须定义接口,对接口进行代理。
CGLib 是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为 JDK 的动态代理提供了很好的补充。
CGLib 是第三方提供的包,所以需要引入 Jar 包
<dependency>
<groupId>cglibgroupId>
<artifactId>cglibartifactId>
<version>2.2.2version>
dependency>
JDK代理和CFLib代理:
使用 CGLib 实现动态代理,CGLib 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类,在 JDK1.6 之前比使用 Java 反射效率要高,唯一需要注意的是,CGLib 不能对声明为 final 的类或者方法进行处理,因为 CGLib 原理是动态生成被代理类的子类。
在 JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于 CGLib 代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理,所以如果有接口使用JDK动态代理,如果没有接口使用CGLib代理。
动态代理和静态代理:
动态代理和静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口每增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题。
优点:
缺点:
适配器模式: 将一个类的接口转换成客户希望的另一个接口,使原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
适配器模式(Adapter)包含以下主要角色:
实现方式: 定义一个适配器类来实现当前系统的业务接口,同时继承现有组件库中已经存在的组件。
类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用。
实现方式: 对象适配器模式可采用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。
注意: 还有一个适配器模式是接口适配器模式。当不希望实现接口中所有的方法时,可以创建一个抽象类 Adapter,实现所有方法。而此时我们只需要继承该抽象类即可。
Reader(字符流)、InputStream(字节流) 的适配使用的是 InputStreamReader。
InputStreamReader 继承自 java.io 包中的 Reader,对其中抽象的未实现的方法给出实现。
从上图可以看出:
结论:从表层来看,InputStreamReader 做了 InputStream 字节流类到 Reader 字符流之间的转换。而从如上 Sun JDK 中的实现类关系结构中可以看出,是 StreamDecoder 的设计实现在实际上采用了适配器模式。
装饰者模式:是指在不改变现有对象结构的情况下,动态地给对象增加一些职责(即增加额外功能)的模式。
装饰(Decorator)模式中的角色:
案例:快餐店
好处:
不采用继承的情况主要有两类:
IO 流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter
BufferedWriter 类图
BufferedWriter 使用装饰者模式对 Writer 子实现类进行了增强,增加了缓冲区,提高了写数据的效率。
静态代理和装饰者模式的区别:
相同点:
不同点:
桥接模式:将抽象与实现分离,使它们可以独立变化。它使用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
桥接(Bridge)模式包含以下主要角色:
视频播放器 实例
桥接模式的好处:
外观模式,又名门面模式,是一种通过多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。
该模式对外有一个统一的接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
外观(Facade)模式是 “迪米特法则” 的典型应用。
外观(Facade)模式主要包含以下角色:
实例:智能家电控制
优点:
缺点: 不符合开闭原则,修改很麻烦。
RequestFacade 类使用了外观模式。
为什么在此处使用外观模式?
定义 RequestFacade 类,分别实现 ServletRequest 和 HttpServletRequest,同时 定义私有成员变量 Request,并且方法的实现调用 Request 的实现。然后将 RequestFacade 上转为 ServletRequest 传给 servlet 的 service 方法,这样即使在 servlet 中被下转为 RequestFacade,也不能访问私有成员变量对象中的方法。既用了 Request,又能防止其中方法被不合理的访问。
组合模式,又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
组合模式主要包含三种角色:
实例:软甲菜单
在使用组合模式时,根据抽象构建类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式。
1, 透明组合模式: 透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中 MenuComponent 声明了 add、remove、getChild 方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。
透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象是不可能有下一个层次的对象,既不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段调用这些方法可能会出错(如果没有提供相应的错误处理代码)。
2, 安全组合模式: 在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点 Menu 类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如文件目录的显示,多级目录的呈现等树形结构数据的操作。
享元模式,运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建对象的数量、避免大量相似对象的开销,从而提高系统资源的利用率。
享元(Flyweight)模式中存在以下两种状态:
享元模式的主要有以下角色:
示例:俄罗斯方块
优点:
缺点: 为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂。
使用场景:
Integer 类使用了享元模式。
Integer 默认先创建并缓存 -128~127 之间的 Integer 对象,当调用 valueOf 时如果参数在 -128~127 之间则计算下标并从缓存中返回,否则创建一个新的 Integer 对象。
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分配行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足"合成复用原则",所以对象行为模式比类行为模式具有更大的灵活性。
行为型模式分为:
模板方法模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
模板方法(Template Method)模式包含以下主要角色:
一般钩子方法是用于判断的逻辑方法,这类方法名一般为 isXxx, 返回值类型为 Boolean 类型。
示例:炒菜
优点:
缺点:
InputStream 类使用了模板方法模式。在 InputStream 类中定义了多个 read() 方法。
策略模式,该模式定义了一系列的算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。
策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
策略模式的主要角色如下:
示例:促销活动
优点:
缺点:
Comparator 中的策略模式。在 Arrays 类中有一个 sort() 方法。
Arrays 就是一个环境角色类,sort方法可以传一个新策略来排序。
命令模式,将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分隔开。这样两者之间通过命令对象沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。
命令模式主要包含以下主要角色:
示例:点餐
优点:
缺点:
Runable 是一个典型的命令模式,Runable 担任命令的角色,Thread 充当的是调用者, Start 方法就是其执行方法。
责任链模式,又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
责任链主要包含以下角色:
示例:请假流程控制系统
优点:
缺点:
在 JavaWeb 应用开发中,FilterChain 是职责链(过滤器)模式的典型应用,
示例:通过按钮控制电梯状态
问题分析:
状态模式,对有状态的对象,把复杂的"判断逻辑"提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
状态模式包含以下主要角色:
对上面示例进行改进,类图如下
优点:
缺点:
观察者模式,又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。
在观察者模式中有如下角色:
示例:微信公众号
优点:
缺点:
在java中,通过 java.util.Observable 类和 java.util.Observer 接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例。
中介者模式,又叫调停模式,定义一个中介角色来封装一系列对象之间的交互,使得原有对象之间耦合松散,且可以独立地改变它们之间的交互。
中介者模式包含以下主要角色:
示例:租房
优点:
缺点: 当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。
迭代器模式,提供一个对象来顺序访问聚合对象中的一系列数据,而不是暴露聚合对象的内部表示。
迭代器模式主要包含以下角色:
示例: 定义一个容器对象。使用迭代器来遍历。
优点:
缺点: 增加了类的个数,这在一定程度上增加了系统的复杂性。
迭代器在 Java 的很多集合类中被广泛应用。
注意:当我们在使用 Java 开发的时候,想使用迭代器模式的话,只要让我们自己定义的容器类实现 java.util.Iterable 并实现其中的 iterator() 方法使其返回一个 java.util.Iterator 的实现类就可以了。
访问者模式,封装了一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。
访问者模式包含以下主要角色:
示例:给宠物喂食
优点:
缺点:
访问者模式用到了一种双分派的技术。
分派: 变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap(), map 变量的静态类型是 Map ,实际类型是 HashMap。根据对象的类型而对方法进行的选择,就是分派(Dispatch), 分派(Dispatch)又分为两种,即静态分派和动态分派。
静态分派(Static Dispatch): 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。
动态分派(Dynamic Dispatch): 发生在运行时期,动态分派动态地置换掉某个方法。Java 通过方法的重写支持动态分派。
Java 编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型,而方法的调用则是根据对象的真实类型,而不是静态类型。
重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。
双分派: 所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。
双分派,实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。
备忘录模式,又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。
备忘录模式的主要角色如下:
备忘录有两个等效的接口:
示例: 游戏挑战 BOSS
游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打 BOSS 前和后一定会不一样的,我们允许玩家如果感觉与 Boss 决斗的效果不理想可以让游戏恢复到决斗之前的状态。
要实现上述示例,有两种方式:
备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。
备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在 Java 语言中,实现双重接口的办法就是将备忘录类设计成发起人的内部成员类。
优点:
缺点: 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
解释器模式,给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
文法(语法)规则: 文法是用于描述的语法结构的形式规则。
抽象语法树: 在计算机科学中,抽象语法树(AbstractSyntaxTree, AST), 或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
解释器模式包含以下主要角色:
示例:设计实现加减法的软件。
优点:
缺点:
黑马程序员