谈到规划形式,咱们许多同伴或许都了解一二,看到源码中的一些规划形式后,茅塞顿开原来代码能够这么写,但真实自己下手的时分,反而会把这些理念抛在了脑后。所以假如把娴熟运用规划形式分个等级(最高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 迪米特准则

这个规划准则,同伴们形似挺多,可是了解的或许不多,那我说一个场景或许同伴们就茅塞顿开。

Android进阶宝典 -- 深究23种设计模式(上)

有三个模块,其间A和B,B和C是有直接的依靠联系,可是A和C之间没有直接的相关联系,假如模块A想要调用模块C中的某个办法,迪米特准则便是舍近求远,A能够经过B,在由B经过调用C中的办法,那么模块A和模块C就相对阻隔,这样的优点在于:减少模块间的耦合,以及相互依靠。

1.7 组成复用准则

组成复用准则:在能经过组合的方式达成相关的情况下,尽量防止运用承继来完成。例如前面咱们说到的Car类,它能够认为是一个笼统类,假如想要一个具体的Car类,那么能够经过承继Car类来完成。

那么组成复用准则想要咱们尽量不要用承继来完成,为什么呢?我认为运用承继没有问题,可是假如由于完成逻辑导致承继的链条特别长,就不建议运用,如下图所示。

Android进阶宝典 -- 深究23种设计模式(上)

那么不用承继,组合的方式有哪些呢?假如了解装修器形式的同伴,或许能够了解其间的道理了,举个比如。

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,一个好的门面乃至都不能让用户知道这个是什么东西,只需求露出一些接口,用户发起恳求拿到服务端数据即可。

Android进阶宝典 -- 深究23种设计模式(上)

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种规划形式,首要介绍了创立型形式和结构型形式中一些比较经典的规划形式,还有一些或许平时用到的规划形式就没有在这儿写,鄙人篇文章中,我会介绍最后一个大类便是行为型的规划形式,其间像策略规划形式、职责链规划形式、观察者规划形式、迭代器等都是经常会用到的,好了,这篇文章就到这儿,未完待续~