以文本和思维导图的方式简明扼要的介绍了GoF的23个经典设计模式,可当成学习设计模式的一个小手册,偶尔看一下,说不定会对大师的思想精髓有新的领悟。
GoF(“四人帮”,又称Gang of Four,即Erich Gamma, Richard Helm, Ralph Johnson & John Vlissides)
设计模式是在软件开发过程中,对于一些普适需求总结的设计模板。根据目的可以分为三类:
(1).创建型:与对象的创建相关。
(2).结构型:处理类或者是对象的组合。
(3).行为型:对类或者对象怎么进行交互,怎样分配职责进行描述。
需要注意:设计模式只是提供一种思路,能够直接套用的情况不多,更多的是处理问题的思路。
面向对象的六大原则:
(1)单一职责原则:就一个类而言,应该仅有一个引起它变化的原因。关键是职责划分
(2)开闭原则:软件中的对象(类·模块·函数)应该对扩展是开放的,对于修改是封闭的。关键是通过抽象实现。
(3)里式替换原则:所以引用基类的地方必须能透明的使用其子类替换。依赖继承与多态特性。
(4)依赖倒置原则:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系通过接口或抽象类产生的。
(5)接口隔离原则:客户端不应该依赖它不需要的接口。类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大,臃肿的接口拆分成更小的和更具体的接口,这样客户端只需要关注他们感兴趣的方法。
(6)迪米特原则【最少知识原则】:一个对象应该对其他对象有最少的了解。一个类应该最自己需要耦合或调用的类知道的最少,类的内部如何实现与调用者或依赖者没关系,调用者或者依赖者只需要知道它需要的方法即可。
1.单例模式-应用最广的模式
定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
实现单例模式主要有以下几个关键点:
(1)构造函数不对外开放,一般为private。
(2)通过一个静态方法或者枚举返回单例类对象。
(3)确保单例类的对象有且只有一个,特别是在多线程环境下。
(4)确保单例类对象在反序列化时不会重新构建对象。
实现方式:
(1)饿汉模式:默认初始化一个自身的静态对象,对外的静态方法每次返回该实例。
(2)懒汉模式:静态方法加同步锁,第一次调用时,如果该实例为空,才会构造。每次调用都同步,造成不必要开销。
(3)Double Check Lock(DCL)双重检查锁定。
if(mInstance==null){
synchronized(Instance.class){
if(mInstance==null){
mInstance=new Instance();
}
}
}
这个关键在与进行了2此mInstance判空,加了类同步锁。由于java编译器允许处理器乱序执行。因此在1.6以下或者并发场景比较复杂时不推荐。这是我们平时用的最多的一种方式。
(4)静态内部类单例模式:
这种方式只在第一次调用静态方法时,才会导致其静态内部类加载到虚拟机,初始化对象。不仅能够保证线程安全,也能够保证单例对象的唯一性,同时延迟了单例的实例化,所以是推荐使用的实现方式。
(5)枚举单例:枚举实例的创建时线程安全的,并且在任何情况下都是一个单例
(6)使用容器实现单例模式:将多种单例类型注入到统一管理器中,根据key获取。
序列化相关:
通过序列化可以将一个单例的实例对象写到磁盘,然后读回来,从而有效获得一个实例。
即使构造函数是私有的,反序列化依然尅通过特殊的途径去创建类的一个实例,相当于调用该类的构造函数。
反序列化操作提供了一个很特别的钩子函数,类中具有一个私有,被实例化得方法readResolve(),这个方法可以让开发人员控制对象的反序列化。
要杜绝单例被反序列化重新生成对象,必须加入如下方法:
private Object readResolve() throws ObjectStreamException{
return mInstance;
}
也就是在readResolve方法中将,mInstance返回,而不是默认重新生成一个对象。枚举不存在这个问题。
优点:
(1)单例模式在内存中只有一个实例,减少内存开支,特别一个对象模块需要频繁创建销毁,而创建销毁时性能又无法优化,单例模式就非常有用。
(2)单例模式可以在系统设置全局的访问点,优化和共享资源访问。
(3)利用单例模式可以避免对资源的多重占用,解决线程安全,统一解决访问问题
缺点:
(1)单例模式没有接口,扩展困难。只能修改源码。
(2)单例对象如果持有context容易引发内存泄漏,此时需要注意传递给单例对象的context最好是Application的context。
2.建造者模式-Builder
定义:将一个复杂对象的构建和表示分离,使得同样的构造过程创建不同的表示结果。
使用:实际开发中,我们一般是直接使用一个builder来进行对象的组装,里面全部是构造或者表示的参数,我们可以设置链式调用,关键点是每个setter方法返回自身,return this;这样就可以链式调用。
AlertDialog.Builder的实现,就是为了构建复杂的AlertDialog,将dialog的构造和表示分开。
这个的使用,在开源项目中比较常见,比如Universal image loader中,通过将ImageLoaderConfig的构造函数,字段私有化,使得外部不能访问内部属性,用户只能通过设计Builder对象的属性,通过Builder构造出ImageLoaderConfig对象,这样就实现构造和表示的分离。
通过配置类的构造器Builder将配置的构造和表示分离开来,同时又将配置从目标类中隔离处理,避免过多的setter方法污染目标类。以及方便的链式调用。
优点:
(1)良好封装性,使用建造者模式可以是客户端不必知道产品内部组成的细节。
(2)构造者独立,容易扩展。
缺点:
产生多余的Builder对象,消耗内存。
3.原型模式-使程序运行更高效
定义:用原型实例,指定创建对象的种类,并通过拷贝这些原型创建新的对象。
使用场景:
(1)类初始化需要消耗非常多的资源,这个资源包括数据,硬件资源等,通过原型靠包避免这些消耗。
(2)通过new产生一个对象需要非常繁琐的数据准备或者访问权限,这是可以使用原型模式。
(3)一个对象需要提供给其他对象访问,而且各个调用者可能读需要修改其值时,可以考虑使用使用原型模式拷贝多个对象供调用者使用,即保护性拷贝。
注意:
(1)通过实行Cloneable接口的原型模式在调用clone函数构造实例时并不一定比new操作更快,只有当通过new构造对象较为耗时或者成本较高时,才能获得效率提升。
(2)clone()方法并不是Cloneable接口中的,而是Object中的方法,,Cloneable是一个标识接口,它表明这个类的对象是可拷贝的,如果没有实现Cloneable接口,但是调用了clone()函数将抛出异常。所以我们一般的都是实现Cloneable接口,覆些clone()方法实现对象的拷贝实现,来实现原型模式。
(3)通过clone拷贝对象时不会执行构造函数。在使用Cloneable实现拷贝的时候,是调用了clone()方法实现,对象的构造韩式是不会执行的。如果构造函数中需要一些特殊初始化,可能发生问题。
(4)深拷贝与浅拷贝:区别在于拷贝对象时,深拷贝对引用型的字段同样采用拷贝的方法。浅拷贝只是单纯拷贝引用的形式。对于引用类型的,如果只是拷贝应用,那么调用者会连原型对象的值一起改变,因为他们引用的最终指针地址是一处。而深拷贝,引用类型的会有新地址内存。
intent就是原型模式。
public Object clone(){
return new Intent(this);
}
只不过它是new比clone效率高,所以内部这么实现。
比如平时应用比较多的,应该是保护性拷贝。
比如订单提交界面,服务器接口,要求在提交的数据里进行一些特定操作,会修改订单数据。但是修改了这些数据在本地显示就会有一些问题,或者上一次提交网络异常,重新提交是原型数据发生了改变。
这种情况,我们就应该讲本地初始数据,当作原型,深拷贝对象给其他调用者去处理,对应流程,那么流程异常后,回退,原型数据是不变的,我们可以继续对原型数据修改,后继续提交等其他处理操作。
优点:
原型模式是在内存中二进制流的拷贝,要比直接new一个对象性能好很多,特别是在一个循环体内产生大量对象时,原型模式可以更好的体现其优点。
缺点:
直接在内存中执行,构造函数不会执行,实际开发中要注意这一点。
4工厂方法模式-应用最广泛的模式