对日常在 Android 中实用设计模式进行一下梳理和总结,文中参考了一些网站和大佬的博客,如 MichaelX(xiong_it) 、菜鸟教程、四月葡萄、IAM四十二等,在这里注明下~另外强烈推荐图说设计模式,看了一部分,有些介绍的还是很通俗易懂的。
设计模式(持续更新ing…)
单例模式 (Singleton pattern)
确保一个类只有一个实例,并且自行实例化并向整个系统提供这个实例(并提供对该实例的全局访问)
饿汉式、懒汉式名词解释:
饿汉式:不管程序是否需要这个对象的实例,总是在类加载的时候就先创建好实例,理解起来就像不管一个人想不想吃东西都把吃的先买好,如同饿怕了一样。
1 |
/** |
- 优点:写法简单,线程安全。
- 缺点:没有懒加载的效果,如果没有使用过的话会造成内存浪费
懒汉式:如果一个对象使用频率不高,占用内存还特别大,明显就不合适用饿汉式了,这时就需要一种懒加载的思想,当程序需要这个实例的时候才去创建对象,就如同一个人懒的饿到不行了才去吃东西。
1 |
/** |
- 优点:懒加载,线程安全,效率较高
- 缺点:volatile 影响一点性能,高并发下有一定的缺陷,某些情况下 DCL 会失效,虽然概率较小
volatile 关键字修饰的变量,一次只能有一个线程操作该变量,保证线程安全。
为什么要在变量 singleton 加上 volatile 关键字?
要理解这个问题,先要了解对象的构造过程,实例化一个对象其实可以分为三个步骤:
- 分配内存空间
- 初始化对象
- 将内存空间的地址赋值给对应的引用
但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:
- 分配内存空间
- 将内存空间的地址赋值给对应的引用
- 初始化对象
如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。
双重校验锁中两个 if 判断的作用?
第一:是为了提高程序的 效率,当 Singleton 对象被创建以后,再获取 Singleton 对象时就 不用去验证同步代码块的锁及后面的代码,直接返回Singleton 对象。
第二:是为了解决多线程下的安全性问题,也就是保证对象的唯一。
如果没有第二个 if 判断,A、B两个线程,A 先执行, 线程 B 再进入 synchronized (instance)块,不用去验证instance 是否为 null,就会直接创建一个 Singleton 新对象,这样整个程序运行下来就有可能创建多个实例。
注意:
如果处于多线程环境,注意保持线程安全,不然就无法保证单例了;
单例模式的默认构造方法的修饰符需改为 private,只能类内部访问,确保外部不能直接 new 出该实例;
单例模式需要提供一个全局访问入口,这个入口通常以 getInstance() 的 public 静态方法形式呈现。
简单工厂模式 (Simple Factory Pattern )
更多详细介绍工程模式,可链接查看 四月葡萄
定义一个接口用于创建对象,但是让子类决定初始化哪个类。工厂方法把一个类的初始化下放到子类。
生成复杂对象时,确定只有一个工厂类,可以使用简单工厂模式。否则有多个工厂类的话,使用工厂方法模式。
优点
代码解耦,创建实例的工作与使用实例的工作分开,使用者不必关心类对象如何创建。
缺点
违背开放封闭原则,若需添加新产品则必须修改工厂类逻辑,会造成工厂逻辑过于复杂。
简单工厂模式使用了静态工厂方法,因此静态方法不能被继承和重写。
工厂类包含了所有实例(产品)的创建逻辑,若工厂类出错,则会造成整个系统都会会受到影响。
Android 开发中的工厂模式实践
场景:项目中使用了 Universal Image Loader作为图片加载框架,过一段时间后,发现 UIL 已经不流行了,想用更加 fashion的 Glide 来代替 UIL,再或者有一天,Glide 不更新,也不 fashion 了… 难道又要换其他图片加载框架?难道又改吗???
这个时候,工厂方法可能可以帮上忙:使用工厂类隔离图片加载的具体实现,对外只暴露一个工厂方法用来外部生产想要的加载框架实例,就可避免上述提到的尴尬。
图片加载接口
1 |
/** |
图片加载工厂类
1 |
public class ImgLoaderClientFactory { |
UilClient:Universal Image Loader封装
1 |
public class UilClient implements ImageLoaderInterf { private static UilClient sInstance; private UilClient() {} public static UilClient getInstance() { |
GlideClient:Glide的二次封装
1 |
public class GlideClient implements ImageLoaderInterf { |
PicassoClient:Picasso封装类
1 |
public class PicassoClient implements ImageLoaderInterf { private static PicassoClient sInstance; private PicassoClient() { |
那么加载图片设置就变成了下面这样:
1 |
ImgLoaderClientFactory.getImageLoaderClient(ImgLoaderClientFactory.UIL).load(mContext, imgUrl, imageView); |
要切换图片框架呢?怎么办?可以单独写一个变量通过赋值,全局使用,或者 全局搜索替换 ImgLoaderClientFactory.UIL 也行,比如想切到Glide,将用到 ImgLoaderClientFactory.UIL 地方改成 ImgLoaderClientFactory.GLIDE 即可。
1 |
ImgLoaderClientFactory.getImageLoaderClient(ImgLoaderClientFactory.GLIDE).load(mContext, imgUrl, imageView); |
策略模式 (Strategy pattern)
定义一组算法,将其各个封装,并且使他们有交换性
策略模式好处在于使得算法在用户使用的时候能独立的改变,单一的修改,并且有良好扩展性。
算法:指的是各个策略的实现逻辑,而非算法领域的数据算法。
优点
- 策略类可以互相替换
由于策略类都实现同一个接口,因此他们能够互相替换。 - 耦合度低,方便扩展
增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合开闭原则。 - 避免使用多重条件选择语句(if-else或者switch)。
缺点
- 策略的增多会导致子类的也会变多
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
使用场景
- 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
- 一个系统需要动态地在几种算法中选择一种。
- 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。