Android常用设计模式之原型模式详解

手机APP/开发
310
0
0
2023-06-03
标签   Android
目录
  • 前言
  • 一、基本使用
  • 二、对象与集合的使用
  • 三、浅拷贝与深拷贝
  • 四、Kotlin的应用
  • 总结

前言

什么是原型模式?

它是指创建对象的种类,并通过拷贝这些原型创建新的对象。

它是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。

原型模式的工作原理很简单:将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程。由于在软件系统中我们经常会遇到需要创建多个相同或者相似对象的情况,因此原型模式在真实开发中的使用频率还是非常高的。

一、基本使用

固定的用法就是 实现 Cloneable 接口 ,重写 clone()方法。

public class UserProfile implements Cloneable {
    public String userId;
    public String name;
    public String age;
    public UserProfile() {
    }
    public UserProfile(String userId, String name, String age) {
        this.userId = userId;
        this.name = name;
        this.age = age;
    }
    @NonNull
    @Override
    public UserProfile clone() throws CloneNotSupportedException {
        return (UserProfile) super.clone();
    }
}

使用:

            val userProfile = UserProfile("1", "张三", "30")
            YYLogUtils.w("userProfile:$userProfile  name:" + userProfile.name + " age:" + userProfile.age + " skill:" + userProfile.skills)
            try {
                val newUser = userProfile.clone()
                newUser.name = "李四"
                YYLogUtils.w("userProfile:$newUser  name:" + newUser.name + " age:" + newUser.age + " skill:" + newUser.skills)
            } catch (e: Exception) {
                e.printStackTrace()
            }

二、对象与集合的使用

对集合与对象的的处理需要额外的注意。

public class UserProfile implements Cloneable {
    public String userId;
    public String name;
    public String age;
    public List<String> skills;
    public UserProfile() {
    }
    public UserProfile(String userId, String name, String age) {
        this.userId = userId;
        this.name = name;
        this.age = age;
    }
    @NonNull
    @Override
    public UserProfile clone() throws CloneNotSupportedException {
        UserProfile profile = (UserProfile) super.clone();
        profile.name = this.name;
        profile.age = this.age;
        profile.userId = this.userId;
        profile.skills = (List<String>) this.skills.clone();
        return profile;
    }
}

例如我们自己处理数据的赋值,那么就会出现这样的问题,List是无法clone的。

如果强制这么使用

           val userProfile = UserProfile("1", "张三", "30")
            val skills = listOf("篮球", "游泳", "长跑", "Java")
            userProfile.skills = skills
            YYLogUtils.w("userProfile:$userProfile  name:" + userProfile.name + " age:" + userProfile.age + " skill:" + userProfile.skills)
            try {
                val newUser = userProfile.clone()
                newUser.name = "李四"
                newUser.skills.add("H5")
                YYLogUtils.w("userProfile:$newUser  name:" + newUser.name + " age:" + newUser.age + " skill:" + newUser.skills)
            } catch (e: Exception) {
                e.printStackTrace()
            }

就会报错:

我们需要改为ArrayList,因为只有它才显示了Cloneable,也就是只有内部成员属性都实现 Cloneable 接口才可以使用

那我们修改类的成员定义为ArrayList

public class UserProfile implements Cloneable {
    public String userId;
    public String name;
    public String age;
    public ArrayList<String> skills = new ArrayList<>();
    public UserProfile() {
    }
    public UserProfile(String userId, String name, String age) {
        this.userId = userId;
        this.name = name;
        this.age = age;
    }
    @NonNull
    @Override
    public UserProfile clone() throws CloneNotSupportedException {
     //两种方法都可以
//        return (UserProfile) super.clone();
        UserProfile profile = (UserProfile) super.clone();
        profile.name = this.name;
        profile.age = this.age;
        profile.userId = this.userId;
        profile.skills = (ArrayList<String>) this.skills.clone();
        return profile;
    }
}

我们就能打印正确的拷贝值:

集合是可以了,那么我们能不能添加自定义的对象呢?又会怎么样?

我们再UserProfile类中添加一个Address的对象

public class UserProfile implements Cloneable {
    public String userId;
    public String name;
    public String age;
    public UserAddress address;
    public ArrayList<String> skills = new ArrayList<>();
    public UserProfile() {
    }
    public UserProfile(String userId, String name, String age) {
        this.userId = userId;
        this.name = name;
        this.age = age;
    }
    @NonNull
    @Override
    public UserProfile clone() throws CloneNotSupportedException {
       return (UserProfile) super.clone();
    }
}

测试一下赋值与拷贝

        fun prototypeTest() {
            val userProfile = UserProfile("1", "张三", "30")
            val skills = arrayListOf("篮球", "游泳", "长跑", "Java")
            userProfile.address = UserAddress("武汉", "楚河汉街")
            userProfile.skills = skills
            YYLogUtils.w(
                "userProfile:$userProfile  name:" + userProfile.name + " age:" + userProfile.age +
                        " skill:" + userProfile.skills + "address:" + userProfile.address + " address-city:" + userProfile.address.city
            )
            try {
                val newUser = userProfile.clone()
                newUser.name = "李四"
                newUser.skills.add("H5")
                newUser.address.city = "长沙"
                YYLogUtils.w(
                    "userProfile:$newUser  name:" + newUser.name + " age:" + newUser.age +
                            " skill:" + newUser.skills + "address:" + newUser.address + " address-city:" + newUser.address.city
                )
                YYLogUtils.w(
                    "userProfile:$userProfile  name:" + userProfile.name + " age:" + userProfile.age +
                            " skill:" + userProfile.skills + "address:" + userProfile.address + " address-city:" + userProfile.address.city
                )
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

打印的结果如下:

可以看到 userProfile 与 newUser 中的 Address 是同一个对象,如果一个对象修改了 Address 的值,那么另一个对象的值就改了。

要修改起来其实很简单,我们把 Address 对象重写 Cloneable 并实现 clone 方法。

public class UserAddress implements Cloneable {
    public String city;
    public String address;
    public UserAddress() {
    }
    public UserAddress(String city, String address) {
        this.city = city;
        this.address = address;
    }
    @NonNull
    @Override
    public UserAddress clone() throws CloneNotSupportedException {
        return (UserAddress) super.clone();
    }
}

然后在UserProfile的clone方法中手动的对对象赋值

public class UserProfile implements Cloneable {
    public String userId;
    public String name;
    public String age;
    public UserAddress address;
    public ArrayList<String> skills = new ArrayList<>();
    public UserProfile() {
    }
    public UserProfile(String userId, String name, String age) {
        this.userId = userId;
        this.name = name;
        this.age = age;
    }
    @NonNull
    @Override
    public UserProfile clone() throws CloneNotSupportedException {
        UserProfile profile = (UserProfile) super.clone();
        // 把地址也做一次克隆,达到深拷贝
        profile.address = address.clone();
        return profile;
    }
}

我们再次运行就可以得到我们想要的结果了:

三、浅拷贝与深拷贝

浅拷贝只会复制值,深拷贝不仅会复制值也会复制引用

浅拷贝又叫影子拷贝,上面我们在拷贝文档时并没有把原文档中的字段都重新构造了一遍,而只是拷贝了引用,也就是Address字段引用了之前的Address字段,这样的话修改新的Address中的内容就会连原Address也改掉了,这就是浅拷贝。

深拷贝就是在浅拷贝的基础上,对于引用类型的字段也要采用拷贝的形式,比如上面的Address,而像String、int这些基本数据类型则没关系

平常的开发中更推荐大家使用深拷贝,就是对象实现 Cloneable 并重写 clone 方法。

例如这样深拷贝:

    @NonNull
    @Override
    public UserProfile clone() throws CloneNotSupportedException {
        UserProfile profile = (UserProfile) super.clone();
        // 把地址也做一次克隆,达到深拷贝
        profile.address = address.clone();
        return profile;
    }

当然还有另一种用法,既不是浅拷贝,也不是深拷贝,例如Intent的clone方法

@Override
public Object clone() {
    return new Intent(this);
}
private Intent(Intent o, boolean all) {
    this.mAction = o.mAction;
    this.mData = o.mData;
    this.mType = o.mType;
    this.mPackage = o.mPackage;
    this.mComponent = o.mComponent;
    if (o.mCategories != null) {
        this.mCategories = new ArraySet<String>(o.mCategories);
    }
}

Intent 的 clone 方法实际上是通过new对象的方法来实现的,并没有调用super.clone()。

四、Kotlin的应用

那么在Kotlin中的使用又有什么不同呢?

其实在Kotlin中调用普通的类是和Java一样的,但是如果是data class 的数据类,内部有copy方法可以快速实现clone的对象。

那么它是浅拷贝还是深拷贝呢?

data class Company(
    var name: String,
    var year: String,
    var address: UserAddress,
)

使用:

    val company = Company("百度", "2001年", UserAddress("北京", "海淀区"))
    YYLogUtils.w("company:$company")
    val newCompany = company.copy()
    newCompany.name = "网易"
    newCompany.address.city = "杭州"
    YYLogUtils.w("newCompany:$newCompany")
    YYLogUtils.w("company:$company")

我们再 data class 中定义一个普通的对象 Address ,我们打印的值如下:

可以看到打印的结果,默认的 data class copy 为浅拷贝。

那么如何实现深拷贝呢?我们仿造Java写 Cloneable 和 clone() 试试。

data class Company(
    var name: String,
    var year: String,
    var address: UserAddress,
) : Cloneable {
    public override fun clone(): Company {
        val company: Company = super.clone() as Company
        company.address = address.clone()
        return company
    }
}

使用的时候我们不使用copy()而使用clone()

    val company = Company("百度", "2001年", UserAddress("北京", "海淀区"))
    YYLogUtils.w("company:$company addressCity:${company.address.city}")
    val newCompany = company.clone()
    newCompany.name = "网易"
    newCompany.address.city = "杭州"
    YYLogUtils.w("newCompany:$newCompany  addressCity:${newCompany.address.city}")
    YYLogUtils.w("company:$company  addressCity:${company.address.city}")

那么打印的结果确实成了深拷贝,就是我们想要的。

另外一些其他的方法,例如一些第三库 github.com/bennyhuo/Ko… 给对象 data class 加注解,让其实现深拷贝的逻辑。

还有一些方法是通过Kotlin反射库的一些方式实现,扩展方法如下

//data class 对象的深拷贝
fun <T : Any> T.deepCopy(): T {
    //如果不是数据类,直接返回
    if (!this::class.isData) {
        return this
    }
    //拿到构造函数
    return this::class.primaryConstructor!!.let { primaryConstructor ->
        primaryConstructor.parameters.map { parameter ->
            //转换类型
            //最终value=第一个参数类型的对象
            val value = (this::class as KClass<T>).memberProperties.first {
                it.name == parameter.name
            }.get(this)
            //如果当前类(这里的当前类指的是参数对应的类型,比如说这里如果非基本类型时)是数据类
            if ((parameter.type.classifier as? KClass<*>)?.isData == true) {
                parameter to value?.deepCopy()
            } else {
                parameter to value
            }
            //最终返回一个新的映射map,即返回一个属性值重新组合的map,并调用callBy返回指定的对象
        }.toMap().let(primaryConstructor::callBy)
    }
}

如果报错的话,可以看看是不是Kotlin反射库没有导入,比如我项目的Kotlin版本为1.5.31 , 那么我导入了一个反射库依赖如下:

api 'org.jetbrains.kotlin:kotlin-reflect:1.5.31'

使用起来很简单,直接使用扩展方法即可:

    val company = Company("百度", "2001年", Address("北京", "海淀区"))
    YYLogUtils.w("company:$company addressCity:${company.address.city}")
    val newCompany = company.deepCopy()
    newCompany.name = "网易"
    newCompany.address.city = "杭州"
    YYLogUtils.w("newCompany:$newCompany  addressCity:${newCompany.address.city}")
    YYLogUtils.w("company:$company  addressCity:${company.address.city}")

打印的结果符合我们的预期:

这种方法需要注意的是只支持 data class ,内部的对象也需要时data class 如果是普通类或Java类的对象都是不行的。并且大家需要注意的是这么,深拷贝的性能是比浅拷贝要差一点的。特别是使用这样扩展方式的形式,他的性能比重写 Cloneable 和 clone()这样的方式性能要差。如果没有必要还是推荐使用浅拷贝实现。

比如之前的文章 MVI框架的展示。我们就是使用浅拷贝,我们就是要改变内存中List的值

    private val _viewStates: MutableLiveData<Demo14ViewState> = MutableLiveData(Demo14ViewState())
    //只需要暴露一个LiveData,包括页面所有状态
    val viewStates: LiveData<Demo14ViewState> = _viewStates
    //当前页面所需的数据与状态
    data class Demo14ViewState(
        val industrys: List<Industry> = emptyList(),
        val schools: List<SchoolBean> = emptyList(),
        var isChanged: Boolean = false
    ) : BaseViewState()
    //获取学校数据
    private fun requestSchool() {
        viewModelScope.launch {
            //开始Loading
            loadStartLoading()
            val result = mRepository.getSchool()
            result.checkSuccess {
                _viewStates.setState {
                    copy(schools = it ?: emptyList())
                }
            }
            loadHideProgress()
        }
    }

例如上文中的copy用法,浅拷贝,当我们调用接口获取到学校的是时候就赋值学校数据,浅拷贝一个对象,并对它赋值,再把它设置给LiveData,当我们获取到行业数据的时候,一样的操作,又并不会对学校的数据做修改,又修改了原生LiveData中的Value值,因为两个对象的引用是不同的,这样就可以触发LiveData的通知。也是一个比较典型的应用。

如果是深拷贝,当然也能实现同样的逻辑,就是会麻烦一点,性能也没有浅拷贝好,所以这个场景使用的是简单的浅拷贝。

总结

原型模式本质上就是对象的拷贝,容易出现的问题也都是深拷贝、浅拷贝。使用原型模式可以解决构建复杂对象的资源消耗问题,能够在某些场景下提升创建对象的效率。

优点:

  • 原型模式是在内存中二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量对象时,原型模式可能更好的体现其优点。
  • 还有一个重要的用途就是保护性拷贝,也就是对某个对象对外可能是只读的,为了防止外部对这个只读对象的修改,通常可以通过返回一个对象拷贝的形式实现只读的限制。

缺点:

  • 这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的,在实际开发中应该注意这个潜在问题。优点是减少了约束,缺点也是减少了约束,需要大家在实际应用时考虑。
  • 通过实现Cloneable接口的原型模式在调用clone函数构造实例时并不一定比通过new操作速度快,只有当通过new构造对象较为耗时或者说成本较高时,通过clone方法才能够获得效率上的提升。

平常的开发中,浅拷贝和深拷贝都有各自的应用场景,如果 class 中都是基础数据,那也不需要关心是浅拷贝还是深拷贝,直接浅拷贝即可,如果包含对象,那么就要看自己的需求来选择是浅拷贝还是深拷贝了。