Android常用設(shè)計(jì)模式之原型模式詳解
前言
什么是原型模式?
它是指創(chuàng)建對(duì)象的種類,并通過拷貝這些原型創(chuàng)建新的對(duì)象。
它是用于創(chuàng)建重復(fù)的對(duì)象,同時(shí)又能保證性能。這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。
這種模式是實(shí)現(xiàn)了一個(gè)原型接口,該接口用于創(chuàng)建當(dāng)前對(duì)象的克隆。當(dāng)直接創(chuàng)建對(duì)象的代價(jià)比較大時(shí),則采用這種模式。
原型模式的工作原理很簡(jiǎn)單:將一個(gè)原型對(duì)象傳給那個(gè)要發(fā)動(dòng)創(chuàng)建的對(duì)象,這個(gè)要發(fā)動(dòng)創(chuàng)建的對(duì)象通過請(qǐng)求原型對(duì)象拷貝自己來實(shí)現(xiàn)創(chuàng)建過程。由于在軟件系統(tǒng)中我們經(jīng)常會(huì)遇到需要?jiǎng)?chuàng)建多個(gè)相同或者相似對(duì)象的情況,因此原型模式在真實(shí)開發(fā)中的使用頻率還是非常高的。
一、基本使用
固定的用法就是 實(shí)現(xiàn) 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()
}


二、對(duì)象與集合的使用
對(duì)集合與對(duì)象的的處理需要額外的注意。
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;
}
}
例如我們自己處理數(shù)據(jù)的賦值,那么就會(huì)出現(xiàn)這樣的問題,List是無法clone的。
如果強(qiáng)制這么使用
val userProfile = UserProfile("1", "張三", "30")
val skills = listOf("籃球", "游泳", "長(zhǎng)跑", "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()
}
就會(huì)報(bào)錯(cuò):

我們需要改為ArrayList,因?yàn)橹挥兴棚@示了Cloneable,也就是只有內(nèi)部成員屬性都實(shí)現(xiàn) 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;
}
}
我們就能打印正確的拷貝值:

集合是可以了,那么我們能不能添加自定義的對(duì)象呢?又會(huì)怎么樣?
我們?cè)賃serProfile類中添加一個(gè)Address的對(duì)象
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();
}
}
測(cè)試一下賦值與拷貝
fun prototypeTest() {
val userProfile = UserProfile("1", "張三", "30")
val skills = arrayListOf("籃球", "游泳", "長(zhǎng)跑", "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 = "長(zhǎng)沙"
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()
}
}
打印的結(jié)果如下:

可以看到 userProfile 與 newUser 中的 Address 是同一個(gè)對(duì)象,如果一個(gè)對(duì)象修改了 Address 的值,那么另一個(gè)對(duì)象的值就改了。
要修改起來其實(shí)很簡(jiǎn)單,我們把 Address 對(duì)象重寫 Cloneable 并實(shí)現(xiàn) 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方法中手動(dòng)的對(duì)對(duì)象賦值
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();
// 把地址也做一次克隆,達(dá)到深拷貝
profile.address = address.clone();
return profile;
}
}
我們?cè)俅芜\(yùn)行就可以得到我們想要的結(jié)果了:

三、淺拷貝與深拷貝
淺拷貝只會(huì)復(fù)制值,深拷貝不僅會(huì)復(fù)制值也會(huì)復(fù)制引用
淺拷貝又叫影子拷貝,上面我們?cè)诳截愇臋n時(shí)并沒有把原文檔中的字段都重新構(gòu)造了一遍,而只是拷貝了引用,也就是Address字段引用了之前的Address字段,這樣的話修改新的Address中的內(nèi)容就會(huì)連原Address也改掉了,這就是淺拷貝。
深拷貝就是在淺拷貝的基礎(chǔ)上,對(duì)于引用類型的字段也要采用拷貝的形式,比如上面的Address,而像String、int這些基本數(shù)據(jù)類型則沒關(guān)系
平常的開發(fā)中更推薦大家使用深拷貝,就是對(duì)象實(shí)現(xiàn) Cloneable 并重寫 clone 方法。
例如這樣深拷貝:
@NonNull
@Override
public UserProfile clone() throws CloneNotSupportedException {
UserProfile profile = (UserProfile) super.clone();
// 把地址也做一次克隆,達(dá)到深拷貝
profile.address = address.clone();
return profile;
}
當(dāng)然還有另一種用法,既不是淺拷貝,也不是深拷貝,例如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 方法實(shí)際上是通過new對(duì)象的方法來實(shí)現(xiàn)的,并沒有調(diào)用super.clone()。
四、Kotlin的應(yīng)用
那么在Kotlin中的使用又有什么不同呢?
其實(shí)在Kotlin中調(diào)用普通的類是和Java一樣的,但是如果是data class 的數(shù)據(jù)類,內(nèi)部有copy方法可以快速實(shí)現(xiàn)clone的對(duì)象。
那么它是淺拷貝還是深拷貝呢?
data class Company(
var name: String,
var year: String,
var address: UserAddress,
)
使用:
val company = Company("百度", "2001年", UserAddress("北京", "海淀區(qū)"))
YYLogUtils.w("company:$company")
val newCompany = company.copy()
newCompany.name = "網(wǎng)易"
newCompany.address.city = "杭州"
YYLogUtils.w("newCompany:$newCompany")
YYLogUtils.w("company:$company")
我們?cè)?data class 中定義一個(gè)普通的對(duì)象 Address ,我們打印的值如下:

可以看到打印的結(jié)果,默認(rèn)的 data class copy 為淺拷貝。
那么如何實(shí)現(xiàn)深拷貝呢?我們仿造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
}
}
使用的時(shí)候我們不使用copy()而使用clone()
val company = Company("百度", "2001年", UserAddress("北京", "海淀區(qū)"))
YYLogUtils.w("company:$company addressCity:${company.address.city}")
val newCompany = company.clone()
newCompany.name = "網(wǎng)易"
newCompany.address.city = "杭州"
YYLogUtils.w("newCompany:$newCompany addressCity:${newCompany.address.city}")
YYLogUtils.w("company:$company addressCity:${company.address.city}")
那么打印的結(jié)果確實(shí)成了深拷貝,就是我們想要的。

另外一些其他的方法,例如一些第三庫 github.com/bennyhuo/Ko… 給對(duì)象 data class 加注解,讓其實(shí)現(xiàn)深拷貝的邏輯。
還有一些方法是通過Kotlin反射庫的一些方式實(shí)現(xiàn),擴(kuò)展方法如下
//data class 對(duì)象的深拷貝
fun <T : Any> T.deepCopy(): T {
//如果不是數(shù)據(jù)類,直接返回
if (!this::class.isData) {
return this
}
//拿到構(gòu)造函數(shù)
return this::class.primaryConstructor!!.let { primaryConstructor ->
primaryConstructor.parameters.map { parameter ->
//轉(zhuǎn)換類型
//最終value=第一個(gè)參數(shù)類型的對(duì)象
val value = (this::class as KClass<T>).memberProperties.first {
it.name == parameter.name
}.get(this)
//如果當(dāng)前類(這里的當(dāng)前類指的是參數(shù)對(duì)應(yīng)的類型,比如說這里如果非基本類型時(shí))是數(shù)據(jù)類
if ((parameter.type.classifier as? KClass<*>)?.isData == true) {
parameter to value?.deepCopy()
} else {
parameter to value
}
//最終返回一個(gè)新的映射map,即返回一個(gè)屬性值重新組合的map,并調(diào)用callBy返回指定的對(duì)象
}.toMap().let(primaryConstructor::callBy)
}
}
如果報(bào)錯(cuò)的話,可以看看是不是Kotlin反射庫沒有導(dǎo)入,比如我項(xiàng)目的Kotlin版本為1.5.31 , 那么我導(dǎo)入了一個(gè)反射庫依賴如下:
api 'org.jetbrains.kotlin:kotlin-reflect:1.5.31'
使用起來很簡(jiǎn)單,直接使用擴(kuò)展方法即可:
val company = Company("百度", "2001年", Address("北京", "海淀區(qū)"))
YYLogUtils.w("company:$company addressCity:${company.address.city}")
val newCompany = company.deepCopy()
newCompany.name = "網(wǎng)易"
newCompany.address.city = "杭州"
YYLogUtils.w("newCompany:$newCompany addressCity:${newCompany.address.city}")
YYLogUtils.w("company:$company addressCity:${company.address.city}")
打印的結(jié)果符合我們的預(yù)期:

這種方法需要注意的是只支持 data class ,內(nèi)部的對(duì)象也需要時(shí)data class 如果是普通類或Java類的對(duì)象都是不行的。并且大家需要注意的是這么,深拷貝的性能是比淺拷貝要差一點(diǎn)的。特別是使用這樣擴(kuò)展方式的形式,他的性能比重寫 Cloneable 和 clone()這樣的方式性能要差。如果沒有必要還是推薦使用淺拷貝實(shí)現(xiàn)。
比如之前的文章 MVI框架的展示。我們就是使用淺拷貝,我們就是要改變內(nèi)存中List的值
private val _viewStates: MutableLiveData<Demo14ViewState> = MutableLiveData(Demo14ViewState())
//只需要暴露一個(gè)LiveData,包括頁面所有狀態(tài)
val viewStates: LiveData<Demo14ViewState> = _viewStates
//當(dāng)前頁面所需的數(shù)據(jù)與狀態(tài)
data class Demo14ViewState(
val industrys: List<Industry> = emptyList(),
val schools: List<SchoolBean> = emptyList(),
var isChanged: Boolean = false
) : BaseViewState()
//獲取學(xué)校數(shù)據(jù)
private fun requestSchool() {
viewModelScope.launch {
//開始Loading
loadStartLoading()
val result = mRepository.getSchool()
result.checkSuccess {
_viewStates.setState {
copy(schools = it ?: emptyList())
}
}
loadHideProgress()
}
}
例如上文中的copy用法,淺拷貝,當(dāng)我們調(diào)用接口獲取到學(xué)校的是時(shí)候就賦值學(xué)校數(shù)據(jù),淺拷貝一個(gè)對(duì)象,并對(duì)它賦值,再把它設(shè)置給LiveData,當(dāng)我們獲取到行業(yè)數(shù)據(jù)的時(shí)候,一樣的操作,又并不會(huì)對(duì)學(xué)校的數(shù)據(jù)做修改,又修改了原生LiveData中的Value值,因?yàn)閮蓚€(gè)對(duì)象的引用是不同的,這樣就可以觸發(fā)LiveData的通知。也是一個(gè)比較典型的應(yīng)用。
如果是深拷貝,當(dāng)然也能實(shí)現(xiàn)同樣的邏輯,就是會(huì)麻煩一點(diǎn),性能也沒有淺拷貝好,所以這個(gè)場(chǎng)景使用的是簡(jiǎn)單的淺拷貝。
總結(jié)
原型模式本質(zhì)上就是對(duì)象的拷貝,容易出現(xiàn)的問題也都是深拷貝、淺拷貝。使用原型模式可以解決構(gòu)建復(fù)雜對(duì)象的資源消耗問題,能夠在某些場(chǎng)景下提升創(chuàng)建對(duì)象的效率。
優(yōu)點(diǎn):
- 原型模式是在內(nèi)存中二進(jìn)制流的拷貝,要比直接new一個(gè)對(duì)象性能好很多,特別是要在一個(gè)循環(huán)體內(nèi)產(chǎn)生大量對(duì)象時(shí),原型模式可能更好的體現(xiàn)其優(yōu)點(diǎn)。
- 還有一個(gè)重要的用途就是保護(hù)性拷貝,也就是對(duì)某個(gè)對(duì)象對(duì)外可能是只讀的,為了防止外部對(duì)這個(gè)只讀對(duì)象的修改,通??梢酝ㄟ^返回一個(gè)對(duì)象拷貝的形式實(shí)現(xiàn)只讀的限制。
缺點(diǎn):
- 這既是它的優(yōu)點(diǎn)也是缺點(diǎn),直接在內(nèi)存中拷貝,構(gòu)造函數(shù)是不會(huì)執(zhí)行的,在實(shí)際開發(fā)中應(yīng)該注意這個(gè)潛在問題。優(yōu)點(diǎn)是減少了約束,缺點(diǎn)也是減少了約束,需要大家在實(shí)際應(yīng)用時(shí)考慮。
- 通過實(shí)現(xiàn)Cloneable接口的原型模式在調(diào)用clone函數(shù)構(gòu)造實(shí)例時(shí)并不一定比通過new操作速度快,只有當(dāng)通過new構(gòu)造對(duì)象較為耗時(shí)或者說成本較高時(shí),通過clone方法才能夠獲得效率上的提升。
平常的開發(fā)中,淺拷貝和深拷貝都有各自的應(yīng)用場(chǎng)景,如果 class 中都是基礎(chǔ)數(shù)據(jù),那也不需要關(guān)心是淺拷貝還是深拷貝,直接淺拷貝即可,如果包含對(duì)象,那么就要看自己的需求來選擇是淺拷貝還是深拷貝了。
以上就是Android常用設(shè)計(jì)模式之原型模式詳解的詳細(xì)內(nèi)容,更多關(guān)于Android 設(shè)計(jì)模式原型模式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Android10渲染Surface的創(chuàng)建過程
這篇文章主要介紹了基于Android10渲染Surface的創(chuàng)建過程,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-08-08
Flutter進(jìn)階之實(shí)現(xiàn)動(dòng)畫效果(二)
這篇文章主要為大家詳細(xì)介紹了Flutter進(jìn)階之實(shí)現(xiàn)動(dòng)畫效果的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
Android 實(shí)現(xiàn)當(dāng)下最流行的吸頂效果
本文主要介紹了Android 實(shí)現(xiàn)當(dāng)下最流行的吸頂效果的示例代碼。具有很好的參考價(jià)值,下面跟著小編一起來看下吧2017-03-03
Android語音識(shí)別技術(shù)詳解及實(shí)例代碼
這篇文章主要介紹了Android語音識(shí)別技術(shù)的相關(guān)資料,并附實(shí)例代碼及實(shí)例實(shí)現(xiàn)效果圖,需要的朋友可以參考下2016-09-09
Android webview 遇到android.os.FileUriExposedException錯(cuò)誤解決辦法
這篇文章主要介紹了Android webview 遇到android.os.FileUriExposedException錯(cuò)誤解決辦法的相關(guān)資料,希望通過本文能幫助到大家,讓大家遇到這樣的問題解決,需要的朋友可以參考下2017-10-10
Android 使用ViewPager實(shí)現(xiàn)圖片左右循環(huán)滑動(dòng)自動(dòng)播放
這篇文章主要介紹了Android 使用ViewPager實(shí)現(xiàn)圖片左右循環(huán)滑動(dòng)自動(dòng)播放的相關(guān)資料,非常不錯(cuò),具有參考解決價(jià)值,需要的朋友可以參考下2016-08-08
Android系統(tǒng)中的藍(lán)牙連接程序編寫實(shí)例教程
這篇文章主要介紹了Android系統(tǒng)中的藍(lán)牙連接程序編寫實(shí)例教程,包括藍(lán)牙的設(shè)備查找及自動(dòng)配對(duì)等各種基礎(chǔ)功能的實(shí)現(xiàn),十分給力,需要的朋友可以參考下2016-04-04
Android讀取手機(jī)通訊錄聯(lián)系人到自己項(xiàng)目
這篇文章主要為大家詳細(xì)介紹了Android讀取手機(jī)通訊錄聯(lián)系人到自己項(xiàng)目,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07
Android項(xiàng)目實(shí)戰(zhàn)之仿網(wǎng)易新聞的頁面(RecyclerView )
這篇文章主要介紹了Android項(xiàng)目實(shí)戰(zhàn)之仿網(wǎng)易新聞的頁面,ViewPager作為RecyclerView的Header,感興趣的小伙伴們可以參考一下2016-01-01

