设计模式(2)-实例生成

Singleton

简介

在程序运行的时候,通常都会生成很多实例。但是有时我们也会希望某个类在程序运行的过程中只能生成一个实例,此时就需要用到Singleton模式。当然,只要我们在编写程序的时候多加注意,确保只调用一次new,那么就可以达到只生成一个实例的目的。但是这种来自程序员本身的保障是不可靠的,而利用Singleton模式,我们能够确保在任何情况下都绝对只有一个实例,并且能够在程序上表现出只存在一个实例。

实例程序

下面我们将介绍单例模式的不同实现方式。单例模式主要分为两种实现方式,分别是饿汉式和懒汉式。第一种实现方式是饿汉式,这种方式不会存在线程安全问题,因为它是一开始就将资源创建好,在需要使用的时候直接返回资源即可。

1
2
3
4
5
6
7
public class Singleton{
private static final Singleton instance = new Singleton();
private Singleton(){};
public static Singleton getInstance(){
return instance;
}
}

第二种方式是懒汉式,这种方式是在需要使用的时候才新建资源,但是基础的实现会存在线程安全问题。下面是懒汉式的基本实现。

1
2
3
4
5
6
7
8
9
10
public class Singleton{
private static Singleton instance;
private Singleton(){};
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}

不过我们可以将其改造成线程安全版本。这种实现下,当使用的时候才去创建资源,同时由于方法使用了synchronized关键字修饰,同一个时刻只有一个线程可以调用,于是不会重复创建资源。

1
2
3
4
5
6
7
8
9
10
public class Singleton{
private static Singleton instance;
private Singleton(){};
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}

但是上面的方式还是存在缺陷的,就是无论何时,getInstance方法都只能有一个线程调用,这样实际上会造成一些效率上的问题。而事实上,在实例创建完成之后,我们就应该允许并发调用getInstance。因此有如下的双重检验锁版本,它只在创建实例的时候不允许并发,保证单例模式的正确性,而在实例创建完成之后,就允许并发调用获取实例了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton{
private static volatile Singleton instance;
private Singleton(){};
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}

Prototype

简介

在Java中,通常情况下,我们生成一个实例都是通过new关键字来完成的,这就意味着在源代码中一定会出现特定类的名称。这似乎是个很自然的结果,但是一些情况下我们不希望在源代码中出现某个特定类的名称,因为这样就意味着该源代码与这个特定类强耦合。

Prototype模式想要达到的效果就是不通过new关键字来生成实例,而是通过实例的复制来生成实例。在Prototype模式中有如下的角色:

  • Prototype:原型角色。需要声明复制现有实例创建新实例的方法
  • ConcretePrototype:具体的原型。需要实现Prototype角色,并实现具体的通过复制创建新实例的方法

示例程序

在这个示例程序中,我们的目标类是两个可以分别完成不同形式字符串的类,但是需要通过Prototype模式来对它们进行管理。在实际使用的时候,通过原型复制进行生成。

首先定义Prototype接口,这个接口表示原型。在接口中声明了两个方法,一个是use方法,这个方法与实际业务相联系,不是该设计模式中的必备。另外一个方法是createClone方法,它表示通过这个方法能够创建出一个实体类。同时Prototype接口本本身继承了Cloneable,从实现了该接口的类对应的示例可以调用clone方法来自动复制实例。

1
2
3
4
public interface Prototype extends Cloneable{
void use(String string);
Prototype createClone();
}

之后我们创建一个类ProtorypeManager用来管理各种原型。该类中有一个Map类型的成员变量,用于管理字符串和实际原型的映射。该类提供两个方法,第一个是register,用于注册原型从而进行管理;第二个方法是create,它接收一个字符串,然后根据对应的原型创建出实例。

1
2
3
4
5
6
7
8
9
10
11
12
public class PrototypeManager {
private final HashMap<String, Prototype> prototypeCases = new HashMap<>();

public void register(String name, Prototype prototype) {
prototypeCases.put(name, prototype);
}

public Prototype create(String prototypeName) {
Prototype prototype = prototypeCases.get(prototypeName);
return prototype.createClone();
}
}

接下来就到了具体的原型实现。这里实现两个原型,它们分别可以完成不同格式字符串的输出,MessageBox可以输出一个被*包裹的字符串,而UnderlinePen则可以输出一个带下划线的字符串。这两个类都需要实现Prototype接口,并实现其中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MessageBox implements Prototype {
@Override
public void use(String string) {
for (int i = 0; i < string.length() + 4; i++) {
System.out.print("*");
}
System.out.println();
System.out.println("* " + string + " *");
for (int i = 0; i < string.length() + 4; i++) {
System.out.print("*");
}
System.out.println();
}

@Override
public Prototype createClone() {
try {
return ((Prototype) clone());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UnderlinePen implements Prototype {
@Override
public void use(String string) {
System.out.println("\"" + string + "\"");
for (int i = 0; i < string.length() + 2; i++) {
System.out.print("=");
}
System.out.println();
}

@Override
public Prototype createClone() {
try {
return ((Prototype) clone());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

之后我们就可以进行Prototype模式的测试。在测试程序中,我们首先需要在管理类中注册原型,之后才能从管理类中根据原型进行实例的创建。

1
2
3
4
5
6
7
8
9
10
11
12
public void test() {
// prototype register
PrototypeManager prototypeManager = new PrototypeManager();
prototypeManager.register("messageBox", new MessageBox());
prototypeManager.register("underlinePen", new UnderlinePen());

// create instance from prototype
Prototype messageBox = prototypeManager.create("messageBox");
messageBox.use("Hello, World");
Prototype underlinePen = prototypeManager.create("underlinePen");
underlinePen.use("Hello, World");
}

说明

Prototype模式通过复制来生成实例,我们可以避免代码之间的强耦合,达到组件的更强的复用性,同时便于管理各种原型。在上面的示例程序中,我们在测试代码中手动注册了原型。事实上,我们完全可以将原型的注册过程放在PrototypeManager的构造方法中,这样在测试代码中将不会出现具体的类名称,进一步解耦了相关代码。

注意到Prototype接口继承了Cloneable接口。在Java中已经提供了用于复制实例的clone方法,但是想要调用该方法,则对应的类必须要实现java.lang.Cloneable接口。如果没有实现该接口的实例调用了clone方法,则会在运行的时候抛出CloneNotSupportedException异常。不过需要注意的是,clone方法并不是定义在Cloneable接口中,而是定义在Object Class中的。Cloneable仅仅是一个标记接口(marker interface),它只是用来标记,说明“可以使用clone方法来进行复制”。同时clone方法进行的是浅复制,当浅复制无法满足要求时,则可以重写clone方法,进行定制化的处理。

不过实例的复制流程一般来说都是一致的。像在上面的createClone方法中,我们可以看到在MessageBox和UnderlinePen中实现是一致的,因此我们完全可以在接口中提供一个默认实现,完成最基本的复制。而如果某个子类需要进行定制化的复制,则只需要覆盖接口中的默认实现即可。

Abstract Factory

在谈到工厂Factory的时候,我们都是希望通过工厂来构建出具体的实例。在一些时候,我们可能需要许多不同的工厂,这些不同的具体的工厂完成不同的具体的行为,但是又具有一定的共性。Abstract Factory模式就是对工厂的进一步抽象,在该模式中,我们会定义抽象的Factory,在该抽象类中会定义生成实例的方式,但是在其中使用的都是抽象的Item类,这里可以理解为抽象的零件。因此抽象工厂模式完成的就是构造一个抽象的工厂,该工厂利用抽象的零件构建出抽象的产品。而如果需要将这些都转为具体的,则需要提供具体的零件实现,具体的产品,具体的工厂。这种方式便于多种工厂的管理,同时增加具体的工厂也是非常容易的。如果需要新增新的具体工厂,则只需要编写对应抽象工厂、抽象零件、抽象产品的子类,并实现其中的抽象方法即可。

该模式的核心思想与Template Method基本一致,都是定义模版之后交由子类实现,只不过在Abstract Factory模式中特定定义了工厂的模版。这里不再提供相关的实例程序。


设计模式(2)-实例生成
http://example.com/2023/03/19/设计模式-2-实例生成/
作者
EverNorif
发布于
2023年3月19日
许可协议