目录
1. 概述
2. 创建型模式
2.1 简单(静态)工厂模式
2.1.1 介绍
2.1.2 实现
2.2 工厂模式
2.3 抽象工厂模式
2.4 单例模式
2.4.1 饿汉模式
2.4.2 懒汉模式
2.4.3 线程安全的懒汉式
2.4.4 DCL单例 - 高性能的懒汉式
2.5 建造者模式
2.6 原型模式
2.7 创建型模式总结
1. 概述
设计模式(Design Pattern)是前辈们经过相当长的一段时间的试验和错误总结出来的,是软件开发过程中面临的通用问题的解决方案。这些解决方案使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
设计模式的优点:
- ① 可以提高程序员的思维能力、编程能力和设计能力。
- ② 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
- ③ 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
有一些重要的设计原则在开篇和大家分享下,这些原则将贯通全文:
- 针对接口编程,而不是针对实现编程。这个很重要,也是优雅的、可扩展的代码的第一步。
- 职责单一原则。每个类都应该只有一个单一的功能,并且该功能应该由这个类完全封装起来。
- 对修改关闭,对扩展开放。对修改关闭是说,我们辛辛苦苦加班写出来的代码,该实现的功能和该修复的 bug 都完成了,别人可不能说改就改;对扩展开放就比较好理解了,也就是说在我们写好的代码基础上,很容易实现扩展。
设计模式是对大家实际工作中写的各种代码进行高层次抽象的总结,其中最出名的当属 Gang of Four(GoF)的分类了,他们将设计模式分类为 23 种经典的模式,根据用途我们又可以分为三大类,分别为创建型模式(5种)、结构型模式(7种)和行为型模式(11种)。
(1)创建型模式
- 简单(静态)工厂模式:一个工厂类根据传入的参量决定创建出那一种产品类的实例。(该模式不属于23种GOF设计模式之一)
- 工厂方法:定义一个创建对象的接口,让子类决定实例化那个类。
- 抽象工厂:创建相关或依赖对象的家族,而无需明确指定具体类。
- 单例模式:某个类只能有一个实例,提供一个全局的访问点。
- 饿汉模式
- 懒汉模式
- 建造者模式:封装一个复杂对象的构建过程,并可以按步骤构造。
- 原型模式:通过复制现有的实例来创建新的实例。
(2)结构型模式
- 外观模式:对外提供一个统一的方法,来访问子系统中的一群接口。
- 桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立的变化。
- 组合模式:将对象组合成树形结构以表示“”部分-整体“”的层次结构。
- 装饰模式:动态的给对象添加新的功能。
- 代理模式:为其他对象提供一个代理以便控制这个对象的访问。
- 适配器模式:将一个类的方法接口转换成客户希望的另外一个接口。
- 亨元(蝇量)模式:通过共享技术来有效的支持大量细粒度的对象。
(3)行为型模式
- 模板模式:定义一个算法结构,而将一些步骤延迟到子类实现。
- 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。
- 策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。
- 状态模式:允许一个对象在其对象内部状态改变时改变它的行为。
- 观察者模式:对象间的一对多的依赖关系。
- 备忘录模式:在不破坏封装的前提下,保持对象的内部状态。
- 中介者模式:用一个中介对象来封装一系列的对象交互。
- 命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
- 访问者模式:在不改变数据结构的前提下,增加作用于一组对象元素的新功能。
- 责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
- 迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
2. 创建型模式
创建型模式的作用就是创建对象,说到创建一个对象,最熟悉的就是 new 一个对象,然后 set 相关属性。但是,在很多场景下,我们需要给客户端提供更加友好的创建对象的方式,尤其是那种我们定义了类,但是需要提供给其他开发者用的时候。
2.1 简单(静态)工厂模式
2.1.1 介绍
① 一句话来说就是:一个工厂类根据传入的参量决定创建出那一种产品类的实例。因为逻辑实现简单,所以称为简单工厂模式,也因为工厂中的方法一般设置为静态,所以也称为静态工厂,它不属于23种模式。
② 简单工厂模式专门定义一个工厂类来负责创建其他类的实例,被创建的实例通常都具有共同的父类,在工厂类中,可以根据参数的不同返回不同类的实例。升级版本简单工厂模式,通过反射根据类的全路径名生成对象。
③ 简单工厂模式就是将这部分创建对象语句分离出来,由工厂类来封装实例化对象的行为,修改时只需要修改类中的操作代码,使用时调用该类不需要考虑实例化对象的行为,使得后期代码维护升级更简单方便,有利于代码的可修改性与可读性。
④ 但是如果增加新的产品的话,需要修改工厂类的判断逻辑,违背开闭原则。
2.1.2 实现
直接上代码:
public class FoodFactory {
public static Food makeFood(String name) {
if (name.equals("noodle")) {
Food noodle = new LanZhouNoodle();
noodle.addSpicy("more");
return noodle;
} else if (name.equals("chicken")) {
Food chicken = new HuangMenChicken();
chicken.addCondiment("potato");
return chicken;
} else {
return null;
}
}
}
其中,LanZhouNoodle 和 HuangMenChicken 都继承自 Food。
简单地说,简单工厂模式通常就是这样:一个工厂类 XxxFactory,里面有一个静态方法,根据我们不同的参数,返回不同的派生自同一个父类(或实现同一接口)的实例对象。
我们强调职责单一原则,一个类只提供一种功能,FoodFactory 的功能就是只要负责生产各种 Food。
2.2 工厂模式
简单工厂模式很简单,如果它能满足我们的需要,我觉得就不要折腾了。之所以需要引入工厂模式,是因为我们往往需要使用两个或两个以上的工厂。
public interface FoodFactory {
Food makeFood(String name);
}
public class ChineseFoodFactory implements FoodFactory {
@Override
public Food makeFood(String name) {
if (name.equals("A")) {
return new ChineseFoodA();
} else if (name.equals("B")) {
return new ChineseFoodB();
} else {
return null;
}
}
}
public class AmericanFoodFactory implements FoodFactory {
@Override
public Food makeFood(String name) {
if (name.equals("A")) {
return new AmericanFoodA();
} else if (name.equals("B")) {
return new AmericanFoodB();
} else {
return null;
}
}
}
其中,ChineseFoodA、ChineseFoodB、AmericanFoodA、AmericanFoodB 都派生自 Food。
客户端调用:
public class APP {
public static void main(String[] args) {
// 先选择一个具体的工厂
FoodFactory factory = new ChineseFoodFactory();
// 由第一步的工厂产生具体的对象,不同的工厂造出不一样的对象
Food food = factory.makeFood("A");
}
}
虽然都是调用 makeFood("A") 制作 A 类食物,但是,不同的工厂生产出来的完全不一样。
第一步,我们需要选取合适的工厂,然后第二步基本上和简单工厂一样。
核心在于,我们需要在第一步选好我们需要的工厂。比如,我们有 LogFactory 接口,实现类有 FileLogFactory 和 KafkaLogFactory,分别对应将日志写入文件和写入 Kafka 中,显然,我们客户端第一步就需要决定到底要实例化 FileLogFactory 还是 KafkaLogFactory,这将决定之后的所有的操作。
虽然简单,不过我也把所有的构件都画到一张图上,这样看着比较清晰:
2.3 抽象工厂模式
当涉及到产品族的时候,就需要引入抽象工厂模式了。
一个经典的例子是造一台电脑。我们先不引入抽象工厂模式,看看怎么实现。
因为电脑是由许多的构件组成的,我们将 CPU 和主板进行抽象,然后 CPU 由 CPUFactory 生产,主板由 MainBoardFactory 生产,然后,我们再将 CPU 和主板搭配起来组合在一起,如下图:
这个时候的客户端调用是这样的:
// 得到 Intel 的 CPU
CPUFactory cpuFactory = new IntelCPUFactory();
CPU cpu = intelCPUFactory.makeCPU();
// 得到 AMD 的主板
MainBoardFactory mainBoardFactory = new AmdMainBoardFactory();
MainBoard mainBoard = mainBoardFactory.make();
// 组装 CPU 和主板
Computer computer = new Computer(cpu, mainBoard);
单独看 CPU 工厂和主板工厂,它们分别是前面我们说的工厂模式。这种方式也容易扩展,因为要给电脑加硬盘的话,只需要加一个 HardDiskFactory 和相应的实现即可,不需要修改现有的工厂。
但是,这种方式有一个问题,那就是如果 Intel 家产的 CPU 和 AMD 产的主板不能兼容使用,那么这代码就容易出错,因为客户端并不知道它们不兼容,也就会错误地出现随意组合。
下面就是我们要说的产品族的概念,它代表了组成某个产品的一系列附件的集合:
当涉及到这种产品族的问题的时候,就需要抽象工厂模式来支持了。我们不再定义 CPU 工厂、主板工厂、硬盘工厂、显示屏工厂等等,我们直接定义电脑工厂,每个电脑工厂负责生产所有的设备,这样能保证肯定不存在兼容问题。
这个时候,对于客户端来说,不再需要单独挑选 CPU厂商、主板厂商、硬盘厂商等,直接选择一家品牌工厂,品牌工厂会负责生产所有的东西,而且能保证肯定是兼容可用的。
public static void main(String[] args) {
// 第一步就要选定一个“大厂”
ComputerFactory cf = new AmdFactory();
// 从这个大厂造 CPU
CPU cpu = cf.makeCPU();
// 从这个大厂造主板
MainBoard board = cf.makeMainBoard();
// 从这个大厂造硬盘
HardDisk hardDisk = cf.makeHardDisk();
// 将同一个厂子出来的 CPU、主板、硬盘组装在一起
Computer result = new Computer(cpu, board, hardDisk);
}
当然,抽象工厂的问题也是显而易见的,比如我们要加个显示器,就需要修改所有的工厂,给所有的工厂都加上制造显示器的方法。这有点违反了对修改关闭,对扩展开放这个设计原则。
2.4 单例模式
2.4.1 饿汉模式
饿汉模式最简单
public class Singleton {
/**
* static:
* ①表示共享变量,语意符合
* ②使得该变量能在getInstance()静态方法中使用
* final:
* ①final修饰的变量值不会改变即常量,语意也符合,当然不加final也是可以的
* ②保证修饰的变量必须在类加载完成时就已经进行赋值。
* final修饰的变量,前面一般加static
*/
private static final Singleton singleton = new Singleton();
/**
* 私有化构造方法,使外部无法通过构造方法构造除singleton外的类实例
* 从而达到单例模式控制类实例数目的目的
*/
private Singleton(){}
/**
* 类实例的全局访问方法
* 因为构造方法以及被私有化,外部不可能通过new对象来调用其中的方法
* 加上static关键词使得外部可以通过类名直接调用该方法获取类实例
* @return
*/
public static Singleton getSingleton() {
return singleton;
}
}
说明:
① 优点:一般使用static和final修饰变量(具体作用已经在代码里描述了),只在类加载时才会初始化,以后都不会,线程绝对安全,无锁,效率高。
② 缺点:类加载的时候就初始化,不管用不用,都占用空间,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单)。
2.4.2 懒汉模式
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton() {
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
说明:
① 优点:在外部需要使用的时候才进行实例化,不使用的时候不会占用空间。
② 缺点:线程不安全。看上去,这段代码没什么明显问题,但它不是线程安全的。假设当前有N个线程同时调用getInstance()方法,由于当前还没有对象生成,所以一部分同时都进入if语句new Singleton(),那么就会由多个线程创建多个多个user对象。
2.4.3 线程安全的懒汉式
public class Singleton {
private static Singleton singleton;
private Singleton(){};
private static synchronized Singleton getSingleton(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
说明:
① 优点:解决了懒汉式线程不安全的问题
② 缺点:线程阻塞,影响性能。
2.4.4 DCL单例 - 高性能的懒汉式
public class Singleton {
/*volatile在这里发挥的作用是:禁止指令重排序(编译器和处理器为了优化程序性能
* 而对指令序列进行排序的一种手段。)
* singleton = new Singleton();这句代码是非原子性操作可分为三行伪代码
* a:memory = allocate() //分配内存,在jvm堆中分配一段区域
* b:ctorInstanc(memory) //初始化对象,在jvm堆中的内存中实例化对象
* c:instance = memory //赋值,设置instance指向刚分配的内存地址
* 上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。
* 重排序是为了优化性能,但是不管怎么重排序,在单线程下程序的执行结果不能被改变
* 保证最终一致性。而在多线程环境下,可能发生重排序,会影响结果。
* ①若A线程执行到代码singleton = new Singleton()时;
* ②同时若B线程进来执行到代码到第一层检查if (singleton == null)
* ③当cpu切换到A线程执行代码singleton = new Singleton();时发生了指令重排序,
* 执行了a-b,没有执行c,此时的singleton对象只有地址,没有内容。然后cpu又切换到了B线程,
* 这时singleton == null为false(==比较的是内存地址),
* 则代码会直接执行到了return,返回一个未初始化的对象(只有地址,没有内容)。
* */
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton() {
/*第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
* ①当多个线程第一次进入,所有线程都进入if语句
* ②当多个线程第二次进入,因为singleton已经不为null,因此所有线程都不会进入if语句,
* 即不会执行锁,从而也就不会因为锁而阻塞,避免锁竞争*/
if (singleton == null) {
/*第一层锁,保证只有一个线程进入,
* ①多个线程第一次进入的时候,只有一个线程会进入,其他线程处于阻塞状态
* 当进入的线程创建完对象出去之后,其他线程又会进入创建对象,所以有了第二次if检查
* ②多个线程第二次是进入不到这里的,因为已被第一次if检查拦截*/
synchronized (Singleton.class) {
/*第二层检查,防止除了进入的第一个线程的其他线程重复创建对象*/
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
说明:代码注释已详细讲解volatile在该单例模式的作用,已经双重锁的作用。
① 优点:解决了线程阻塞的问题。
② 缺点:多个线程第一次进入的时候会造成大量的线程阻塞,代码不够优雅。
静态内部类的方式
public class Singleton {
private Singleton(){}
private static class LayzInner{
private static Singleton singleton = new Singleton();
}
public static Singleton getSingleton(){
return LayzInner.singleton;
}
}
说明:
① 优点:第一次类创建的时候加载,避免了内存浪费,不存在阻塞问题,线程安全,唯一性
② 缺点:序列化-漏洞:反射,会破坏内部类单例模式
2.5 建造者模式
经常碰见的 XxxBuilder 的类,通常都是建造者模式的产物。建造者模式其实有很多的变种,但是对于客户端来说,我们的使用通常都是一个模式的:
Food food = new FoodBuilder().a().b().c().build();
Food food = Food.builder().a().b().c().build();
套路就是先 new 一个 Builder,然后可以链式地调用一堆方法,最后再调用一次 build() 方法,我们需要的对象就有了。
来一个中规中矩的建造者模式:
class User {
// 下面是“一堆”的属性
private String name;
private String password;
private String nickName;
private int age;
// 构造方法私有化,不然客户端就会直接调用构造方法了
private User(String name, String password, String nickName, int age) {
this.name = name;
this.password = password;
this.nickName = nickName;
this.age = age;
}
// 静态方法,用于生成一个 Builder,这个不一定要有,不过写这个方法是一个很好的习惯,
// 有些代码要求别人写 new User.UserBuilder().a()...build() 看上去就没那么好
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder {
// 下面是和 User 一模一样的一堆属性
private String name;
private String password;
private String nickName;
private int age;
private UserBuilder() {
}
// 链式调用设置各个属性值,返回 this,即 UserBuilder
public UserBuilder name(String name) {
this.name = name;
return this;
}
public UserBuilder password(String password) {
this.password = password;
return this;
}
public UserBuilder nickName(String nickName) {
this.nickName = nickName;
return this;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
// build() 方法负责将 UserBuilder 中设置好的属性“复制”到 User 中。
// 当然,可以在 “复制” 之前做点检验
public User build() {
if (name == null || password == null) {
throw new RuntimeException("用户名和密码必填");
}
if (age <= 0 || age >= 150) {
throw new RuntimeException("年龄不合法");
}
// 还可以做赋予”默认值“的功能
if (nickName == null) {
nickName = name;
}
return new User(name, password, nickName, age);
}
}
}
核心是:先把所有的属性都设置给 Builder,然后 build() 方法的时候,将这些属性复制给实际产生的对象。
看看客户端的调用:
public class APP {
public static void main(String[] args) {
User d = User.builder().name("foo").password("pAss12345").age(25).build();
}
}
说实话,建造者模式的链式写法很吸引人,但是,多写了很多“无用”的 builder 的代码,感觉这个模式没什么用。不过,当属性很多,而且有些必填,有些选填的时候,这个模式会使代码清晰很多。我们可以在 Builder 的构造方法中强制让调用者提供必填字段,还有,在 build() 方法中校验各个参数比在 User 的构造方法中校验,代码要优雅一些。
题外话,强烈建议读者使用 lombok,用了 lombok 以后,上面的一大堆代码会变成如下这样:
@Builder
class User {
private String name;
private String password;
private String nickName;
private int age;
}
当然,如果你只是想要链式写法,不想要建造者模式,有个很简单的办法,User 的 getter 方法不变,所有的 setter 方法都让其 return this 就可以了,然后就可以像下面这样调用:
User user = new User().setName("").setPassword("").setAge(20);
很多人是这么用的,但是这种写法不太优雅,不是很推荐使用。
2.6 原型模式
在某些情况下,需要创建对象的副本,但复制一个对象的成本可能很高,或者希望避免与对象的具体类耦合。例如,当创建对象的过程较为复杂,或者对象包含大量共享的状态时,使用常规的创建方法可能会导致性能下降。
原型模式的解决方案是通过复制现有对象来创建新对象,而不是从头开始构建。这允许我们以更高效的方式创建新对象,同时避免了与对象类的直接耦合。核心概念是在原型对象的基础上进行克隆,使得新对象具有与原型相同的初始状态。
原型模式很简单:有一个原型实例,基于这个原型实例产生新的实例,也就是“克隆”了。
Object 类中有一个 clone() 方法,它用于生成一个新的对象,当然,如果我们要调用这个方法,java 要求我们的类必须先实现 Cloneable 接口,此接口没有定义任何方法,但是不这么做的话,在 clone() 的时候,会抛出 CloneNotSupportedException 异常。
protected native Object clone() throws CloneNotSupportedException;
注意事项:
- 深克隆问题:原型模式默认进行浅克隆,即复制对象本身和其引用。如果对象内部包含其他对象的引用,可能需要实现深克隆来复制整个对象结构。
- 克隆方法的实现:某些对象可能不容易进行克隆,特别是涉及到文件、网络连接等资源的情况。
Java 的克隆是浅克隆,碰到对象引用的时候,克隆出来的对象和原对象中的引用将指向同一个对象。通常实现深克隆的方法是将对象进行序列化,然后再进行反序列化。
总之,原型模式是一种在需要创建对象副本时非常有用的设计模式,它提供了一种灵活且高效的方法来处理对象的复制需求。
2.7 创建型模式总结
创建型模式总体上比较简单,它们的作用就是为了产生实例对象,算是各种工作的第一步了,因为我们写的是面向对象的代码,所以我们第一步当然是需要创建一个对象了。
总结:
- 工厂模式在简单工厂模式的基础上增加了选择工厂的维度,需要第一步选择合适的工厂;
- 抽象工厂模式有产品族的概念,如果各个产品是存在兼容性问题的,就要用抽象工厂模式;
- 单例模式为了保证全局使用的是同一对象,一方面是安全性考虑,一方面是为了节省资源;
- 建造者模式专门对付属性很多的那种类,为了让代码更优美;
- 原型模式用得比较少,了解和 Object 类中的 clone() 方法相关的知识即可。
后续待更新......