设计模式初体验(一)— 创建型

本文最后更新于:1 分钟前

工厂模式

工厂模式的本质就是对获取对象过程的抽象。

简单工厂模式

简单工厂模式又称为静态工厂方法模型,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

对于产品种类相对较少的情况,考虑使用简单工厂模式可以很方便地创建所需产品:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//抽象产品类
public interface Fruit {
public abstract void eat();
}
//具体产品类:Apple
public class Apple implements Fruit {
public void eat(){
System.out.println("Eat Apple");
}
}
//具体产品类:Orange
public class Orange implements Fruit {
public void eat(){
System.out.println("Eat Orange");
}
}
//工厂类:Factory
public class Factory {
public static Fruit getInstance(String name) {
if (name.equals("Apple")){
return new Apple();
}else if (name.equals("Orange")){
return new Orange();
}
return null;
}
}
//测试客户端类:Client
public class Client {
public static void main(String[] a) {
Fruit f=Factory.getInstance("Apple");
if(f!=null){
f.eat();
}
}
}

通过反射来获取创建对象的类会更加直观,此时增加产品也不需要每次都对工厂类进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//抽象产品类
public interface Fruit {
public abstract void eat();
}
//具体产品类:Apple
public class Apple implements Fruit {
public void eat(){
System.out.println("Eat Apple");
}
}
//具体产品类:Orange
public class Orange implements Fruit {
public void eat(){
System.out.println("Eat Orange");
}
}
//工厂类:Factory
public class Factory {
public static Fruit getInstance(String ClassName) {
Fruit f=null;
try {
f=(Fruit)Class.forName(ClassName).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return f;
}
}
//测试客户端类:Client
public class Client {
public static void main(String[] a) {
Fruit f=Factory.getInstance("*.Apple"); // *.Apple 代表的是Apple类的全限定类名
if(f!=null){
f.eat();
}
}
}

但是方法参数是字符串,可控性有待提升,可以改为以下通过类方法newInstance直接创建对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//工厂类:Factory
public class Factory {
public static Fruit getInstance(Class<? extends Fruit> clazz) {
try {
if (clazz != null) {
return clazz.newInstance();
}
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
//测试客户端类:Client
public class Client {
public static void main(String[] a) {
Fruit f=Factory.getInstance(Apple.class);
if(f!=null){
f.eat();
}
}
}

在客户端 Client类 中通过switch方法创建相应类型的 Fruit 是完全没有问题的。为什么要单独分出来一个工厂类来进行对象的创建呢?

这是因为要考虑到代码的可修改性与可读性。在不使用简单工厂的客户端代码中,如果增加一种类型的书,需要修改其中的switch语句,而其后的操作是不需要修改的。针对代码优化进行解耦,就要分离其中的变化部分以及不变的部分。当不使用简单工厂类进行创建对象时,需要从客户端中使用switch的代码中直接修改。如果主函数中使用switch的代码地方过多,需要全部修改,这就很容易漏掉某个地方,忘记修改,从而造成程序错误。简单工厂模式就是将这部分创建对象语句分离出来,由这个类来封装实例化对象的行为,修改时只需要修改类中的操作代码,使用时调用该类不需要考虑实例化对象的行为,使得后期代码维护升级更简单方便。这其实也是在对每处创建对象这一高耦合性的行为进行解耦。

优点

结构简单,调用方便。工厂和产品的指责区分明确。

缺点

工厂类很单一,同时负责所有产品的创建,当产品基数过多时,工厂类会非常臃肿,违背高聚合原则。

工厂方法模式

工厂方法模式指定义一个创建对象的接口,但由实现这个接口的类来决定是梨花哪个类

工厂方法模式适用于以下场景:

  1. 创建对象需要大量重复的代码
  2. 客户端不依赖产品类实例如何被创建、实现等细节
  3. 一个类通过其子类来指定创建哪个对象

以下是工厂模式的通用写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//抽象产品类:Fruit
public interface Fruit {
void eat();
}
//具体产品类:Apple
public class Apple implements Fruit{
@Override
public void eat(){
System.out.println("Eat Apple");
}
}
//具体产品类:Orange
public class Orange implements Fruit {
@Override
public void eat(){
System.out.println("Eat Orange");
}
}
//抽象工厂类:Factory
public interface Factory {
Fruit getInstance();
}
//具体工厂类(生产Apple对象):AppleFactory
public class AppleFactory implements Factory{
@Override
public Fruit getInstance(){
return new Apple();
}
}
//具体工厂类(生产Orange对象):OrangeFactory
public class OrangeFactory implements Factory{
@Override
public Fruit getInstance(){
return new Orange();
}
}
//测试客户端类:Client
public class Client {
public static void main(String[] args) {
//通过AppleFactory实现工厂接口
Factory factoryApple = new AppleFactory();
factoryApple.getInstance().eat();
//通过OrangeFactory实现工厂接口
Factory factoryOrange = new OrangeFactory();
factoryOrange.getInstance().eat();
}
}

优点

  1. 灵活性强,对于新产品的创建,只需要多写一个相应的工厂类
  2. 是典型的解耦框架。高层模块只需要知道产品的抽象类,无需关心其他实现类(满足迪米特法则、依赖倒置原则和里氏替换原则)

缺点

  1. 类的个数容易过多(指每增加一种产品就会多一个产品相关的工厂),增加复杂度
  2. 增加了系统的抽象性和理解难度
  3. 抽象产品只能生产一种产品(这个可以通过抽象工厂解决)

抽象工厂模式

抽象工厂模式指提供一个创建一系列相关或依赖对象的接口,无需指定他们具体的类。(意思是客户端不必指定产品的具体类型,创建多个产品族中的产品)

产品等级结构:产品等级结构就是产品的继承结构。比如一个抽象类是水果,它的子类有苹果和橙子。抽象水果和具体水果就构成了一个产品等级结构——抽象水果是父类,具体水果是其子类。

产品族:产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品。比如苹果工厂生产的苹果和苹果汁,或者橙子工厂生产的橙子和橙汁。

抽象工厂模式的通用写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//抽象产品类:Fruit
public interface Fruit {
void eat();
}
//具体产品类:Apple
public class Apple implements Fruit{
@Override
public void eat(){
System.out.println("Eat Apple");
}
}
//具体产品类:Orange
public class Orange implements Fruit{
@Override
public void eat(){
System.out.println("Eat Orange");
}
}
//抽象产品类:FruitJuice
public interface FruitJuice {
void drink();
}
//具体产品类:AppleJuice
public class AppleJuice implements FruitJuice{
@Override
public void drink(){
System.out.println("Drink AppleJuice");
}
}
//具体产品类:AppleJuice
public class OrangeJuice implements FruitJuice{
@Override
public void drink(){
System.out.println("Drink OrangeJuice");
}
}
//抽象工厂类:Factory
public interface Factory {
Fruit getFruit();
FruitJuice getFruitJuice();
}
//具体工厂类(生产Apple的产品族):AppleFactory
public class AppleFactory implements Factory{
@Override
public Fruit getFruit(){
return new Apple();
}
@Override
public FruitJuice getFruitJuice(){
return new AppleJuice();
}
}
//具体工厂类(生产Orange的产品族):AppleFactory
public class OrangeFactory implements Factory{
@Override
public Fruit getFruit(){
return new Orange();
}
@Override
public FruitJuice getFruitJuice(){
return new OrangeJuice();
}
}
//测试客户端类:Client
public class Client {
public static void main(String[] args) {
//通过AppleFactory实现工厂接口
Factory appleFactory = new AppleFactory();
//通过OrangeFactory实现工厂接口
Factory orangeFactory = new OrangeFactory();
//生产Apple的不同产品族
appleFactory.getFruit().eat();
appleFactory.getFruitJuice().drink();
//生产Orange的不同产品族
orangeFactory.getFruit().eat();
orangeFactory.getFruitJuice().drink();
}
}

优点

  1. 如果需要增加产品族时,抽象工厂可以保证客户端始终只使用同一个产品的产品族
  2. 增加新产品族时,只需实现一个新的具体工厂就行,不需要对已有代码进行修改

缺点

  1. 规定了所有可能被创建的产品集合,产品族中扩展新的产品就比较困难,需要修改抽象工厂的接口
  2. 增加了系统的抽象性和理解难度

单例模式

饿汉式

饿汉式单例写法在类加载的时候立即初始化,并且创建单例对象。它绝对线程安全。

1
2
3
4
5
6
7
8
9
10
//饿汉式单例模式
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 LazySimpleSingleton {
private static LazySimpleSingleton instance;
private LazySimpleSingleton(){}
public static LazySimpleSingleton getInstance(){
if(instance == null){
instance = new LazySimpleSingleton();
}
return instance;
}
}

懒汉式(线程安全)

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

这种写法能够在多线程中很好的工作,但是每次调用getInstance方法时都需要进行同步,造成不必要的同步开销,而且大部分时候我们是用不到同步的,所以不建议用这种模式。

双重检查模式(DCL)

这个就非常重要了,面试的时候考的最多的就是这个单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton(){}

public static LazyDoubleCheckSingleton getInstance(){
// 两次检查
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}

当第一个线程调用getInstance()方法时,第二个线程也可以调用。当第一个线程执行到synchronized时会上锁,第二个线程就会变成 MONITOR 状态,出现阻塞。此时,阻塞并不是基于整个 LazyDoubleCheckSingleton 类的阻塞,而是在 getInstance() 方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感觉不到。

枚举单例模式

上述讲的几种单例模式实现中,有一种情况下他们会重新创建对象,那就是反序列化,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 定义饥饿式单例模式可序列化
public class HungrySingleton implements Serializable {
private static final HungrySingleton instance = new HungrySingleton();

private HungrySingleton(){}

public static HungrySingleton getInstance() {
return instance;
}
}

// 测试单例模式序列化之后再反序列化读取时是否还是单例
public class EnumSingletonTest {
public static void main(String[] args) {
try {
HungrySingleton instance1 = null;

HungrySingleton instance2 = HungrySingleton.getInstance();

FileOutputStream fos = new FileOutputStream("HungrySingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();

FileInputStream fis = new FileInputStream("HungrySingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (HungrySingleton) ois.readObject();
ois.close();

System.out.println("before singleton: " + instance2);
System.out.println("after singleton: " +instance1);
System.out.println("compare two singletons: " +instance2.equals(instance1));

}catch (Exception e) {
e.printStackTrace();
}
}
}

运行结果为:

1
2
3
before singleton: javaBase.DesignModel.SigletonModel.HungrySingleton@12a3a380
after singleton: javaBase.DesignModel.SigletonModel.HungrySingleton@27d6c5e0
compare two singletons: false

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,所以我们可以通过枚举类来实现单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 枚举单例模式类
public enum EnumSingleton {
INSTANCE;
private Object data;

public Object getData(){
return data;
}

public void setData(Object data){
this.data = data;
}

public static EnumSingleton getInstance(){
return INSTANCE;
}
}

// 枚举单例模式测试类
public class EnumSingletonTest {
public static void main(String[] args) {
try {
EnumSingleton instance1 = null;

EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());

FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();

FileInputStream fis = new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (EnumSingleton) ois.readObject();
ois.close();

System.out.println("before singleton: " + instance2.getData());
System.out.println("after singleton: " +instance1.getData());
System.out.println("compare two singletons: " +instance2.getData().equals(instance1.getData()));

}catch (Exception e) {
e.printStackTrace();
}
}
}

运行结果为:

1
2
3
before singleton: java.lang.Object@4f3f5b24
after singleton: java.lang.Object@4f3f5b24
compare two singletons: true

优点:

  1. 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  2. 可以避免对资源的多重占用。
  3. 单例模式设置全局访问点,可以优化和共享资源的访问。

缺点:

  1. 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  2. 可以避免对资源的多重占用。
  3. 单例模式设置全局访问点,可以优化和共享资源的访问。

原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//抽象原型类:Fruit
public interface Fruit<T> {
T clone();
}
//具体产品类:Apple
public class Apple implements Fruit<Apple>{
private String name;

public Apple(String name){
this.name = name;
}

@Override
public Apple clone() {
return new Apple(this.name);
}

@Override
public String toString(){
return "Apple{" +
"name = " + this.name +
"}";
}
}
//具体产品类:Orange
public class Orange implements Fruit<Orange> {
private String name;

public Orange(String name){
this.name = name;
}

@Override
public Orange clone() {
return new Orange(this.name);
}

@Override
public String toString(){
return "Orange{" +
"name = " + this.name +
"}";
}
}
//测试客户端类:Client
public class Client {
public static void main(String[] args) {
//创建原型对象
Apple applePrototype = new Apple("apple");
System.out.println(applePrototype.toString());
Apple apple1 = applePrototype.clone();
System.out.println(apple1.toString());
System.out.println(applePrototype.equals(apple1));
}
}

优点

  1. java自带的原型模式是基于内存二进制流的复制,性能上比直接new一个对象更优。
  2. 可以使用深克隆方式来保存对象的状态:使用原型模式将对象复制一份,并将其状态保存起来,这种操作简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可以辅助实现撤销操作。

缺点

  1. 需要为每一个类都配置一个clone方法
  2. clone方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则
  3. 实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来就会很麻烦。

建造者模式

建造者模式将一个复杂对象的构建过程和它的表示进行分离,使得同样的构建过程可以创建不同的表示。

构建代表创建一个对象,表示代表对象的行为、方法。

对应到代码其实就是用接口规定行为,然后由具体的实现类进行构建。

如果把我们买车的时候定义的车辆配置(低配、标配、高配,还有可能增配、减配)使用建造者模式来表现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//产品实体类:Car
@Data
public class Car {
private String brand;
private String type;
private String engine;
private int seat;

@Override
public String toString(){
return "Product - " + "brand: " + brand + " type: " + type + " engine: " + engine + " seat: " + seat;
}
}
//抽象建造者接口:Builder
public interface Builder {
Car builder();
}
//建造者实现类:CarBuilder
public class CarBuilder implements Builder{
private Car product = new Car();

@Override
public Car builder(){
return product;
}
//将复杂的对象创建过程封装起来,具体创建步骤由用户进行决定
public CarBuilder addBrand(String brand){
product.setBrand(brand);
return this;
}

public CarBuilder addType(String type){
product.setType(type);
return this;
}

public CarBuilder addEngine(String engine){
product.setEngine(engine);
return this;
}

public CarBuilder addSeat(int seat){
product.setSeat(seat);
return this;
}
}
//测试客户端类
public class Client {
public static void main(String[] args) {
Builder builder = new CarBuilder()
.addBrand("Benz")
.addType("AMG")
.addEngine("V8")
.addSeat(2);
System.out.println(builder.builder());
}
}

建造者模式 vs 工厂模式

  1. 建造者模式更加注重方法的调用顺序,工厂模式注重创建对象。
  2. 建造者模式能创建复杂的对象,由各种复杂的部件组成;工厂模式创建出来的对象都一样。
  3. 它们关注的重点不一样。工厂模式只要把对象创建出来就行了,而建造者模式不仅要创建出对象,还要知道对象由哪些部件构成。
  4. 建造者模式根据建造过程中的顺序不一样,最终的对象部件组成也不一样。

优点

  1. 封装性好,构建和表示分离
  2. 可拓展性比较好,建造类之间独立,在一定程度上解耦
  3. 便于控制细节,建造者可以对创建过程进行逐步细化,而不对其他模块产生影响

缺点

  1. 需要多创建一个 Builder 对象 (建造者对象)
  2. 如果产品类内部发生变化,则建造者也要同步修改,后期维护成本较高