设计模式
摘要:什么是设计模式
设计模式是经过总结、优化的,对我们经常会碰到的一些编程问题的可重用解决方案。
这里列举了三种最基本的设计模式:
- 创建模式,提供实例化的方法,为适合的状况提供相应的对象创建方法
- 结构化模式,通常用来处理实体之间的关系,使得这些实体能够更好地协同工作
- 行为模式,用于在不同的实体间进行同学,为实体之间的同学提供更容易,更灵活的通信方法
创建型
0、简单工厂模式
01、普通
就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。首先看下关系图:
Sender是接口,MailSender和SmsSender是实现这个接口的类,然后使用SendFactory创建实例。
1 | public class SendFactory { |
02、多个方法
是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
1 | public class SendFactory2 { |
03、多个静态方法
将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
1 | public class SendFactory3 { |
总结
工厂模式适合:凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。
在以上的三种模式中,第一种如果传入的字符串有误,不能正确创建对象,
第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。
1、工厂方法模式(Factory Method)
简单工厂模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则。
解法:工厂方法模式,创建一个工厂接口和创建多个工厂实现类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。
增加一个Provider接口,用于提供工厂类,共有的方法在接口中定义:
1 | package com.lrr; |
工厂类:
1 | public class SendMailFactory implements Provider { |
1 | public class SendSmsFactory implements Provider { |
测试方法:
1 | public static void test4(){ |
其实这个模式的好处就是,如果你现在想增加一个功能:发及时信息,则只需做一个实现类,实现Sender接口,同时做一个工厂类,实现Provider接口,就OK了,无需去改动现成的代码。这样做,拓展性较好!
2、抽象工厂模式
工厂方法模式:
- 一个抽象产品类,可以派生出多个具体产品类。(Sender)
- 一个抽象工厂类,可以派生出多个具体工厂类。 (Provider)
- 每个具体工厂类只能创建一个具体产品类的实例。
抽象工厂模式:
- 多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。(多个Sender,Speaker)
- 一个抽象工厂类,可以派生出多个具体工厂类。
- 每个具体工厂类可以创建多个具体产品类的实例,也就是创建的是一个产品线下的多个产品。
区别:
- 工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。
- 工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。
- 工厂方法创建 “一种” 产品,他的着重点在于”怎么创建”,也就是说如果你开发,你的大量代码很可能围绕着这种产品的构造,初始化这些细节上面。也因为如此,类似的产品之间有很多可以复用的特征,所以会和模版方法相随。
抽象工厂需要创建一些列产品,着重点在于”创建哪些”产品上,也就是说,如果你开发,你的主要任务是划分不同差异的产品线,并且尽量保持每条产品线接口一致,从而可以从同一个抽象工厂继承。
所以说抽象工厂就像工厂,而工厂方法则像是工厂的一种产品生产线
3、单例模式
单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:
1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
1 | public class Singleton { |
在getInstance
方法中synchronized
方法锁住的是整个对象,性能会有所下降,因为每次调用getInstance()
,都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,可以改成下面的形式。
1 | public static Singleton getInstance() { |
似乎解决了之前提到的问题,将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 | private static class SingletonFactory{ |
实际情况是,单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式:
1 | public class Singleton { |
总结
synchronized关键字锁定的是对象,在用的时候,一定要在恰当的地方使用(注意需要使用锁的对象和过程,可能有的时候并不是整个对象及整个过程都需要锁)。
5、原型模式(Prototype)
原型模式虽然是创建型的模式,但是与工程模式没有关系,从名字即可看出,该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。本小结会通过对象的复制,进行讲解。在Java中,复制对象是通过clone()实现的,先创建一个原型类:
1 | public class Prototype implements Cloneable{ |
一个原型类,只需要实现Cloneable接口,覆写clone方法,此处的重点是super.clone()这句话,super.clone()调用的是Object的clone()方法,而在Object类中,clone()是native的。
浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。
深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。
1 | class SerializableObject implements Serializable { |
要实现深复制,需要采用流的形式读入当前对象的二进制输入,再写出二进制数据对应的对象。
基于Python的设计模式实现
单例模式
1. 使用模块
Python的模块就是天然的单例模式,因为模块在第一次导入时,会生成 .pyc
文件,第二次导入时,就会直接加载 .pyc
文件,而不会再次执行模块代码。因此,我们只需把相关函数和数据定义在一个模块中,就可以获得一个单例对象了。如果真的想要一个单例类,可以像下面这样做:
mysingleton.py
1 | class Singleton(object): |
将上面的代码保存在文件 mysington.py
中,要使用时,直接在其他文件中导入此文件中的对象,这个对象既是单例模式的对象
1 | from a import singleton |
2. 使用装饰器
1 | def Singleton(cls): |
3. 使用类
可以支持多线程的单例模式:
1 | import time |
这种方式实现的单例模式,使用时会有限制,以后实例化必须通过 obj = Singleton.instance()
。
如果用 obj=Singleton()
,这种方式得到的不是单例
4. 基于__new__
方法实现(推荐使用,方便)
通过上面例子,我们可以知道,当我们实现单例时,为了保证线程安全需要在内部加入锁
我们知道,当我们实例化一个对象时,是先执行了类的__new__方法**(我们没写时,默认调用object.__new__),实例化对象;然后**再执行类的__init__方法,对这个对象进行初始化,所有我们可以基于这个,实现单例模式:
1 | '''基于new方法实现''' |
采用这种方式的单例模式,以后实例化对象时,和平时实例化对象的方法一样 obj = Singleton()
。
基于Java的设计模式实现
单例模式
在知道了什么是单例模式后,我想你一定会想到静态类,“既然只使用一个对象,为何不干脆使用静态类?”,这里我会将单例模式和静态类进行一个比较。
- 单例可以继承和被继承,方法可以被override,而静态方法不可以。
- 静态方法中产生的对象会在执行后被释放,进而被GC清理,不会一直存在于内存中。
- 静态类会在第一次运行时初始化,单例模式可以有其他的选择,即可以延迟加载。
- 基于2, 3条,由于单例对象往往存在于DAO层(例如sessionFactory),如果反复的初始化和释放,则会占用很多资源,而使用单例模式将其常驻于内存可以更加节约资源。
- 静态方法有更高的访问效率。
- 单例模式很容易被测试。
几个关于静态类的误解:
误解一:静态方法常驻内存而实例方法不是。
实际上,特殊编写的实例方法可以常驻内存,而静态方法需要不断初始化和释放。
误解二:静态方法在堆(heap)上,实例方法在栈(stack)上。
实际上,都是加载到特殊的不可写的代码内存区域中。
静态类和单例模式情景的选择:
情景一:不需要维持任何状态,仅仅用于全局访问,此时更适合使用静态类。
情景二:需要维持一些特定的状态,此时更适合使用单例模式。
懒汉模式
1 | public class SingletonDemo { |
如上,通过提供一个静态的对象instance,利用private权限的构造方法和getInstance()方法来给予访问者一个单例。
缺点是,没有考虑到线程安全,可能存在多个访问者同时访问,并同时构造了多个对象的问题。之所以叫做懒汉模式,主要是因为此种方法可以非常明显的lazy loading。
针对懒汉模式线程不安全的问题,我们自然想到了,在getInstance()方法前加锁,于是就有了第二种实现。
线程安全的懒汉模式
1 | public class SingletonDemo { |
然而并发其实是一种特殊情况,大多时候这个锁占用的额外资源都浪费了,这种打补丁方式写出来的结构效率很低。
饿汉模式
1 | public class SingletonDemo { |
直接在运行这个类的时候进行一次loading,之后直接访问。显然,这种方法没有起到lazy loading的效果,考虑到前面提到的和静态类的对比,这种方法只比静态类多了一个内存常驻而已。
静态内部类加载
1 | public class SingletonDemo { |
使用内部类的好处是,静态内部类不会在单例加载时就加载,而是在调用getInstance()
方法时才进行加载,达到了类似懒汉模式的效果,而这种方法又是线程安全的。
双重锁校验
1 | public class SingletonDemo { |
接下来我解释一下在并发时,双重校验锁法会有怎样的情景:
STEP 1. 线程A访问getInstance()方法,因为单例还没有实例化,所以进入了锁定块。
STEP 2. 线程B访问getInstance()方法,因为单例还没有实例化,得以访问接下来代码块,而接下来代码块已经被线程1锁定。
STEP 3. 线程A进入下一判断,因为单例还没有实例化,所以进行单例实例化,成功实例化后退出代码块,解除锁定。
STEP 4. 线程B进入接下来代码块,锁定线程,进入下一判断,因为已经实例化,退出代码块,解除锁定。
STEP 5. 线程A初始化并获取到了单例实例并返回,线程B获取了在线程A中初始化的单例。
理论上双重校验锁法是线程安全的,并且,这种方法实现了lazyloading。