盘点那些开发中用到的设计模式(第一部分)

537 阅读8分钟

前言

这段时间一直在解析Android常用的开源框架(绞尽脑汁),关于网络框架中的OkHttp和retrofit(目前进度),其中对于retrofit用到了非常多的设计模式,所以为了更好的理解源码,这里提前复习下设计模式

可以看看retrofit中用到哪些设计模式

1.Retrofit构建过程 
建造者模式、工厂方法模式

2.创建网络请求接口实例过程
外观模式、代理模式、单例模式、策略模式、装饰模式(建造者模式)

3.生成并执行请求过程
适配器模式(代理模式、装饰模式)

工厂方法模式

适用于复杂对象的创建,定义一个创建对象的类,让子类去决定实例化哪个类

实现方式

下面我们来实际下工厂模式如何实现

  • 创建产品,实现公共接口
interface Product {
    val name:String
    fun show()
}
  • 创建具体产品多个,需要继承Product
//具体产品A
class ProductA(override val name: String = "ProductA") : Product {
    override fun show() {
        Log.e("ProductA", "show: ")
    }
}

//具体产品B
class ProductB(override val name: String = "ProductB") : Product {
    override fun show() {
        Log.e("ProductB", "show: ")
    }
}
  • 创建抽象工厂,实现公共接口
interface Factory {
    fun makeProduct() : Product
}
  • 实现具体工厂,继承Factory
class FactoryA : Factory {
    override fun makeProduct(): Product {
        return ProductA()
    }
}

class FactoryB:Factory {
    override fun makeProduct(): Product {
        return ProductB()
    }

}

Android中的应用

下面以BitmapFactory为例,生成

// 生成 Bitmap 对象的工厂类 BitmapFactory
public class BitmapFactory {
    ···
    public static Bitmap decodeFile(String pathName) {
        ···
    }
    ···

    public static Bitmap decodeResource(Resources res, int id, Options opts) {
        validate(opts);
        Bitmap bm = null;
        InputStream is = null; 
        
        try {
            final TypedValue value = new TypedValue();
            is = res.openRawResource(id, value);

            bm = decodeResourceStream(res, value, is, null, opts);
        } 
        ···
        return bm;
    }
    ···
}

总结

  • 优点
  1. 符合单一职责原则,每个具体工厂只负责对应的产品
  2. 当我们需要新增产品的时候,只需要新增一个产品的具体类和相应工厂的子类
  • 缺点
  1. 一个具体工厂类只能创建一个具体产品,如果需要新增一种产品的话,我们还需要创建一个新的对应工厂类,这样无疑会增加开销

建造者模式

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示,适用于初始化的对象比较复杂且参数较多的情况。

实现方式

  • 首先我们看下创建个dialog是怎么用的,怎么去构建复杂对象的,在kotlin中一般使用apply函数就可以实现了
  fun createDialog(context: Context) {
        val dialog = Dialog(context).apply {
            setTitle("DialogA")
            setCancelable(true)
            setCanceledOnTouchOutside(true)
            setContentView(contentView)
        }
    }
  • 看到上面Lambda表达式里可以调用**Dialog.show()**等其他与构建对象无关的方法,或者不想公开构造函数,只想通过Builder来构建对象,这时可以使用Type-Safe Builders
class CarsBuilder
    (
    val model: String?,
    val name: String?,
    val year: Int
) {

    //使用的是Types-Builder来进行创建
     constructor(builder: Builder) : this(builder.model, builder.name,builder.year)

    class Builder {
        var model:String? = null
        var name:String? = null
        var year:Int = -1

        fun build() = CarsBuilder(this)
    }

    companion object {
        inline fun build(block: Builder.() -> Unit) = Builder().apply(block).build()
    }
}

val cars = CarsBuilder.build {
    model = "丰田凯美瑞"
    name = "双擎2021"
    year = 2021
}

  • 这样写的好处在于Builder类中的成员函数返回Builder对象自身,让它支持链式调用,如果只想传入部分参数,即可在定义build的时候写就可以了,如果之后需要用新的配置参数,直接添加进来,这样也提高了代码的可扩展性和可读性

现在我们来简单总结下Builder模式的创建

  1. 定义内部类Builder,它的成员变量和外部类是一样的
  2. 构造方法对成员变量赋值,然后返回自己本身this
  3. 创建一个外部创建方法build(),使用Type-Safe Builders来进行创建,外部类提供一个私有构造函数供内部类调用,在该函数中完成成员变量的赋值,取值为Builder对象中对应的成变量的值

Android中的应用

  • 对话框的创建 可以看下AlertDialog。Builder()
 fun showDialog(context: Context) {
        val builder =  AlertDialog.Builder(context)
            .setTitle("TEST")
            .setMessage("Test")

        val dialog = builder.create()
        dialog.show()
    }

AlertDialog.java

public class AlertDialog extends Dialog implements DialogInterface {
    ···

    public static class Builder {
        private final AlertController.AlertParams P;
        ···

        public Builder(Context context) {
            this(context, resolveDialogTheme(context, ResourceId.ID_NULL));
        }
        ···

        public Builder setTitle(CharSequence title) {
            P.mTitle = title;
            return this;
        }
        ···

        public Builder setMessage(CharSequence message) {
            P.mMessage = message;
            return this;
        }
        ···

        public AlertDialog create() {
            // Context has already been wrapped with the appropriate theme.
            final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
            P.apply(dialog.mAlert);
            ···
            return dialog;
        }
        ···
    }
}
  • 网络框架Okhttp(之前写的开源框架有说到过)
  val okHttpClient = OkHttpClient()
  val request = Request.Builder()
            .url(BASE_URL)
            .build()

总结

  • 优点
  1. Builder模式的好处在于将配置的构建和表示抽离出来,避免写过多的setter,getter方法;
  2. builder创建常用链式调用,这样代码显得简洁,易懂
  • 缺点 因为内部和外部都有引用,相对来说,内存消耗会比较大,但是对于现在手机应用来说,问题不太

单例模式

确保某一个类只有一个实例,并自动实例化向整个系统提供这个实例,且可以避免产生多个对象消耗资源。

对于上述单例模式的定义,可以说是很官方了,在移动开发中具体有什么好处呢?在于它至始至终都只有一个实例,每次只实例一次大大提高了我们的性能

实现方式

对于单例模式大致实现方式有两种:饿汉模式和懒汉模式

饿汉模式

对于kotlin实现方式,直接调用object实现即可

object SingleTest {
  ...
}

//kotlin调用
SingleTest.xx()
//java调用
SingleTest.INSTANCE.xx()

这个其实和饿汉式实现原理是一样的,其实可以反编译看看

public final class SingleTest {
   @NotNull
   public static final SingleTest INSTANCE;

   private SingleTest() {
   }

   static {
      SingleTest var0 = new SingleTest();
      INSTANCE = var0;
   }
}

实际上看出object还是利用了INSTANCE静态变量,这样看来在类加载的时候就去实例化了,但是还是会存在一些问题,在这个object类不允许有自定义构造函数constractor;由于一开始创建了对象但是并没有调用,会导致资源浪费;如果在里面做了耗时操作的话,会导致加载较慢

懒汉模式

这种单例是在你调用的时候再去实例化,在kotlin当中Lazy就刚好适合这种场景,可以看下使用方式

class SingleTon constructor() {

   companion object {
       val instance: SingleTon by lazy { SingleTon() }
   }
}

这样看起来是不是简洁了许多,那么延迟属性Lazy到底做了什么操作呢?其实它默认也是线程安全的,synchronized 修饰方法、双重检查锁定、静态内部类都是线程安全的懒汉式

public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE    // 声明为 volatile 变量
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this
    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {  // 第一次检查
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }
            return synchronized(lock) {  // 加锁锁定
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {  // 第二次检查
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                }
                else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
    ...
}
  • 可以看到延迟属性Lazy的代码中也是用到了双重校验模式进行延迟初始化,同样是线程安全的,相对来说调用起来简洁方便了许多
  • 其中可以传入三种线程安全模式LazyThreadSafetyMode,默认是LazyThreadSafetyMode.SYNCHRONIZED同步锁
    • 第一种SYNCHRONIZED,使用同步锁保证只有一个线程可以进行实例化
    • PUBLICATION,可以允许多个线程进行实例化,但是最先返回的那个线程才会做为lazy的初始化实例.
    • NONE,不是线程安全的,也没有任何保证和开销(我是基本用不到)

Android中的应用

在输入法开发当中,使用到的InputMethodManager,这个类初始化的时候就用到了单例模式,用于控制显示或隐藏输入法面板,下面是它的代码

InputMethodManager.java

/**
* Retrieve the global InputMethodManager instance, creating it if it
* doesn't already exist.
* @hide
*/
public static InputMethodManager getInstance() {
    synchronized (InputMethodManager.class) {
        if (sInstance == null) {
            try {
                sInstance = new InputMethodManager(Looper.getMainLooper());
            } catch (ServiceNotFoundException e) {
                throw new IllegalStateException(e);
            }
        }
        return sInstance;
    }
}

总结

优缺点

  • 优点
  1. 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就防止其它对象对自己的实例化,确保所有的对象都访问一个实例
  2. 提供了对唯一实例的受控访问。
  3. 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
  4. 避免对共享资源的多重占用。
  • 缺点
  1. 如果对于同一个对象要在不同的场景中发送变化的,使用单例会引发数据错误,不能保存彼此的状态
  2. 如果使用的单例对象的功能过于繁重,在一定程度上违背了单一职责原则(具体可以去看面向对象设计原则)
  3. 不要泛用单例模式,如果你去实例化的对象没有被调用,有可能会被系统GC,从而对象状态就丢失了

适用场景

单例模式因为只实例化一个对象,所以那些对象可以公用的场景都可以使用

  1. 需要频繁访问数据库的对象

  2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。

  3. 有状态的工具类对象。

  4. 那种频繁调用又被销毁的对象