一、概述
之前已经看过很多关于Java 23种设计模式的文章,而大多都是讲基础理论和图示表达,但很少有具体的应用理解。俗话说,实践才是最好的老师,所以本文将对常用的几种设计模式讲一讲他们的实际应用,已提高对它们的理解。
ps:本文不详细讲解原理,只带大家理解
二、设计模式概要
分类
- 总体来说设计模式分为三大类:
创建型模式
结构型模式
行为型模式。
六大原则—总原则:开闭原则
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等,后面的具体设计中我们会提到这点。
- 单一职责原则
- 里氏替换原则
- 依赖倒转原则
- 接口隔离原则
- 迪米特法则(最少知道原则)
- 合成复用原则
这一节可以参考Java开发中的23种设计模式详解 这里不再赘述,因为实在对记忆和理解没有太大帮助
三、Java常用设计模式
本文主要涉及以下14种模式,都是自己的理解,具体还请参照Java开发中的23种设计模式详解
- 单例模式
- 建造者模式
- 适配器模式
- 装饰器模式
- 代理模式
- 外观模式
- 组合模式
- 享元模式
- 模版模式
- 观察者模式
- 迭代子模式
- 责任链模式
- 命令模式
- 备忘录模式
1、单例模式
单例模式是非常常用且最容易理解的设计模式之一,它能保证在JVM中只有一个实例对象存在,因此会给系统带来以下好处:
1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
3、有些类的实例在全局中只允许存在一个,如Android中的Application。
单例模式基本写法,以下代码引用其他博客代码
public class Singleton {
/* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */
private static Singleton instance = null;
/* 私有构造方法,防止被实例化 */
private Singleton() {
}
/* 静态工程方法,创建实例 */
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
public Object readResolve() {
return instance;
}
}
这个类是最基础的单例的形式,可以满足单线程的需求,但在多线程环境中就会出现问题。因为多个线程可能都进入了instance = new Singleton()
代码块,这就导致可能产生多个实例。
要解决该问题,首先想到使用 synchronized
字段,如下代码块
方法1(不推荐)
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
该方案中synchronized
锁住整个对象,那么每次调用getInstance()
都会上锁,导致性能下降
方法2(不推荐)
public static Singleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
该方案中似乎没有什么问题,但实际情况比我们想象的要复杂的多,问题就在instance = new Singleton();
中,因为在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的因此有可能instance已经被分配内存控件,但实际上还没有初始化。
相对较优的单例方案(推荐)
public class Singleton {
...
/* 此处使用一个内部类来维护单例 */
private static class SingletonFactory {
private static Singleton instance = new Singleton();
}
/* 获取实例 */
public static Singleton getInstance() {
return SingletonFactory.instance;
}
}
该方案中利用静态内部类来维护单例,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。但使用该方案如果在构造函数中抛出异常,则永远得不到单例。
备选方案
要解决多线程单例的问题,最关键的问题是要将访问和创建分开,那么也可以采用以下方案(只有第一次创建时会上锁,不影响性能)
public class SingletonTest {
private static SingletonTest instance = null;
private static synchronized void syncInit() {
if (instance == null) {
instance = new SingletonTest();
}
}
public static SingletonTest getInstance() {
if (instance == null) {
syncInit();
}
return instance;
}
}
Android中的应用
在Android中最明显的单例是Application,该单例无需创建,因为安卓已经帮我们做好了,只需要静态存储即可,也无需担心GC问题,因为Application的生命周期与应用一致
public class GlobalApplication extends Application {
private static GlobalApplication instance;
@Override
public void onCreate() {
super.onCreate();
instance = this;
}
}
2、建造者模式
相信Android工程狮们对建造者模式一定不会陌生,因为AlertDialog
的创建往往用到了AlertDialog.Builder
。有人觉得使用建造者模式是多此一举,包括本人一开始学习的时候也是这么认为。但事实上,Builder的作用除了创建对象外,主要给实例赋初始值,同时可以保证了赋值顺序。有时候,我们对象初始化的赋值顺序是固定不可变的,这需要Builder来做约束
new AlertDialog.Builder(self)
.setTitle("标题")
.setMessage("简单消息框")
.setPositiveButton("确定", null)
.show();
3、适配器模式
适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题,具体可以参考Java开发中的23种设计模式详解
相信Android工程狮对适配器在熟悉不过了,当我们使用ListView、GridView、RecyclerView等控件加载数据时,都用到了适配器。
这个如介绍所说的,适配器的目的是 消除由于接口不匹配所造成的类的兼容性问题 只要理解这句话就基本已经掌握适配器的精髓了
4、装饰模式
装饰器模式,动态给一个对象增加新功能,实现原对象接口,并持有原对象
这句话理解起来有点似懂非懂,借用其他博客的图
其中Sourceable
是接口,Source
是需要被装饰的类,Decorator
是我们编写的装饰器,它同样实现Sourceable
接口,并在Decorator
中持有Source对象
代码如下:
// 通用接口
public interface Sourceable {
public void method();
}
// 原始类
public class Source implements Sourceable {
@Override
public void method() {
System.out.println("the original method!");
}
}
// 装饰后用 Sourceable 接收,调用method()就能得到装饰后的结果
public class Decorator implements Sourceable {
private Sourceable source;
public Decorator(Sourceable source){
super();
this.source = source;
}
@Override
public void method() {//被装饰的方法
System.out.println("before decorator!");
source.method();
System.out.println("after decorator!");
}
}
Android中的应用
在Android中Context继承用了装饰模式,如下图:
Context 有2个实现类,ContextImpl 和 ContextWrapper。ContextWrapper 类人如其名,实际上只是做装饰作用,实际持有 ContextImpl 的对象,在整个Android环境中也是 ContextImpl 在起实际作用
5、代理模式
代理模式与装饰模式非常相似,主要区别在与代理模式对代理对象有控制权,允许在重写的方法中决定是否调用被代理对象的方法;而装饰模式则没有控制器,在重写方法中,需要调用被装饰对象的该方法,可以在前后增加装饰代码,增强功能。
可参考:代理模式 vs 装饰模式
6、外观模式
外观模式(Facade),他隐藏了系统的复杂性,并向客户端提供了一个可以访问系统的接口。这种类型的设计模式属于结构性模式。为子系统中的一组接口提供了一个统一的访问接口,这个接口使得子系统更容易被访问或者使用。
举个栗子,电脑的启动包好了CPU,硬盘,内存等模块的初始化和启动,而对于用户来说只关心开机这个动作,偷来一张图:
外观模式的应用较为广泛,而且方便,常常用于一个原子性操作需要调用多重操作的情况。
7、组合模式
组合模式有时又叫部分-整体模式在处理类似树形结构的问题时比较方便
在理解时,可以将组合模式理解为一个树的数据结构,大致数据结构参考下面的代码
public class TreeNode {
// 节点数据
private String name;
private TreeNode parent; // 父节点
private Vector<TreeNode> children = new Vector<TreeNode>(); // 子节点,可以是多个
....
}
Android中的应用
组合模式在Android中集中体现在View、ViewGroup的嵌套关系。如,我们可以获取到一个RelativeLayout去执行getParent()和getChildAt(index) 来获取子空间或父控件
另外,在自定义表示文件存储的树状结构时,也可以使用组合模式
8、享元模式
顾名思义,享元模式的核心理念是共享相似或相同的细粒度对象
常见的2个应用场景:线程池
数据库连接池
举个栗子:
- 服务端程序猿可能比较清楚,当访问数据库时,我们通常需要通过数据库连接对象
Connection
,而创建Server与Database的连键是一个耗时操作,当我们每次想访问数据库时都去创建Connection
是一件非常耗时又影响GC的事。因此,我们将Connection
事先创建放入一个集合(池子)中,每次访问数据库,则从中取出一个Connection
,使用完毕放回集合中。这种方式也叫数据库连接池。与线程池同理
如下图:
Android中的应用
享元模式非常常见,尤其在多线程网络请求中,将请求线程通过线程池管理起来。如OkHttp、Glide图片加载框架等都用到了线程池
9、模版模式
模版模式需要一个抽象的父类,在父类中定义一个主方法和1-N个钩子方法,可以是抽象或者实际的。通常情况下,父类已经将业务逻辑处理完毕,子类集成时只需要对其中钩子方法进行重写就可以达到快速扩展的功能。 先来张图:
图中所示是封装网络请求的一种方式。代码如下:
// 这是封装的父类模版,所有网络请求都是同样的逻辑
public abstract class BaseRequest<T> {
// 发送网络请求
public RequestCall send() {
PostFormBuilder postFormBuilder = OkHttpUtils
.post()
.url(getBaseUrl() + getUrl());// 得到接口地址,getUrl()是抽象的
// ... 封装请求参数
mRequestCall = postFormBuilder.build();
mRequestCall.execute(new StringCallback() {
// ...
@Override
public void onResponse(String response, int id) {
ResultBean<T> resultBean = fromJson(response);//对返回参数解析,fromJson是抽象的
}
});
return mRequestCall;
}
// 由子类决定调用哪个接口
protected abstract String getUrl();
// 由子类决定如何解析
protected abstract ResultBean<T> fromJson(String json) throws Exception;
}
// 子类只要集成父类,并实现getUrl()和fromJson()无需关心具体请求逻辑,可以实现快速扩展
public class LoginRequest extends BaseAntsRequest<UserInfo> {
// 定义访问接口
@Override
protected String getUrl() {
return "/user/login";
}
// 定义解析方式
@Override
protected ResultBean<UserInfo> fromJson(String json) throws Exception {
Gson gson = new Gson();
Type type = new TypeToken<ResultBean<UserInfo>>() {
}.getType();
ResultBean<UserInfo> resultBean = gson.fromJson(json, type);
return resultBean;
}
}
10、观察者模式
观察者模式中有2个主要的对象,一个是观察者,另一个是被观察者。他们之间的关系是订阅关系。
作为Android工程狮,我们经常会给View设置click事件,其实这就是观察者模式的一种。
View.setOnclickListener() 是订阅过程
View 是被观察者
OnclickListener 是观察者
View 被点击触发事件,并回调 OnclickListener.onclick()方法
几乎在Android中用到的回调都属于观察者。
RxJava
RxJava 是当前非常流行的一种异步框架,它将观察者模式运用到了极致!
极力推荐一篇关于RxJava的博文:给 Android 开发者的 RxJava 详解
11、迭代子模式
这个模式不想多解释。如果没用过迭代器 iterator 请自行补课。
12、责任链模式
有多个对象,并且每个对象持有下一个对象的引用,形成一条链式引用。请求在该链式表达中传递,直到某一个对象对它进行处理。
从数据结构上来讲,其本质就是一个链表结构
常用的应用场景:审批流程
13、命令模式
本人对该模式有深刻的印象,在某次做蓝牙低功耗(BLE)时,用到该模式,当然后来了解到 struts也是类似的模式实现的。
那么来讲讲BLE封装时的体现。
由于蓝牙BLE是以byte数组方式传递数据,并且一次最多传输20字节,所以这里定义一条数据为一个数据片piece
,在实际运用过程中,通常一条指令由一个或多个数据片组成,这里定义为Cmd
。而调用时 Client只需要生成一个Cmd
再将它交给BleWrapper
执行就可以了,无需关心具体流程。
下面我们来看一下代码
// Cmd 指令类
public class BleCmdBean extends BleBaseBean {
// 完整的16进制字符串格式命令
public BleCmdBean(String commandStr) {
this(BleHexConvert.parseHexStringToBytes(commandStr));
}
// 完整的16进制格式命令
public BleCmdBean(byte[] command) {
// 这里自动解析数据格式
this.command=command;
type = command[1];
cmd = command[2];
...
}
}
// 命令执行者,Ble封装类
public class BleWrapper {
// 提供给外部调用 发送命令
public void sendCmd(BleCmdBean cmdBean, BleCmdCallback bleCmdCallback, long cmdTimeOut, long notifyTimeOut) {
/* 判断是否准备好发送指令,是否合法 */
if (!isReadyToSend(cmdBean)) {// 不合法时停止执行,在方法体内已经回调失败
return;
}
/* 初始化各状态值 */
initState();
mCurrentCmdBean = cmdBean;
sendCmds(cmdBean.getCommands(), 0); // 分片(piece)执行蓝牙指令,先执行第一个
}
}
这里省略了很多代码,实际上 命令模式 的关键在于,调用者只需要将指令告诉到实施者,而不需要关心其具体实现。
14、备忘录模式
先了解一下备忘录模式的概念:主要目的是保存一个对象的某个状态,以便在适当的时候恢复对象
这里偷一张图:
如图所示,Original
原数据在转换成合适的数据 Memento
后使用 Storage
进行存储。在适当的情况下,从 Storage
取出并恢复 Original
。 这种模式很好理解,而且经常在用。
Android中的应用
那么,备忘录模式在安卓中有哪些应用场景呢?
举个栗子:网络请求回来的数据对象存储! 话不多说,先看图!
代码就借助其他博客的代码说明:
public class Original {
private String value;
public Original(String value) {
this.value = value;
}
public Memento createMemento(){
return new Memento(value);
}
public void restoreMemento(Memento memento){
this.value = memento.getValue();
}
}
public class Memento {
private String value;
public Memento(String value) {
this.value = value;
}
}
public class Storage {
private Memento memento;
public Storage(Memento memento) {
this.memento = memento;
}
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
与上图类似,当我们从网络上获取到 Book
对象时,由于这个对象是API接口锁定义的,并不一定适用于数据库的存储,此时可能需要转换成数据库容易识别和存储的对象 BookModel
。而当我们需要从数据库中获取时,再将 BookModel
转换成 Book
,实现数据恢复!
PS: 本人也是归纳整理+自己理解,有很多不对的地方还请指出。