设计模式

摘要:什么是设计模式

设计模式是经过总结、优化的,对我们经常会碰到的一些编程问题的可重用解决方案。

这里列举了三种最基本的设计模式:

  • 创建模式,提供实例化的方法,为适合的状况提供相应的对象创建方法
  • 结构化模式,通常用来处理实体之间的关系,使得这些实体能够更好地协同工作
  • 行为模式,用于在不同的实体间进行同学,为实体之间的同学提供更容易,更灵活的通信方法

创建型

0、简单工厂模式

01、普通

就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。首先看下关系图:

image-20210830203906760

Sender是接口,MailSender和SmsSender是实现这个接口的类,然后使用SendFactory创建实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SendFactory {
public Sender produce(String type){
if("mail".equals(type)){
return new MailSender();
}
else if("sms".equals(type)){
return new SmsSender();
}
else{
System.out.println("请输入正确的类型");
return null;
}
}
}
02、多个方法

是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。

image-20210830204054291

1
2
3
4
5
6
7
8
public class SendFactory2 {
public Sender produceMail(){
return new MailSender();
}
public Sender produceSms(){
return new SmsSender();
}
}
03、多个静态方法

将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。

1
2
3
4
5
6
7
8
9
public class SendFactory3 {
public static Sender produceMail(){
return new MailSender();
}

public static Sender produceSms(){
return new SmsSender();
}
}
总结

工厂模式适合:凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。

在以上的三种模式中,第一种如果传入的字符串有误,不能正确创建对象,

第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。

1、工厂方法模式(Factory Method)

简单工厂模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则。

解法:工厂方法模式,创建一个工厂接口和创建多个工厂实现类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。

image-20210830205548571

增加一个Provider接口,用于提供工厂类,共有的方法在接口中定义:

1
2
3
4
5
6
package com.lrr;

public interface Provider {
public Sender produce();
}

工厂类:

1
2
3
4
5
6
public class SendMailFactory implements Provider {
@Override
public Sender produce() {
return new MailSender();
}
}
1
2
3
4
5
6
7
public class SendSmsFactory implements Provider {
@Override
public Sender produce() {
return new SmsSender();
}
}

测试方法:

1
2
3
4
5
6
public static void test4(){
// 测试工厂模式
Provider provider = new SendMailFactory();
Sender sender = provider.produce();
sender.Send();
}

其实这个模式的好处就是,如果你现在想增加一个功能:发及时信息,则只需做一个实现类,实现Sender接口,同时做一个工厂类,实现Provider接口,就OK了,无需去改动现成的代码。这样做,拓展性较好!

2、抽象工厂模式

工厂方法模式:

  • 一个抽象产品类,可以派生出多个具体产品类。(Sender)
  • 一个抽象工厂类,可以派生出多个具体工厂类。 (Provider)
  • 每个具体工厂类只能创建一个具体产品类的实例。

抽象工厂模式:

  • 多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。(多个Sender,Speaker)
  • 一个抽象工厂类,可以派生出多个具体工厂类。
  • 每个具体工厂类可以创建多个具体产品类的实例,也就是创建的是一个产品线下的多个产品。

区别:

  • 工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个
  • 工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个
  • 工厂方法创建 “一种” 产品,他的着重点在于”怎么创建”,也就是说如果你开发,你的大量代码很可能围绕着这种产品的构造,初始化这些细节上面。也因为如此,类似的产品之间有很多可以复用的特征,所以会和模版方法相随。

抽象工厂需要创建一些列产品,着重点在于”创建哪些”产品上,也就是说,如果你开发,你的主要任务是划分不同差异的产品线,并且尽量保持每条产品线接口一致,从而可以从同一个抽象工厂继承。

所以说抽象工厂就像工厂,而工厂方法则像是工厂的一种产品生产线

3、单例模式

单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:

1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。

2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。

3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Singleton {
// TODO 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载
private static Singleton instance = null;

// 私有构造方法,防止被实例化
private Singleton(){ }

// 静态工程方法,创建实例
// 添加synchronized关键可以把对象锁住,用于解决多线程环境下的同步问题
public static synchronized Singleton getInstance(){
if (instance == null){
return new Singleton();
}
return instance;
}
// 如果该对象被用于序列化,可以保证对象在序列化前后保持一致
// 这样当JVM从内存中反序列化地"组装"一个新对象时,就会自动调用这个 readResolve方法来返回我们指定好的对象了, 单例规则也就得到了保证.
public Object readResolve(){
return instance;
}
}

getInstance方法中synchronized方法锁住的是整个对象,性能会有所下降,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,可以改成下面的形式。

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

似乎解决了之前提到的问题,将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。但是,这样的情况,还是有可能有问题的,看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例

a>A、B线程同时进入了第一个if判断

b>A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();

c>由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。

d>B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。

e>此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。

所以程序还是有可能发生错误,其实程序在运行过程是很复杂的,从这点我们就可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们对该程序做进一步优化:

1
2
3
4
5
6
7
private static class SingletonFactory{
private static Singleton instance = new Singleton();
}

public static Singleton getInstance(){
return SingletonFactory.instance;
}

实际情况是,单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton {  

/* 私有构造方法,防止被实例化 */
private Singleton() {
}

/* 此处使用一个内部类来维护单例 */
private static class SingletonFactory {
private static Singleton instance = new Singleton();
}

/* 获取实例 */
public static Singleton getInstance() {
return SingletonFactory.instance;
}

/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
public Object readResolve() {
return getInstance();
}
}
总结

synchronized关键字锁定的是对象,在用的时候,一定要在恰当的地方使用(注意需要使用锁的对象和过程,可能有的时候并不是整个对象及整个过程都需要锁)。

5、原型模式(Prototype)

原型模式虽然是创建型的模式,但是与工程模式没有关系,从名字即可看出,该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。本小结会通过对象的复制,进行讲解。在Java中,复制对象是通过clone()实现的,先创建一个原型类:

1
2
3
4
5
6
public class Prototype  implements Cloneable{
public Object clone() throws CloneNotSupportedException{
Prototype prototype = (Prototype) super.clone();
return prototype;
}
}

一个原型类,只需要实现Cloneable接口,覆写clone方法,此处的重点是super.clone()这句话,super.clone()调用的是Object的clone()方法,而在Object类中,clone()是native的。

浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。

深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。

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
class SerializableObject implements Serializable {
private static final long serialVersionUID = 1L;
}

public class Prototype2 implements Cloneable, Serializable {
private static final long serialVersionUID = 1L;
private String string;
private SerializableObject obj;

// TODO 浅复制
public Object clone() throws CloneNotSupportedException{
Prototype2 proto = (Prototype2) super.clone();
return proto;
}

// TODO 深复制
public Object deepClone() throws IOException, ClassNotFoundException{

// 写入当前对象的二进制流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);

// 读出二进流产生的新对象
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}

public String getString(){return string;}
public void setString(String string){this.string=string;}

public SerializableObject getObj(){return obj;}

public void setObj(SerializableObject obj){
this.obj = obj;
}
}

要实现深复制,需要采用流的形式读入当前对象的二进制输入,再写出二进制数据对应的对象


基于Python的设计模式实现

单例模式

1. 使用模块

Python的模块就是天然的单例模式,因为模块在第一次导入时,会生成 .pyc文件,第二次导入时,就会直接加载 .pyc文件,而不会再次执行模块代码。因此,我们只需把相关函数和数据定义在一个模块中,就可以获得一个单例对象了。如果真的想要一个单例类,可以像下面这样做:

mysingleton.py

1
2
3
4
class Singleton(object):
def foo(self):
pass
singleton = SIngleton()

将上面的代码保存在文件 mysington.py中,要使用时,直接在其他文件中导入此文件中的对象,这个对象既是单例模式的对象

1
from a import singleton
2. 使用装饰器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def Singleton(cls):
_instance = {}
def _singleton(*args,**kargs):
if cls not in _instance:
_instance[cls] = cls(*args,**kargs)
return _instance[cls]
return _singleton

@Singleton
class A(object):
a = 1
def __init__(self,x=0) -> None:
self.x = x
a1 = A(2)
a2 = A(3)
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
import time
import threading
class Singleton:
_instance_lock = threading.Lock()
def __init__(self) -> None:
time.sleep(1)

@classmethod
def instance(cls,*args,**kwargs):
# print("-->",cls)
# 用于判断对象是否包含对应的属性
if not hasattr(Singleton,"_instance"):
with Singleton._instance_lock:
if not hasattr(Singleton,"_instance"):
Singleton._instance = Singleton(*args,**kwargs)
return Singleton._instance

def task(arg):
obj = Singleton.instance()
print(obj)

for i in range(10):
t = threading.Thread(target=task,args=[i,])
t.start()

time.sleep(20)
obj = Singleton.instance()
print(obj)

这种方式实现的单例模式,使用时会有限制,以后实例化必须通过 obj = Singleton.instance()

如果用 obj=Singleton() ,这种方式得到的不是单例

4. 基于__new__方法实现(推荐使用,方便)

通过上面例子,我们可以知道,当我们实现单例时,为了保证线程安全需要在内部加入锁

我们知道,当我们实例化一个对象时,是先执行了类的__new__方法**(我们没写时,默认调用object.__new__),实例化对象;然后**再执行类的__init__方法,对这个对象进行初始化,所有我们可以基于这个,实现单例模式:

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
'''基于new方法实现'''
import threading
class Singleton:
_instance_lock = threading.Lock()

def __init__(self) -> None:
pass

def __new__(cls, *args,**kwargs):
if not hasattr(Singleton,"_instance"):
with Singleton._instance_lock:
Singleton._instance = object.__new__(cls)
return Singleton._instance

obj1 = Singleton()
obj2 = Singleton()
print(obj1,obj2)

def task(arg):
obj = Singleton()
print(obj)

for i in range(10):
t = threading.Thread(target=task,args=[i,])
t.start()

采用这种方式的单例模式,以后实例化对象时,和平时实例化对象的方法一样 obj = Singleton()


基于Java的设计模式实现

单例模式

在知道了什么是单例模式后,我想你一定会想到静态类,“既然只使用一个对象,为何不干脆使用静态类?”,这里我会将单例模式和静态类进行一个比较。

  1. 单例可以继承和被继承,方法可以被override,而静态方法不可以。
  2. 静态方法中产生的对象会在执行后被释放,进而被GC清理,不会一直存在于内存中。
  3. 静态类会在第一次运行时初始化,单例模式可以有其他的选择,即可以延迟加载。
  4. 基于2, 3条,由于单例对象往往存在于DAO层(例如sessionFactory),如果反复的初始化和释放,则会占用很多资源,而使用单例模式将其常驻于内存可以更加节约资源。
  5. 静态方法有更高的访问效率。
  6. 单例模式很容易被测试。

几个关于静态类的误解:

误解一:静态方法常驻内存而实例方法不是。

实际上,特殊编写的实例方法可以常驻内存,而静态方法需要不断初始化和释放。

误解二:静态方法在堆(heap)上,实例方法在栈(stack)上。

实际上,都是加载到特殊的不可写的代码内存区域中。

静态类和单例模式情景的选择:

情景一:不需要维持任何状态,仅仅用于全局访问,此时更适合使用静态类。

情景二:需要维持一些特定的状态,此时更适合使用单例模式。

懒汉模式
1
2
3
4
5
6
7
8
9
10
11
12
public class SingletonDemo {
private static SingletonDemo instance;
private SingletonDemo(){

}
public static SingletonDemo getInstance(){
if(instance==null){
instance=new SingletonDemo();
}
return instance;
}
}

如上,通过提供一个静态的对象instance,利用private权限的构造方法和getInstance()方法来给予访问者一个单例。

缺点是,没有考虑到线程安全,可能存在多个访问者同时访问,并同时构造了多个对象的问题。之所以叫做懒汉模式,主要是因为此种方法可以非常明显的lazy loading。

针对懒汉模式线程不安全的问题,我们自然想到了,在getInstance()方法前加锁,于是就有了第二种实现。

线程安全的懒汉模式
1
2
3
4
5
6
7
8
9
10
11
12
public class SingletonDemo {
private static SingletonDemo instance;
private SingletonDemo(){

}
public static synchronized SingletonDemo getInstance(){
if(instance==null){
instance=new SingletonDemo();
}
return instance;
}
}

然而并发其实是一种特殊情况,大多时候这个锁占用的额外资源都浪费了,这种打补丁方式写出来的结构效率很低。

饿汉模式
1
2
3
4
5
6
7
8
9
public class SingletonDemo {
private static SingletonDemo instance=new SingletonDemo(); // 载入内存
private SingletonDemo(){

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

直接在运行这个类的时候进行一次loading,之后直接访问。显然,这种方法没有起到lazy loading的效果,考虑到前面提到的和静态类的对比,这种方法只比静态类多了一个内存常驻而已。

静态内部类加载
1
2
3
4
5
6
7
8
9
10
11
public class SingletonDemo {
private static class SingletonHolder{
private static SingletonDemo instance=new SingletonDemo();
}
private SingletonDemo(){
System.out.println("Singleton has loaded");
}
public static SingletonDemo getInstance(){
return SingletonHolder.instance;
}
}

使用内部类的好处是,静态内部类不会在单例加载时就加载,而是在调用getInstance()方法时才进行加载,达到了类似懒汉模式的效果,而这种方法又是线程安全的。

双重锁校验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SingletonDemo {
private static SingletonDemo instance;
private SingletonDemo(){
System.out.println("Singleton has loaded");
}
public static SingletonDemo getInstance(){
if(instance==null){
synchronized (SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo();
}
}
}
return instance;
}
}

接下来我解释一下在并发时,双重校验锁法会有怎样的情景:

STEP 1. 线程A访问getInstance()方法,因为单例还没有实例化,所以进入了锁定块。

STEP 2. 线程B访问getInstance()方法,因为单例还没有实例化,得以访问接下来代码块,而接下来代码块已经被线程1锁定。

STEP 3. 线程A进入下一判断,因为单例还没有实例化,所以进行单例实例化,成功实例化后退出代码块,解除锁定。

STEP 4. 线程B进入接下来代码块,锁定线程,进入下一判断,因为已经实例化,退出代码块,解除锁定。

STEP 5. 线程A初始化并获取到了单例实例并返回,线程B获取了在线程A中初始化的单例。

理论上双重校验锁法是线程安全的,并且,这种方法实现了lazyloading。

作者

bd160jbgm

发布于

2021-08-30

更新于

2021-10-07

许可协议