单例模式(Singleton)
单例模式是设计模式中最简单也是最好理解的一个模式,它确保一个类只有一个实例,并且这个实例对外开放,可以被其他类调用使用。单例的核心在于如何创建单例,创建单例的方式常用的有4种,这里因为篇幅原因仅使用静态内部类的方式来实现单例,因为它保证了懒加载并且避免了同步开销。想知道其他3种创建方式和进一步了解单例的可以查看这篇文章:手写单例模式以及保证安全性
首先我们先来看一下使用静态内部类实现单例的代码:
// 单例创建对象 音乐类
public class Music {
// 私有化构造方法,防止外部实例化
private Music() {}
private static class Handler {
private static final Music INSTANCE = new Music();
}
public static Music getInstance() {
return Handler.INSTANCE;
}
}
// 客户端代码
public class Main {
public static void main(String[] args) {
Music music1 = Music.getInstance();
Music music2 = Music.getInstance();
System.out.println(music1);
System.out.println(music2);
}
}
// 控制台输出
-- com.sora.Music@72ea2f77
-- com.sora.Music@72ea2f77
我们以音乐类举例,需要私有化构造方法,防止外部创建对象。继续在内部创建一个静态内部类,我们都知道静态成员(如静态变量和静态方法)随着类的加载而加载,而静态内部类的加载是延迟的,只有在被显式调用时才会加载。因此,静态内部类不仅实现了懒加载,而且避免了同步开销,同时又保证了线程安全。
这里继续说点跟单例相关的几个问题,感兴趣的可以看看:
在Spring框架中,Bean默认是单例的,这是基于Spring容器级别的单例,它的生命周期和Spring容器生命周期绑定,而我们通过Java创建的单例对象则是JVM级别,会在整个应用程序的JVM生命周期内存在。
Spring中,我们可以通过 @Scope("Prototype") 将默认的单例创建改为原型创建。还有request、session等,这些都用的不多
使用单例对象时,我们的重点肯定是线程安全性。假设我们有一个类负责累加一个数并且返回,如果此时多个线程都调用了这个类,而这个类又是由Spring管理且是单例的,那么Spring如何保证线程安全呢?答案是Spring并不会保证多线程下的线程安全。因此多线程环境下,如果该单例对象存在可变状态,就需要我们手动处理线程安全问题,常见的几种方法如下:
1. 无状态设计:无状态表示该对象不包含任何可变的实例对象,每次调用方法时,所有操作都依赖参数进行处理
2. 使用局部变量:如果我们要保存一个临时结果,不要使用全局变量!使用**局部变量**!因为局部变量是线程私有的,线程之间不会共享。
3. ThreadLocal:ThreadLocal为每一个线程都提供了一个副本,实现了线程间的隔离
4. 如果这个类必须要使用全局变量保存一些数据,那么必须使用线程安全的类,例如**ConcurrentHashMap**
适用场景:缓存类的实现、线程池管理、全局ID生成器、数据库连接池等等
工厂方法(Factory Method)
在讲解工厂方法之前,我们先来了解一下简单工厂,简单工厂是由一个接口、多个接口实现类以及一个工厂类组成,我们以游戏平台举例,平台就是一个接口,而Steam、Epic等就是具体的实现类,工厂类则是负责创建实现类的地方。我们具体来看代码:
// 接口类
public interface Platform {
void print();
}
// 第一个实现类
public class Epic implements Platform {
@Override
public void print() {
System.out.println("我是Epic平台");
}
}
// 第二个实现类
public class Origin implements Platform {
@Override
public void print() {
System.out.println("我是橘子平台");
}
}
// 第三个实现类
public class Steam implements Platform {
@Override
public void print() {
System.out.println("我是Steam平台");
}
}
// 工厂类
public class GamePlatform {
public Platform getPlatform(String platform) {
if ("Epic".equalsIgnoreCase(platform)) {
return new Epic();
} else if ("Steam".equalsIgnoreCase(platform)) {
return new Steam();
} else if ("Origin".equalsIgnoreCase(platform)) {
return new Origin();
}
return null;
}
}
// 使用简单工厂
public class Main {
public static void main(String[] args) {
String param = "steam";
// 创建工厂类实例
GamePlatform gamePlatform = new GamePlatform();
Platform steam = gamePlatform.getPlatform(param);
steam.print();
param = "epic";
Platform epic = gamePlatform.getPlatform(param);
epic.print();
}
}
// 控制台输出
-- 我是Steam平台
-- 我是Epic平台
如果我们不使用简单工厂来完成这个案例,我们会直接new Steam(), new Epic()来完成对象的创建,那我们为什么要把创建对象的任务交给工厂类呢?因为简单工厂提供了解耦、维护性和灵活性。如果我们不使用简单工厂,那么当一个对象的构造逻辑发生改变,例如需要传入一些固定参数,我们需要在每一个用到的地方去修改并测试,但如果是简单工厂,我们直接在工厂类内进行修改即可,这便是解耦和维护。灵活性则表现在扩展,如果后续我们需要新加平台,没有使用简单工厂的情况下,我们需要去每一个地方去手动新增一个平台,而简单工厂只需要新增一个实现类,随后在工厂类内新加一个条件即可,通过参数来控制工厂类获取的具体实现对象。但这样破坏了开闭原则(对扩展开放,对修改关闭),所以工厂方法就这样出来了。
工厂方法在简单工厂的基础上做了一些修改,将原本一个工厂拆开了,每个对象都有属于自己的工厂。工厂方法共有四个角色:
- 产品接口:定义所有产品的行为
- 产品实现类:实现产品接口,负责具体的产品实现
- 抽象工厂:所有工厂实现类的父类,声明返回产品对象的工厂方法,返回值必须是产品接口
- 工厂实现类:继承抽象工厂类,重写返回产品对象工厂的方法,使其返回不同类型的产品
下面我们以创建不同的支付对象举例,代码如下:
// 支付接口
public interface Pay {
void pay();
}
// 支付宝支付类
public class AliPay implements Pay {
@Override
public void pay() {
System.out.println("用户使用AliPay支付,开始执行具体逻辑……");
}
}
// 微信支付类
public class WechatPay implements Pay {
@Override
public void pay() {
System.out.println("用户使用微信支付,开始执行具体逻辑……");
}
}
到目前为止都是跟简单工厂一样,工厂部分可以看到阿里云和微信都有对应的工厂且继承了一个抽象类,这个抽象类就是负责声明一个创建工厂的方法。具体工厂代码如下:
// 支付工厂 所以支付对象工厂都要继承 相当于父类
public abstract class PayFactory {
public abstract Pay getPayType();
}
// 阿里云支付工厂
public class AliPayFactory extends PayFactory {
@Override
public Pay getPayType() {
return new AliPay();
}
}
// 微信支付工厂
public class WechatPayFactory extends PayFactory {
@Override
public Pay getPayType() {
return new WechatPay();
}
}
之后通过参数来控制工厂的创建并调用支付方法,以后要新增支付方法,只需要实现一个新的支付类,创建一个新的支付工厂即可,解决了简单工厂的弊端(新增内容需要修改工厂类),符合开闭原则。下面是具体的客户端代码:
public class Main {
public static void main(String[] args) {
// 支付参数
String param = "ali";
PayFactory factory = null;
if ("wechat".equals(param)) {
factory = new WechatPayFactory();
} else if ("ali".equals(param)) {
factory = new AliPayFactory();
}
Pay payType = factory.getPayType();
payType.pay();
}
}
// 控制台输出
-- 用户使用AliPay支付,开始执行具体逻辑……
适用场景:开发与线上环境的快速切换,数据库连接类型,多类型文件生成与读取等等
建造者(Builder)
建造者模式更注重构建对象的这个过程,通过分步创建一个复杂的对象,将产品的创建与产品的本身进行分离,构建的过程就可以获得不同的对象。在代码中使用链式调用,方便的同时增加了可读性。我们一般通过建造者模式来创建那些有非传参数的对象或者参数过多的对象。
相信大家肯定见过类似于下面这样的代码,一个类的构造函数有多个可选参数,为了保证正常调用会写多个方法来进行重载,在这种情况下,我们就可以使用建造者的设计模式来完成。
public class Computer {
Computer(String cpu) { …… }
Computer(String cpu, String gpu) { …… }
Computer(String cpu, String gpu, String board) { …… }
这里我们假设cpu、gpu、board和arm是必传参数。使用建造者模式:
public class Computer {
private String cpu;
private String gpu;
private String board;
private String arm;
private String ssd;
private String power;
public Computer(ComputerBuilder computerBuilder) {
this.cpu = computerBuilder.cpu;
this.gpu = computerBuilder.gpu;
this.board = computerBuilder.board;
this.arm = computerBuilder.arm;
this.ssd = computerBuilder.ssd;
this.power = computerBuilder.power;
}
public static class ComputerBuilder{
private final String cpu;
private final String gpu;
private final String board;
private final String arm;
private String ssd;
private String power;
public ComputerBuilder(String cpu, String gpu, String board, String arm) {
this.cpu = cpu;
this.gpu = gpu;
this.board = board;
this.arm = arm;
}
public ComputerBuilder setSsd(String ssd) {
this.ssd = ssd;
return this;
}
public ComputerBuilder setPower(String power) {
this.power = power;
return this;
}
public Computer build() {
return new Computer(this);
}
}
@Override
public String toString() {
return "Computer{" +
"cpu='" + cpu + ''' +
", gpu='" + gpu + ''' +
", board='" + board + ''' +
", arm='" + arm + ''' +
", ssd='" + ssd + ''' +
", power='" + power + ''' +
'}';
}
}
// 客户端代码
public class Main {
public static void main(String[] args) {
Computer computerBuilder = new Computer
.ComputerBuilder("7800x", "7900xtx", "B660M", "32G")
.build();
Computer computerBuilder2 = new Computer
.ComputerBuilder("7800x", "7900xtx", "B660M", "32G")
.setSsd("2T")
.setPower("1000w")
.build();
System.out.println(computerBuilder);
System.out.println(computerBuilder2);
}
}
// 控制台输出
-- Computer{cpu='7800x', gpu='7900xtx', board='B660M', arm='32G', ssd='null', power='null'}
-- Computer{cpu='7800x', gpu='7900xtx', board='B660M', arm='32G', ssd='2T', power='1000w'}
电脑类的所有属性都要经过ComputerBuilder来完成,并且在内部配置了必选参数和可选参数,必选参数保证了对象创建的完整性同时避免了构造函数爆炸的情况,可选参数带来了扩展性与灵活性,可以随时新增字段而不影响现有代码,链式调用又直观体现了该对象的整体结构
适用场景:复杂对象的创建,数据库连接参数配置,http连接参数的配置等等
抽象工厂(Abstract Factory)
在讲抽象工厂前强烈建议先把工厂方法搞明白,会好理解很多。下面我们开始学习抽象工厂。
学习抽象工厂前我们要先了解2个概念,产品族和产品等级结构
产品族:是指一组相关联的产品,它们一起协作或具有某种共同属性。以麦当劳举例,麦当劳生产的汉堡、薯条、可乐可以被看作一个产品族,所有这些产品都属于麦当劳的风格。
产品等级结构:指产品的不同种类或类型的层次关系。比如,汉堡、薯条、饮料这些都是“快餐产品”的不同类型,它们代表了产品的不同等级结构。
下面继续说说抽象工厂的角色,总体上,抽象工厂的角色与工厂方法类似,但具体的职责会有点不同:
- 工厂方法侧重为单个产品提供接口,每个工厂只会生产一个产品
- 抽象工厂则为产品族提供接口,每个工厂可以生产同一产品族下的所有产品类型
抽象工厂中主要有以下角色:
- 产品接口:为每种产品声明接口
- 产品实现类:实现产品接口,负责具体的产品实现
- 抽象工厂:所有工厂实现类的父类,声明创建了一系列相关产品的接口
- 工厂实现类:继承抽象工厂类,负责创建具体的产品
抽象工厂模式的核心思想是为产品族提供一个创建接口。一个工厂不但负责创建一类产品(比如汉堡),还要为同一个产品族中的其他相关产品提供创建方式(如薯条、饮料)。通过抽象工厂模式,我们可以根据具体的需求生成同一产品族中的相关产品,而不必关心每个产品的具体实现细节,同时也隔离了具体类,客户端只通过接口来与产品交互,不直接依赖具体产品类,做到了解耦的同时增强了扩展性。但缺点同样是因为高扩展性,我们新增一个产品等级结构时,需要修改抽象工厂类以及各个工厂的具体实现。
下面以游戏开发商举例,每个游戏开发商都会开发不同类型的游戏,比如 RPG(角色扮演)、ACT(动作)等,这些不同的游戏类型属于产品等级结构。而多个游戏开发商,如卡普空、索尼等,都能够开发这些类型的游戏,这些开发商则可以看作是产品族的不同实现。我们以卡普空公司为例,创建这两种类型的游戏:
// 动作类游戏接口 (字段:游戏名、游戏详情、上手难度)
public interface ACT {
void name();
void details();
void level();
}
// 角色扮演类游戏接口 (字段:游戏名、游戏详情)
public interface RPG {
void name();
void details();
}
// 卡普空的动作类游戏具体实现
public class MonsterHunter implements ACT {
@Override
public void name() {
System.out.println("游戏:怪物猎人");
}
@Override
public void details() {
System.out.println("游戏介绍:在怪物猎人的世界中,你将扮演一名猎人在新大陆上……");
}
@Override
public void level() {
System.out.println("上手难度:★★★★☆");
}
}
// 卡普空的角色扮演类游戏具体实现
public class ResidentEvil implements RPG {
@Override
public void name() {
System.out.println("游戏:生化危机");
}
@Override
public void details() {
System.out.println("游戏介绍:里昂接到总统特令,负责去孤岛上寻找阿什莉并将其救出,在岛上……");
}
}
// 抽象工厂类 声明所有的产品等级结构
public abstract class GameFactory {
public abstract ACT createACTGame();
public abstract RPG createRPGGame();
}
// 创建卡普空工厂,返回所有产品等级结构
public class CapComFactory extends GameFactory {
@Override
public ACT createACTGame() {
return new MonsterHunter();
}
@Override
public RPG createRPGGame() {
return new ResidentEvil();
}
}
// 客户端
public class Main {
public static void main(String[] args) {
CapComFactory capComFactory = new CapComFactory();
System.out.println("创建卡普空下的RPG游戏:");
RPG capComRPGGame = capComFactory.createRPGGame();
capComRPGGame.name();
capComRPGGame.details();
System.out.println("\n创建卡普空下的ACT游戏:");
ACT capComACTGame = capComFactory.createACTGame();
capComACTGame.name();
capComACTGame.details();
capComACTGame.level();
}
}
// 控制台输出
-- 创建卡普空下的RPG游戏:
-- 游戏:生化危机
-- 游戏介绍:里昂接到总统特令,负责去孤岛上寻找阿什莉并将其救出,在岛上……
-- 创建卡普空下的ACT游戏:
-- 游戏:怪物猎人
-- 游戏介绍:在怪物猎人的世界中,你将扮演一名猎人在新大陆上……
-- 上手难度:★★★★☆
在这个例子中,卡普空这个产品族为我们提供了两个产品等级结构的游戏:一个动作类(ACT)和一个角色扮演类(RPG)。通过抽象工厂,我们实现了不同类型游戏的创建。同时我们可以很容易地扩展另一个游戏开发商产品族:
// 索尼的动作类游戏具体实现
public class GodOfWar implements ACT {
@Override
public void name() {
System.out.println("游戏:战神");
}
@Override
public void details() {
System.out.println("游戏介绍:奎托斯与儿子成功将母亲的遗骨撒落在高山,但门外的男人究竟是……");
}
@Override
public void level() {
System.out.println("上手难度:★★☆☆☆");
}
}
// 索尼的角色扮演类游戏具体实现
public class TheLastOfUs implements RPG {
@Override
public void name() {
System.out.println("游戏:最后生还者");
}
@Override
public void details() {
System.out.println("开发商:人类因现代传染病而面临绝种危机,当环境从废墟的都市再度自然化时……");
}
}
// 创建索尼工厂,返回所有产品等级结构
public class SonyFactory extends GameFactory {
@Override
public ACT createACTGame() {
return new GodOfWar();
}
@Override
public RPG createRPGGame() {
return new TheLastOfUs();
}
}
// 客户端
public class Main {
public static void main(String[] args) {
SonyFactory sonyFactory = new SonyFactory();
System.out.println("\n创建索尼下的RPG游戏:");
RPG sonyRPGGame = sonyFactory.createRPGGame();
sonyRPGGame.name();
sonyRPGGame.details();
System.out.println("\n创建索尼下的ACT游戏:");
ACT sonyACTGame = sonyFactory.createACTGame();
sonyACTGame.name();
sonyACTGame.details();
sonyACTGame.level();
}
}
// 控制台输出
-- 创建索尼下的RPG游戏:
-- 游戏:最后生还者
-- 开发商:人类因现代传染病而面临绝种危机,当环境从废墟的都市再度自然化时……
-- 创建索尼下的ACT游戏:
-- 游戏:战神
-- 游戏介绍:奎托斯与儿子成功将母亲的遗骨撒落在高山,但门外的男人究竟是……
-- 上手难度:★★☆☆☆
可以看到,我们只需要在产品接口的基础上,完成对索尼产品的具体实现,并新增一个索尼工厂,即可获得索尼产品族下的所有产品。也就是对于扩展性而言,新增一个产品族非常简单,没有破坏开闭原则,而当新增产品等级结构时,需要修改现有的抽象工厂类以及工厂类的实现会破坏开闭原则。
适用场景:系统需要与多个产品族进行交互(例如windows与mac、跨平台应用)
原型模式(Prototype)
原型模式的核心是使用现有对象作为模板,通过克隆的方式创建新的对象。一般当创建对象的开销比较大或者对象结构比较复杂时,会使用原型模式,通过clone方法来实现对象的复制,同样我们也可以自己选择实现方式是浅拷贝还是深拷贝,下面是原型模式的角色:
- 原型接口:定义克隆方法,所有具体原型类都要实现这个接口
- 具体原型类:实现原型接口,具体定义了如何复制自身
这里再提一嘴深拷贝和浅拷贝:
- 深拷贝:复制对象以及引用类型的数据,克隆出的对象与原对象完全独立,互不影响
- 浅拷贝:只复制对象的基本数据类型,引用数据类型的引用,克隆出的对象和原对象共享同一个引用地址
如果我们使用浅拷贝,修改任一对象里的对象或集合,另一个也会发生改变,因为这个对象都指向同一个地址。
PS:String数据类型也是引用类型的数据,但因为底层是不可变的数组,所以我们修改其中一个对象另一个并不会发生改变。所以对于String类型,我们可以放心使用,因为任何修改本质上都是创建了新的对象
下面我们来看原型模式的示例代码:
// 原型接口
public interface Prototype {
Prototype clone();
}
// 原型类
public class Album implements Prototype {
public String name;
public String sings;
public ArrayList<String> list = new ArrayList<>();
public Album() {
}
public List getList() {
return list;
}
@Override
public String toString() {
return "Album{" +
"name='" + name + ''' +
", sings='" + sings + ''' +
", list=" + list +
'}';
}
public Album(String name, String sings, ArrayList<String> list) {
this.name = name;
this.sings = sings;
this.list = list;
}
// 克隆实现(浅拷贝)
@Override
public Prototype clone() {
return new Album(this.name, this.sings, this.list);
}
}
原型模式的使用很简单,只需要一个原型接口即可,内部包含一个clone方法,具体实现深拷贝还是浅拷贝则由原型类决定。这里我克隆Album类,实现原型接口后,在clone方法内创建一个新对象并填充数据返回,这样的方式是浅拷贝。我们来看一下浅拷贝的结果:
// 客户端示例
public class Main {
public static void main(String[] args) {
Album album = new Album("透明な君を掬う", "nayuta", new ArrayList(){{
add("透明な君");
add("誰そ彼");
add("追慕");
add("僕が見つけた綻び");
add("灰燼");
add("彼は誰");
add("君を掬う");
}});
System.out.println("原对象::" + album);
Album clone = (Album) album.clone();
System.out.println("克隆对象:" + clone);
System.out.println("修改克隆对象,删掉最后一首歌曲……");
clone.getList().removeLast();
System.out.println("原对象:" + album);
System.out.println("克隆对象:" + clone);
}
}
// 控制台输出
-- 原对象::Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰, 君を掬う]}
-- 克隆对象:Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰, 君を掬う]}
-- 修改克隆对象,删掉最后一首歌曲……
-- 原对象:Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰]}
-- 克隆对象:Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰]}
我们删除克隆对象集合的一个数据后,原对象也发生了相应的变更,因为他们的使用的list实际上是一个。如果要实现深拷贝,需要将原型类中的clone方法需要改成这样:
// 这里我通过序列化和反序列化的方式实现了深拷贝,但注意对象必须实现 Serializable 接口
// 克隆实现(深拷贝)
public Prototype clone() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Prototype) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
改为深拷贝后,只有克隆对象的集合数据发生了改变,原对象的集合并没有收到影响,这就是深拷贝和浅拷贝的区别
-- 原对象::Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰, 君を掬う]}
-- 克隆对象:Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰, 君を掬う]}
-- 修改克隆对象,删掉最后一首歌曲……
-- 原对象:Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰, 君を掬う]}
-- 克隆对象:Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰]}
适用场景:需要大量创建的对象、缓存对象的创建、复杂商品/报表模板的生成等等
结束语
非常感谢看到这里的人,因为23种设计模式目前只有创建型是全部写完的,剩下还在缓慢填坑,想预览的可以查看这篇博客:个人对设计模式的理解