常见设计模式概览、相似对比及最佳实践
一、常见设计模式概述
1. 单例模式
必要性:静态变量在程序一开始就需要创建好对象,如果这个对象非常耗费资源,并且这次程序执行没有使用它,造成不必要的浪费(伪需求)
适合场景:线程池、缓存、路由表、默认设置等
分类:饱汉/饿汉、线程安全/线程不安全
要点:私有构造
–> 如何防止反射攻击:1.static记录标识位 2.枚举实现
实现方式:静态内部类、双加锁、枚举
-
饿汉式是线程安全的:
–> 前提:同一类加载器,一个类只会被初始化一次;
有些仅有主动引用(new, getstatic, putstatic, invokestatic字节码指令),如果类没有进行过初始化,由需要先触发其初始化 --> <cinit>() 阻塞执行 -
静态内部类实现饱汉且线程安全原理:
饱汉:外部类加载的时候,内部类不会被加载,静态内部类只是调用的时候用了外部类的名字而已(被动引用)
线程安全:被动引用执行的初始化,即<cinit>()也是阻塞执行 -
枚举为什么是最佳实践?(支持自家方法)
1.反射内部屏蔽了枚举
2.序列化/反序列化安全 -> 序列化前后两个对象相等
3.写法简单 -
双加锁演进历程: 加锁 -> 避免串行化设计(双加锁) -> volatile(禁止指令重排序)
-> 禁止重排序可引申:Java创建对象的三步曲(分配空间、调用构造函数初始化、指向分配好的空间);Sync保证的是同步块和外部代码的有序性和原子性,内部还是会重排序,故会出现“半个对象”问题;C++(new的三种形态) -
单例引起的内存泄露:GCRoot
引申思考:
7. 饿汉的加载时机?所有类是应用启动的时候就load的?
–> 当然是用到的时候初始化,你以为是变形金刚?充能100%然后启动,黄花菜都凉了。除非这个单例类设计的不合理,有很多和该对象无关的方法被提前调用(饱汉和饿汉本质上差别就是:饿汉在类加载的时候就创造了对象)。所以一直觉得饱汉饿汉是个伪需求,为了加速业务启动,甚至懒加载预启动更合适。
–> 冷知识:IDE默认实现的单例是饿汉线程不安全的,kt的object也是
- 对比C++智能指针的实现(多个地方用到了一个对象,减少构造析构)
2. 工厂模式
静态工厂模式、工厂方法模式、抽象工厂方式的联系和区别:
(简单的new一个产品类) 静态工厂
–> (开闭原则,new推迟到对应的子类工厂) 工厂方法
–> (打破工厂和产品一对一关系) 抽象工厂
抽象工厂是工厂方法模式的扩展,但因为抽象工厂里定义了生产不同产品族的接口(扩展需要增删接口),又重新违反了开闭原则
3. 适配器模式
对象适配器:把适配工作交给适配器,低耦合 --> 转换
类适配器:通过多继承让类具备多种功能 --> 功能增强
(Java单继承,只能通过接口曲线救国;C++注意菱形继承问题/钻石问题)
缺点:过多使用适配器,会让系统非常零乱,不易于整体把握。例如:明明调用的是A接口,内部却被适配成类B接
4. 命令模式
实现:方法只有execute,可以执行一串Command;调用者和请求接收者分离
关注点分离:如MVC,C控制V
特点:支持多个命令(宏命令)、撤销
适合场景:日志的持久化(store/load)、电视遥控器
5. 组合模式
理解:递归实现功能,父菜单和子菜单使用一致的方法来处理(官方称之为透明化) --> 很容易形成一种树形结构(方便拆装)
优化:如遍历成本较高,组合节点的缓存
6. 装饰者模式
要求:装饰者必须实现被装饰者的接口,方便被外层调用(里氏替换)
理解:就是在对象上包裹了一层,补充功能,代替继承
7. 外观模式
目的:提供一个统一的接口,调用子系统的功能
使用场景:子系统很复杂,但调用方无需关心内部实现
对比:现有的类不符合(适配器),接口实现太大太复杂(外观)、补充功能(装饰者)
8. 迭代器模式
实现:通过hasNext()和next(),顺序访问元素,不暴露对象内部
9. 代理模式
目的:控制对对象对访问,保护被代理对象
对比:适配器重在适配,代理重在保护
适合场景:智能指针(智能引用代理)、缓存代理(CDN:减少计算和网络延时)、防火墙代理(避免恶意攻击)
分类:
> 动态代理(代理对象在运行时通过反射动态创建:InvocationHandler)
所有的函数调用最终都会经过invoke函数的转发,可实现日志记录、性能统计、拦截、权限控制等(即AOP)
> 静态代理(运行前,代理对象就已经生成)
10. 策略模式
理解:定义功能接口,在子类选择不同的实现,适用于多方案的实施和对比
11.状态模式
理解:状态模式定义好了状态之间的转换,可替代多个条件判断
12.模板方法模式
理解:Hook,定义好算法框架,子类可在不改变算法结构的情况下,重新定义算法里的某些步骤
对比:策略模式是继承的一个体现,子类实现整个算法;模板方法是实现算法框架
13. 原型模式
序列化是深拷贝的一种体现
–> 序列化如果不是深拷贝,捣鼓出个对象引用能干啥
14.享元模式
池化支持重用大量的细粒度对象
二、相似模式对比
- 装饰器模式 VS 代理模式:装饰器是增强功能;代理模式是控制访问
- 桥接模式 VS 中介模式:中介模式类似注册管理中心,以调停者为中心形成星状结构;桥接模式主要是隔离多个维度的变化
- 外观模式 VS 中介模式:外观是对外封装统一的高层接口,中介者避免多个互相协作的对象直接引用,松耦合
- 策略模式 VS 状态模式:策略模式重在算法的替换,状态模式重在通过状态改变行为
三、最佳实践
1. Android+JDK中的实践
- 模版方法:Activity生命周期、AbstractList(其实抽象类基本上都会用)
- 装饰者:ContextWrapper内部有个Context引用mContext
- 解释器:Manifest中定义Activity,PMS作为解释器解释
- 备忘录:Activity的onSaveInstanceState和onRestoreInstanceState,用于保存和恢复、序列化
- 建造者:AlertDialog、StringBuilder
- 观察者:ListView的Adapter,里面的nofityDataSetChanged()
- 代理:AIDL(Binder,BBinder和BPBinder的关系 --> 从Binder架构图细化进去)、动态代理
- 中介者:Binder机制中的ServiceManager和Binder驱动(中介者对象是将系统从网状结构转为以调停者为中心的星型结构,类似注册管理中心)
- 适配器:Adapter、Arrays.asList()
- 命令:Handler模式是典型的命令模式:Looper不知道Message的实现细节,只管调用它执行/线程池处理execute也沾点边
- 原型:Intent.clone(),Object.clone()
- 责任链:有序广播 & 事件分发(体现在onTouchEvent返回true消费事件)
- 策略:动画中的插值器、Comparator的比较器
- 组合:View和ViewGroup都实现了View接口
- 享元:Message.obtain()->对象池,常量池(char是0 ~ 127,其他是-127 ~ 128)、线程池
- 单例+静态工厂:getSystemService(),根据传入的参数决定返回哪个单例对象
- 抽象工厂:Service的onBind方法可以看成是一个工厂方法,从framework角度Service可以当做一个具体工厂
- 状态:Wi-Fi的状态
- 迭代器:SQLiteDatabase的Cursor
- 访问者:编译期注解(APT)
- 装饰:ContextWrapper、Reader/Writer、Collections转线程安全的类
- 外观:如startActivty、sendBroadcast、bindService等等,内部实现非常复杂
- 桥接:View的描述维护和绘制到屏幕上桥接(Display、HardwareLayer和Canvas)
- 简单工厂/静态工厂方法:DateFormat
- 工厂方法:Collection(抽象工厂)、ArrayList/LinkedList(具体工厂)、Iterator(抽象产品)、(ListItr/Itr)具体产品
2. 开源中的实践
- OkHttp3:Builder创建OkHttpClient、Request、Response等、责任链实现拦截器
- Retrofit2:动态代理解析请求的接口参数(HttpServiceMethod),CallAdapter适配不同平台的OkHttpCall、有必要则提供Converter选择不同的解析器(策略模式)、接口的解析可以认为是解析器模式,Builder、工厂、外观也有体现
- Glide:into这么方便,里面实现这么复杂,就必须提一下外观模式了、常规Builder、可配置的缓存策略(策略模式)、RequestManagerRetirever(猎犬)的applicationManager是双加锁的单例
- Gson:静态工厂创建各种类型的解析适配器,并缓存(适配器+静态工厂),毕竟是个序列化框架,read/write自带原型模式
- EventBus:观察者模式、注册中心(总线)唯一(单例模式)、根据不同情况选择不同的Poster(策略)
- Dagger2:利用工厂模式实现依赖注入
- RxJava2:观察者模式、如map操作符将ObservableCreate包装成ObservableMap(装饰)
3. 思维发散
- 端口复用技术、SocketServer的port映射多个fd来绑定多个连接(select/poll/epoll获取数据),这种构成星型结构的,可用理解为中介者模式
- 网络的自治系统,内部自行管理,对外提供一个出入口,可用理解为外观模式
- 代码的坏味道:
1.代码重复;
2.方法过长;
3.类提供的功能太多;
4.数据泥团(数据成批出现在多个对象中,此时需要封装成类);
5.冗余类(不干啥活,也没啥意义,却多了个对象);
6.需要太多注释(代码解释不通)