谈到设计模式,我们很多伙伴可能都了解一二,看到源码中的一些设计模式后,恍然大悟原来代码可以这么写,但真正自己下手的时候,反而会把这些理念抛在了脑后。所以如果把熟练使用设计模式分个等级(最高10级),只是看懂了设计模式,只能是1,而能用某些设计模式优化代码结构,就算是3级,当看到某个业务逻辑就能知道使用什么样的设计模式,就能达到8级甚至以上。
所以想要真正的熟悉设计模式,当然也要从看开始,我自己一开始写代码的时候,也是眉毛胡子一把抓,很随意,但是自己慢慢有了”代码洁癖“之后,就会注意这些问题,作为博主自己肯定也不是到了熟练使用设计模式这个阶段,但是还是想把一些自己的思想分享给伙伴们。
1 老生常谈 - 7大设计原则
这部分可能比较枯燥,伙伴们也都熟记这些原则,可真正写代码的时候,貌似并没有完全遵守这些原则,包括我自己在内,那么这些原则到底怎么样才算是没有违背呢?
7大设计原则:SOLID,分别为单一职责原则、开闭原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特原则、合成复用原则。
1.1 单一职责
其实从名字上我们就已经知道,当我们封装一个类的时候,它遵循的是面向对象,因此这个类应该只做自己该干的事,例如一个User类,它可以提供用户信息,但是让它提供今天是几月几号的能力,就不合理了。正常一个类不能超过200行代码,可见伙伴们动辄一个类上千行,那么就有可能存在冗余其他职责的嫌疑了。
1.2 开闭原则
这个原则应该是6大设计原则中最终的一个原则。开闭原则即为面向修改关闭,面向扩展开发,看下面这个例子
class SDKImpl {
fun init(){
// TODO impl
}
fun send(){
// TODO impl
}
}
当我们接到一个新的需求时,例如要使用A SDK来完成,其中有两个方法,我们完成了具体的实现;后续的时候,发现A SDK有问题,想要换成B SDK,这个时候如果要去修改SDKImpl类中的方法为了满足新SDK的功能,此时就违背了开闭原则。
interface ISDK {
fun init(){
}
fun send(){
}
}
这个时候,就需要接口或者抽象类来提供一个稳定的抽象层,当有新的需求来的时候,只需要派生出一个新的对象就能够避免对原有逻辑的修改,这样就完成了面向抽象是开放的原则。
class ASDKImpl : ISDK{
override fun init() {
super.init()
}
override fun send() {
super.send()
}
}
class BSDKImpl : ISDK{
override fun init() {
super.init()
}
override fun send() {
super.send()
}
}
像这样派生多个子类,一般情况下都会有一个静态代理类负责管理这些子类的切换,之后的设计模式中会提到。
1.3 里氏替换原则
对于里氏替换原则,伙伴们熟悉吗?但是在项目中,我想伙伴们都见到过,但是可能还没有认知,这个原则是干什么的?伙伴们可以这么理解:子类可以扩展父类中的方法,但是不能改变父类中原有的逻辑。
open class Car {
var mSpeed: Int = 0
fun setSpeed(speed: Int) {
this.mSpeed = speed
}
fun getRunTime(duration: Long): Long {
return duration / mSpeed
}
}
有一个简单的Car类,能够设置速度并计算出运行的时长;
class AudiCar : Car() {
override fun setSpeed(speed: Int){
mSpeed = 0
}
}
如果有一个子类实现了Car,并重写了父类的方法,此时将mSpeed设置为0,其实父类原始逻辑中,会把mSpeed赋值,而子类中则是永远为0,就会导致调用父类getRunTime方法报错,所以就违背了里氏替换原则。
1.4 依赖倒置原则
依赖倒置原则,其实跟前面1.2节中的开闭原则类似,其实就是面向接口编程。当我们在设计一个框架的时候,我们的分层设计需要遵循的一个原则就是:高层的模块最好不要直接依赖低层的直接实现,而是通过操作接口完成低层实现的调用。
这个是什么意思呢?还是拿1.2中的例子来说,假如我们在当前版本要使用A SDK,我们的框架设计如下,有一个Manager类:
object SdkManager {
fun init(sdk:ASDKImpl){
sdk.init()
}
}
我们看是直接调用了A SDK实现类的init方法,当下个版本的时候,要换成B SDK,那么就需要修改init方法,这样就违背了开闭原则。
当然也可能这么想,就是我加一个方法不行吗?
object SdkManager {
fun init(sdk:ASDKImpl){
sdk.init()
}
fun initBSdk(sdk:BSDKImpl){
sdk.init()
}
}
当然也没有问题,但是我们需要想到一点就是,如果SDK的种类很多,岂不是要加很多方法,所以回到我们前面说到的,高层的模块不要直接操作直接子类,而是需要通过接口来调度。
object SdkManager {
fun init(sdk:ISDK){
sdk.init()
}
}
这样整个实现就非常灵活了,在使用某个SDK时,只需要传入对应的具体实现即可。
SdkManager.init(ASDKImpl())
1.5 接口隔离原则
还是对接1.4的话题,接口隔离原则就是在设计接口的时候,尽量保证接口的单一性。什么意思呢?并不说每个接口只能写一个方法,那这样下去整个项目的接口就爆炸了,而是说一个接口也要保证职责单一,这样才能保证派生类的职责单一,其实和1.1是遥相呼应的。
1.6 迪米特原则
这个设计原则,伙伴们貌似挺多,但是了解的可能不多,那我说一个场景可能伙伴们就恍然大悟。
有三个模块,其中A和B,B和C是有直接的依赖关系,但是A和C之间没有直接的关联关系,如果模块A想要调用模块C中的某个方法,迪米特原则就是舍近求远,A可以通过B,在由B通过调用C中的方法,那么模块A和模块C就相对隔离,这样的好处在于:减少模块间的耦合,以及相互依赖。
1.7 合成复用原则
合成复用原则:在能通过组合的方式达成关联的情况下,尽量避免使用继承来实现。例如前面我们提到的Car类,它可以认为是一个抽象类,如果想要一个具体的Car类,那么可以通过继承Car类来实现。
那么合成复用原则想要我们尽量不要用继承来实现,为什么呢?我认为使用继承没有问题,但是如果因为实现逻辑导致继承的链条特别长,就不建议使用,如下图所示。
那么不用继承,组合的方式有哪些呢?如果熟悉装饰器模式的伙伴,或许能够理解其中的道理了,举个例子。
class BenzCarWrapper(val car: Car) {
fun setSpeed(speed: Int) {
car.setSpeed(speed)
}
fun getRunTime(duration: Long): Long {
return car.getRunTime(duration)
}
fun getName():String{
return "xxx"
}
}
BenzCarWrapper可以认为是对于Car的一次包装,除了能够调用Car中的方法,还能够扩展其他的方法,这里其实就没有通过继承来实现。
2 进入设计模式世界
通过前面对于设计原则的了解,其实都是为了给设计模式做铺垫,这里我不会讲所有的设计模式,因为在实际的项目中可能根本就用不到,或者使用的频率很低,这里主要介绍核心的设计模式,相信会对日常的业务开发有所帮助。
2.1 单例设计模式
不讲了,这个我相信是伙伴们用的最多的一种设计模式了。
2.2 工厂模式
工厂模式,我相信也是伙伴们听的最多的一种设计模式,但是在实际的开发过程中真正去使用工厂设计模式的却是很少。工厂模式的作用是什么呢?就是我们前面在介绍开闭原则的时候,提到的面向扩展开放,当我们设计接口并创建多个派生类的时候,如何去拿到每个派生类的实例对象,就会使用到工厂设计模式。
class SimpleFactory {
enum class SdkType(val index: Int) {
ASDK(1), BSDK(2)
}
companion object {
fun getSdk(type: Int): ISDK {
return when (type) {
SdkType.ASDK.index -> {
ASDKImpl()
}
SdkType.BSDK.index -> {
BSDKImpl()
}
else -> {
ASDKImpl()
}
}
}
}
}
首先我们先看下简单工厂设计模式,它的理念就是通过传入的参数看命中哪个枚举值或者其他的标识,来判断要具体生产哪个类型的SDK。
使用这种工厂模式的好处就是:将创建和调用分离开。通常我们在创建一个对象的时候,都是直接new出来,那么这样会带来一个问题就是,当这个对象我们不再使用的时候,就需要改原先的逻辑代码,将其换成新的类。
// 老版本使用ASDKImpl
val sdk1 = ASDKImpl()
sdk1.init()
// 新版本使用ASDKImpl的扩展版本v2
//val sdk1 = ASDKImpl()
val sdk1_v2 = ASDKV2Impl()
sdk1.init()
修改原先的代码逻辑是大忌,我们无法保证修改一定没有问题,但是工厂模式的优势在于,上层的业务逻辑不需要改动,例如下面的调用:
val sdk1 = SimpleFactory.getSdk(SimpleFactory.SdkType.ASDK.index)
sdk1.init()
当有SDK版本需要改动,只需要修改SimpleFactory中实现,将其换成扩展版本即可。
companion object {
fun getSdk(type: Int): ISDK {
return when (type) {
SdkType.ASDK.index -> {
// ASDKImpl()
ASDKV2Impl()
}
SdkType.BSDK.index -> {
BSDKImpl()
}
else -> {
ASDKImpl()
}
}
}
}
这种设计适应的场景是派生类的个数少,如果有成百上千的派生类,那么getSdk这个方法就会变得巨大不易维护,因此很少会用简单工厂设计模式。
这里我再简单介绍下工厂模式的另一个变种:工厂方法模式,其实和简单工厂不一样的是,每个产品都有自己对应的一个工厂,互不干扰。
interface IAbsFactory {
fun create():ISDK
}
class ASdkFactory : IAbsFactory {
override fun create(): ISDK {
return ASDKImpl()
}
}
class BSdkFactory : IAbsFactory {
override fun create(): ISDK {
return BSDKImpl()
}
}
那么在使用的时候,如果使用ASDK,那么就使用ASdkFactory创建即可,如果有涉及到ASDK的改动,那么也不需要上层业务发起修改,只需修改ASdkFactory的实现逻辑即可。
val sdk1 = ASdkFactory().create()
sdk1.init()
工厂方法模式存在的弊端就是当有新的产品出现之后,必须要创建一个新的Factory,因此针对这个问题,出现了第三种工厂设计模式:抽象工厂设计模式。
抽象工厂设计模式,是能够在一个工厂中生产不同的产品,看下面的示例:
interface IAbsFactory {
fun createSdkA():ISDK
fun createSdkB():ISDK
}
我们可以看到,在IAbsFactory接口中,声明了所有产品的创建方法,而且也遵循了接口隔离的原则,只负责生产,而没有其他额外的逻辑。
class SdkFactory : IAbsFactory {
override fun createSdkA(): ISDK {
return ASDKImpl()
}
override fun createSdkB(): ISDK {
return BSDKImpl()
}
}
val sdk1 = SdkFactory().createSdkA()
sdk1.init()
其实工厂设计模式的核心还是在于,业务层的调用与创建的隔离,提高维护性和扩展性,其实有点儿像依赖注入,有熟悉Dagger2和Hilt的伙伴应该有这个感受,还有就是Bitmap的创建,其实也是通过工厂模式来实现的,调用不同的方式逻辑,但是真正使用的时候,还是需要看场景,像抽象工厂模式,如果产品非常多,也会造成接口爆炸。
2.3 建造者设计模式
建造者设计模式,用于对外暴露这个类对象创建时,能够传入那些参数,从而创建一个对象,常见的就是创建Dialog的时候,传入一些必要的参数,创建一个Dialog并显示,这里就不详细介绍了,这个同上面几种设计模式一致,都是创建型的设计模式。
2.4 代理模式
代理模式主要分为两种:静态代理模式和动态代理模式。
首先我们先看一下静态代理模式,还是拿1.2中SDK的例子来说,如果我们想要对ASDKImpl的init方法执行前后加上一些逻辑的处理,那么如果使用静态代理,就需要SDKProxy代理类持有某个类的具体引用。
object SDKProxy : ISDK {
//代理SDK A
private var sdka: ASDKImpl? = null
fun setSdk(asdkImpl: ASDKImpl) {
this.sdka = asdkImpl
}
override fun init() {
if (sdka == null){
sdka = ASDKImpl()
}
//todo 方法执行前的处理
sdka?.init()
//todo 方法执行后的处理
}
override fun send() {
}
}
当然这种写法还有优化,因为SDK的实现类众多,如果需要持有每个实现类的引用不现实,其实可以通过面向接口编程,遵循依赖倒置原则,框架层只操作接口,上层可以传递对象的实现类。
object SDKProxy : ISDK {
//代理SDK A
private var sdka: ISDK? = null
fun setSdk(sdk: ISDK) {
this.sdka = sdk
}
override fun init() {
//todo 方法执行前的处理
sdka?.init()
//todo 方法执行后的处理
}
override fun send() {
}
}
SDKProxy.setSdk(ASDKImpl())
SDKProxy.init()
这个在实际的开发中其实也会经常用到,但是静态代理存在的一个问题就是,如果存在多种代理关系,即存在多个接口,那么就需要创建多个代理类,有没有可能只有一个代理类,就能够实现所有接口的代理,那么就引出了动态代理的概念。
其实动态代理出现的时候,面向的就是接口,只有通过接口才能完成动态代理,
class SDKProxy2 {
fun <T> proxy(t: T): T? {
return Proxy.newProxyInstance(
t!!::class.java.classLoader,
t!!::class.java.interfaces,
ProxyHandler(t)
) as? T
}
private inner class ProxyHandler<T>(val target: T) : InvocationHandler {
override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {
//todo 方法调用前的处理
Log.e("TAG", "方法调用前的处理")
val obj = if (args.isNullOrEmpty()) {
method?.invoke(target)
} else {
method?.invoke(target, args)
}
//todo 方法调用后的处理
Log.e("TAG", "方法调用后的处理")
return obj
}
}
}
其实我们很容易就能看到和静态代理的区别,动态代理我们也可以传入一个实例对象,但是不需要像静态代理那样,还需要处理方法的执行,当通过newProxyInstance创建一个代理对象之后,调用其中的方法,例如init方法,那么就会走到InvocationHandler中的invoke方法中,在这里也可以做方法执行前的逻辑处理。
val sdkA = SdkFactory().createSdkA()
SDKProxy2().proxy(sdkA)?.init()
使用动态代理,很大程度上也是为了调用与实现的隔离,只不过是动态代理的灵活性更强,甚至能够影响方法执行的逻辑;而且只需要一个代理类就可以完成所有接口的代理。
2.5 桥接模式
桥接模式,其实就是为了遵循合成复用的原则,避免通过静态的继承关系造成类与类之间的强耦合关系。
举个例子:
interface ICar {
fun getName(): String
}
class SimpleCar : ICar {
override fun getName(): String {
return "普通的车"
}
}
这是一辆普通的车,在此基础上,我想给它染成红色,变成红色的车,这样就需要继承(为啥要继承SimpleCar,实现ICar接口不可以吗?其实继承父类目的肯定也是想要使用父类中的一些属性或者方法)
class RedCar : SimpleCar() {
override fun getName(): String {
return "红色的车"
}
}
那么这样一来,其实违背了里氏替换原则,相当于把父类的方法完全重写了;而且还有一个问题就是,如果属性注解增加,就会一直需要继承,因此桥接模式出现,就是为了解决这个问题。
因为颜色是一个属性,而且是一个抽象的属性,具体有红色、白色、蓝色等等,因此可以把颜色也做一次抽象。
interface IColor {
fun getColorName():String
}
class RedColor : IColor {
override fun getColorName(): String {
return "红色"
}
}
对原先的ICar接口也做一次改造,增加一个设置颜色的方法。
interface ICar {
fun getName(): String
fun setColor(color: IColor)
}
最终的实现如下:
open class SimpleCar : ICar {
private var color: IColor? = null
override fun getName(): String {
return "普通的${color?.getColorName()}车"
}
override fun setColor(color: IColor) {
this.color = color
}
}
其实桥接模式还是比较简单的,整体的思想还是避免过度的继承,与下面要介绍的装饰器模式有点儿类似。
2.6 装饰器模式
装饰器模式,目的在于不改变当前对象结构的情况下,动态地为该对象增加一些职责,常见的就是InputStream和OutputStream,它们有很多对应装饰对象,例如FileInputStream、BufferedInputStream等。
我们举一个经典的例子,珍珠奶茶
interface IMilkTea {
fun create()
}
class SimpleMilkTea : IMilkTea {
override fun create() {
Log.e("TAG","这是一杯原味奶茶")
}
}
接下来,我们需要一个抽象的装饰器,用来扩展奶茶类的实现。
abstract class AbsDecorate(val milktea: IMilkTea) : IMilkTea {
override fun create() {
milktea.create()
}
}
珍珠奶茶的具体实现类。
class ZhenzhuMT(milktea:IMilkTea) : AbsDecorate(milktea) {
override fun create() {
super.create()
Log.e("TAG","这杯加了珍珠")
}
}
val mt = ZhenzhuMT(SimpleMilkTea())
mt.create()
这样的话,我们就是在原有SimpleMilkTea的基础上了,新增了ZhenzhuMT的业务,而且并没有影响到SimpleMilkTea业务的原有逻辑。
2.7 门面设计模式
门面设计模式,又称为外观设计模式,也是我们在日常开发中最常用的一个设计模式之一,只不过我们并没有认知到。当我们在使用第三方的SDK的时候,其实有很多细节的实现,但对于外层的调用者来说,它不需要关心内部的实现逻辑,例如网络库,调用者不需要关系它是OkHttp还是Retrofit,一个好的门面甚至都不能让用户知道这个是什么东西,只需要暴露一些接口,用户发起请求拿到服务端数据即可。
class Facede {
private val asdk:ASDKImpl = ASDKImpl()
fun init(){
asdk.init()
}
}
详细的代码就不写了,Facede属于唯一对外暴露的类,当ASDKImpl中的init发生修改之后,其实上层的调用是不受影响的。
2.8 享元模式
享元模式,目的就是为了避免重复地创建对象,像在使用代理模式时,每次创建对象都是通过new出来一个新的对象。
class SdkFactory : IAbsFactory {
override fun createSdkA(): ISDK {
return ASDKImpl()
}
override fun createSdkB(): ISDK {
return BSDKImpl()
}
}
虽然这也是临时变量,方法执行完毕之后就会被虚拟机回收,但是在GC之前,这些临时对象依然占用JVM的内存,会导致GC提前,因此享元模式就是为了解决这些问题。
class SharedSDKFactory {
private val sharedMap: MutableMap<String, ISDK> by lazy {
mutableMapOf()
}
fun getComponent(key: String): ISDK? {
if (sharedMap.containsKey(key)) {
return sharedMap[key]
} else {
//创建新的SDK
val sdk = ASDKImpl()
sharedMap[key] = sdk
return sdk
}
}
}
在SharedSDKFactory中,采用Map将注册过的实现类存储起来,每个SDKImpl对应一个Key,在没有获取到实例的时候,需要重新创建一个;如果存在,那么就直接从缓存中获取。
class SdkFactory : IAbsFactory {
override fun createSdkA(): ISDK? {
return SharedSDKFactory.getComponent("A")
}
override fun createSdkB(): ISDK {
return BSDKImpl()
}
}
前面这8种设计模式,主要介绍了创建型模式和结构型模式中一些比较经典的设计模式,还有一些可能平时用到的设计模式就没有在这里写,在下篇文章中,我会介绍最后一个大类就是行为型的设计模式,其中像策略设计模式、责任链设计模式、观察者设计模式、迭代器等都是经常会用到的,好了,这篇文章就到这里,未完待续~